From bae802bcfa1e8aca2ae0ebb4f601ab4b6c417b9c Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 13 May 2020 16:56:09 -0300 Subject: [PATCH] [KEYCLOAK-11784] - Using Hibernate Extension --- .../jpa/src/main/resources/META-INF/beans.xml | 0 quarkus/extensions/pom.xml | 84 ++++- .../jpa/QuarkusJpaConnectionProvider.java | 44 --- .../QuarkusJpaConnectionProviderFactory.java | 269 +++++---------- .../liquibase/FastServiceLocator.java | 145 ++++++++ .../connections/liquibase/KeycloakLogger.java | 103 ++++++ .../liquibase/QuarkusJpaUpdaterProvider.java | 325 ++++++++++++++++++ .../QuarkusJpaUpdaterProviderFactory.java | 54 +++ .../QuarkusLiquibaseConnectionProvider.java | 189 ++++++++++ .../src/main/resources/META-INF/beans.xml | 0 ...ions.jpa.updater.JpaUpdaterProviderFactory | 18 + ...se.conn.LiquibaseConnectionProviderFactory | 18 + quarkus/pom.xml | 3 +- quarkus/server/pom.xml | 28 +- .../keycloak/services/resources/Dummy.java | 5 + .../resources/META-INF/keycloak.properties | 13 +- .../main/resources/META-INF/persistence.xml | 93 +++++ .../src/main/resources/application.properties | 2 - .../resources/KeycloakApplication.java | 3 +- .../org/keycloak/testsuite/TestPlatform.java | 2 +- .../provider/wildfly/WildflyPlatform.java | 2 +- 21 files changed, 1148 insertions(+), 252 deletions(-) create mode 100644 model/jpa/src/main/resources/META-INF/beans.xml delete mode 100644 quarkus/extensions/src/main/java/org/keycloak/connections/jpa/QuarkusJpaConnectionProvider.java create mode 100644 quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/FastServiceLocator.java create mode 100644 quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/KeycloakLogger.java create mode 100755 quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/QuarkusJpaUpdaterProvider.java create mode 100755 quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/QuarkusJpaUpdaterProviderFactory.java create mode 100644 quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/QuarkusLiquibaseConnectionProvider.java create mode 100644 quarkus/extensions/src/main/resources/META-INF/beans.xml create mode 100644 quarkus/extensions/src/main/resources/META-INF/services/org.keycloak.connections.jpa.updater.JpaUpdaterProviderFactory create mode 100644 quarkus/extensions/src/main/resources/META-INF/services/org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory create mode 100644 quarkus/server/src/main/resources/META-INF/persistence.xml diff --git a/model/jpa/src/main/resources/META-INF/beans.xml b/model/jpa/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/quarkus/extensions/pom.xml b/quarkus/extensions/pom.xml index a652da7d9d..2931b75f43 100644 --- a/quarkus/extensions/pom.xml +++ b/quarkus/extensions/pom.xml @@ -43,6 +43,21 @@ org.keycloak keycloak-model-jpa + + + * + * + + + + + jakarta.persistence + jakarta.persistence-api + + + org.hibernate + hibernate-core + ${hibernate.version} org.keycloak @@ -75,7 +90,16 @@ io.quarkus quarkus-agroal - jar + + + org.keycloak + keycloak-common + + + * + * + + io.quarkus @@ -91,6 +115,10 @@ + + org.liquibase + liquibase-core + junit junit @@ -100,16 +128,68 @@ + + org.apache.maven.plugins + maven-dependency-plugin + + + extract-liquibase-for-indexing + generate-sources + + unpack-dependencies + + + org.liquibase + ${project.build.directory}/liquibase-extracted + + + + + org.jboss.jandex jandex-maven-plugin - 1.0.6 make-index jandex + process-sources + + liquibase.idx + false + + + ${project.build.directory}/liquibase-extracted + + + + + + + + + maven-resources-plugin + + + copy-liquibase-index + process-resources + + copy-resources + + + ${project.build.directory}/classes/ + + + ${project.build.directory}/liquibase-extracted + + META-INF/liquibase.idx + + false + + + diff --git a/quarkus/extensions/src/main/java/org/keycloak/connections/jpa/QuarkusJpaConnectionProvider.java b/quarkus/extensions/src/main/java/org/keycloak/connections/jpa/QuarkusJpaConnectionProvider.java deleted file mode 100644 index 760bee3cd1..0000000000 --- a/quarkus/extensions/src/main/java/org/keycloak/connections/jpa/QuarkusJpaConnectionProvider.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2020 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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 org.keycloak.connections.jpa; - -import org.jboss.logging.Logger; - -import javax.persistence.EntityManager; - -public class QuarkusJpaConnectionProvider implements JpaConnectionProvider { - - private static final Logger logger = Logger.getLogger(QuarkusJpaConnectionProvider.class); - private final EntityManager em; - - public QuarkusJpaConnectionProvider(EntityManager em) { - this.em = em; - } - - @Override - public EntityManager getEntityManager() { - return em; - } - - @Override - public void close() { - logger.trace("QuarkusJpaConnectionProvider close()"); - em.close(); - } - -} diff --git a/quarkus/extensions/src/main/java/org/keycloak/connections/jpa/QuarkusJpaConnectionProviderFactory.java b/quarkus/extensions/src/main/java/org/keycloak/connections/jpa/QuarkusJpaConnectionProviderFactory.java index 17d0bc7d8e..6058b1db24 100644 --- a/quarkus/extensions/src/main/java/org/keycloak/connections/jpa/QuarkusJpaConnectionProviderFactory.java +++ b/quarkus/extensions/src/main/java/org/keycloak/connections/jpa/QuarkusJpaConnectionProviderFactory.java @@ -17,35 +17,34 @@ package org.keycloak.connections.jpa; -import org.hibernate.cfg.AvailableSettings; -import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; +import static org.keycloak.connections.liquibase.QuarkusJpaUpdaterProvider.VERIFY_AND_RUN_MASTER_CHANGELOG; + +import org.hibernate.internal.SessionFactoryImpl; +import org.hibernate.internal.SessionImpl; import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.ServerStartupError; +import org.keycloak.common.Version; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; -import org.keycloak.connections.jpa.util.JpaUtils; +import org.keycloak.migration.ModelVersion; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.KeycloakSessionTask; -import org.keycloak.models.KeycloakTransactionManager; import org.keycloak.models.dblock.DBLockManager; import org.keycloak.models.dblock.DBLockProvider; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ServerInfoAwareProviderFactory; -import org.keycloak.timer.TimerProvider; import org.keycloak.transaction.JtaTransactionManagerLookup; +import javax.enterprise.inject.Instance; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.SynchronizationType; -import javax.sql.DataSource; import java.io.File; import java.sql.Connection; import java.sql.DatabaseMetaData; +import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; +import java.sql.Statement; import java.util.LinkedHashMap; import java.util.Map; import javax.enterprise.inject.spi.CDI; @@ -57,11 +56,13 @@ public class QuarkusJpaConnectionProviderFactory implements JpaConnectionProvide private static final Logger logger = Logger.getLogger(QuarkusJpaConnectionProviderFactory.class); + private static final String SQL_GET_LATEST_VERSION = "SELECT VERSION FROM %sMIGRATION_MODEL"; + enum MigrationStrategy { UPDATE, VALIDATE, MANUAL } - private volatile EntityManagerFactory emf; + private EntityManagerFactory emf; private Config.Scope config; @@ -75,19 +76,22 @@ public class QuarkusJpaConnectionProviderFactory implements JpaConnectionProvide @Override public JpaConnectionProvider create(KeycloakSession session) { logger.trace("Create QuarkusJpaConnectionProvider"); - lazyInit(session); - EntityManager em; if (!jtaEnabled) { logger.trace("enlisting EntityManager in JpaKeycloakTransaction"); em = emf.createEntityManager(); + try { + SessionImpl.class.cast(em).connection().setAutoCommit(false); + } catch (SQLException cause) { + throw new RuntimeException(cause); + } } else { em = emf.createEntityManager(SynchronizationType.SYNCHRONIZED); } em = PersistenceExceptionConverter.create(em); if (!jtaEnabled) session.getTransactionManager().enlist(new JpaKeycloakTransaction(em)); - return new QuarkusJpaConnectionProvider(em); + return new DefaultJpaConnectionProvider(em); } @Override @@ -111,11 +115,7 @@ public class QuarkusJpaConnectionProviderFactory implements JpaConnectionProvide public void postInit(KeycloakSessionFactory factory) { this.factory = factory; checkJtaEnabled(factory); - } - - @Override - public int order() { - return 100; + lazyInit(); } protected void checkJtaEnabled(KeycloakSessionFactory factory) { @@ -127,78 +127,8 @@ public class QuarkusJpaConnectionProviderFactory implements JpaConnectionProvide } } - private void lazyInit(KeycloakSession session) { - if (emf == null) { - synchronized (this) { - if (emf == null) { - KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> { - logger.debug("Initializing Quarkus JPA connections"); - - Map properties = new HashMap<>(); - - String unitName = "keycloak-default"; - - String schema = getSchema(); - if (schema != null) { - properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema); - } - - MigrationStrategy migrationStrategy = getMigrationStrategy(); - boolean initializeEmpty = config.getBoolean("initializeEmpty", true); - File databaseUpdateFile = getDatabaseUpdateFile(); - - properties.put("hibernate.show_sql", config.getBoolean("showSql", false)); - properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); - properties.put(AvailableSettings.CONNECTION_HANDLING, - PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT); - - properties.put(AvailableSettings.DATASOURCE, getDataSource()); - - Connection connection = getConnection(); - try { - prepareOperationalInfo(connection); - - String driverDialect = detectDialect(connection); - if (driverDialect != null) { - properties.put("hibernate.dialect", driverDialect); - } - - migration(migrationStrategy, initializeEmpty, schema, databaseUpdateFile, connection, session); - - int globalStatsInterval = config.getInt("globalStatsInterval", -1); - if (globalStatsInterval != -1) { - properties.put("hibernate.generate_statistics", true); - } - - logger.trace("Creating EntityManagerFactory"); - logger.tracev("***** create EMF jtaEnabled {0} ", jtaEnabled); - - Collection classLoaders = new ArrayList<>(); - if (properties.containsKey(AvailableSettings.CLASSLOADERS)) { - classLoaders.addAll((Collection) properties.get(AvailableSettings.CLASSLOADERS)); - } - classLoaders.add(getClass().getClassLoader()); - properties.put(AvailableSettings.CLASSLOADERS, classLoaders); - emf = JpaUtils.createEntityManagerFactory(session, unitName, properties, jtaEnabled); - logger.trace("EntityManagerFactory created"); - - if (globalStatsInterval != -1) { - startGlobalStats(session, globalStatsInterval); - } - } finally { - // Close after creating EntityManagerFactory to prevent in-mem databases from closing - if (connection != null) { - try { - connection.close(); - } catch (SQLException e) { - logger.warn("Can't close connection", e); - } - } - } - }); - } - } - } + private String getSchema(String schema) { + return schema == null ? "" : schema + "."; } private File getDatabaseUpdateFile() { @@ -221,56 +151,31 @@ public class QuarkusJpaConnectionProviderFactory implements JpaConnectionProvide } } + void migration(String schema, Connection connection, KeycloakSession session) { + MigrationStrategy strategy = getMigrationStrategy(); + boolean initializeEmpty = config.getBoolean("initializeEmpty", true); + File databaseUpdateFile = getDatabaseUpdateFile(); - protected String detectDialect(Connection connection) { - String driverDialect = config.get("driverDialect"); - if (driverDialect != null && driverDialect.length() > 0) { - return driverDialect; - } else { - try { - String dbProductName = connection.getMetaData().getDatabaseProductName(); - String dbProductVersion = connection.getMetaData().getDatabaseProductVersion(); + String version = null; - // For MSSQL2014, we may need to fix the autodetected dialect by hibernate - if (dbProductName.equals("Microsoft SQL Server")) { - String topVersionStr = dbProductVersion.split("\\.")[0]; - boolean shouldSet2012Dialect = true; - try { - int topVersion = Integer.parseInt(topVersionStr); - if (topVersion < 12) { - shouldSet2012Dialect = false; - } - } catch (NumberFormatException nfe) { - } - if (shouldSet2012Dialect) { - String sql2012Dialect = "org.hibernate.dialect.SQLServer2012Dialect"; - logger.debugf("Manually override hibernate dialect to %s", sql2012Dialect); - return sql2012Dialect; + try { + try (Statement statement = connection.createStatement()) { + try (ResultSet rs = statement.executeQuery(String.format(SQL_GET_LATEST_VERSION, getSchema(schema)))) { + if (rs.next()) { + version = rs.getString(1); } } - // For Oracle19c, we may need to set dialect explicitly to workaround https://hibernate.atlassian.net/browse/HHH-13184 - if (dbProductName.equals("Oracle") && connection.getMetaData().getDatabaseMajorVersion() > 12) { - logger.debugf("Manually specify dialect for Oracle to org.hibernate.dialect.Oracle12cDialect"); - return "org.hibernate.dialect.Oracle12cDialect"; - } - } catch (SQLException e) { - logger.warnf("Unable to detect hibernate dialect due database exception : %s", e.getMessage()); } - - return null; + } catch (SQLException ignore) { + // migration model probably does not exist so we assume the database is empty } - } - protected void startGlobalStats(KeycloakSession session, int globalStatsIntervalSecs) { - logger.debugf("Started Hibernate statistics with the interval %s seconds", globalStatsIntervalSecs); - TimerProvider timer = session.getProvider(TimerProvider.class); - timer.scheduleTask(new HibernateStatsReporter(emf), globalStatsIntervalSecs * 1000, "ReportHibernateGlobalStats"); - } - - void migration(MigrationStrategy strategy, boolean initializeEmpty, String schema, File databaseUpdateFile, Connection connection, KeycloakSession session) { JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); + session.setAttribute(VERIFY_AND_RUN_MASTER_CHANGELOG, version == null || !version.equals(new ModelVersion(Version.VERSION_KEYCLOAK).toString())); + JpaUpdaterProvider.Status status = updater.validate(connection, schema); + if (status == JpaUpdaterProvider.Status.VALID) { logger.debug("Database is up-to-date"); } else if (status == JpaUpdaterProvider.Status.EMPTY) { @@ -303,39 +208,36 @@ public class QuarkusJpaConnectionProviderFactory implements JpaConnectionProvide } protected void update(Connection connection, String schema, KeycloakSession session, JpaUpdaterProvider updater) { - runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession lockSession) -> { - DBLockManager dbLockManager = new DBLockManager(lockSession); - DBLockProvider dbLock2 = dbLockManager.getDBLock(); - dbLock2.waitForLock(DBLockProvider.Namespace.DATABASE); - try { - updater.update(connection, schema); - } finally { - dbLock2.releaseLock(); - } - }, KeycloakTransactionManager.JTAPolicy.NOT_SUPPORTED); + DBLockManager dbLockManager = new DBLockManager(session); + DBLockProvider dbLock2 = dbLockManager.getDBLock(); + dbLock2.waitForLock(DBLockProvider.Namespace.DATABASE); + try { + updater.update(connection, schema); + } finally { + dbLock2.releaseLock(); + } } - protected void export(Connection connection, String schema, File databaseUpdateFile, KeycloakSession session, JpaUpdaterProvider updater) { - runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession lockSession) -> { - DBLockManager dbLockManager = new DBLockManager(lockSession); - DBLockProvider dbLock2 = dbLockManager.getDBLock(); - dbLock2.waitForLock(DBLockProvider.Namespace.DATABASE); - try { - updater.export(connection, schema, databaseUpdateFile); - } finally { - dbLock2.releaseLock(); - } - }, KeycloakTransactionManager.JTAPolicy.NOT_SUPPORTED); + protected void export(Connection connection, String schema, File databaseUpdateFile, KeycloakSession session, + JpaUpdaterProvider updater) { + DBLockManager dbLockManager = new DBLockManager(session); + DBLockProvider dbLock2 = dbLockManager.getDBLock(); + dbLock2.waitForLock(DBLockProvider.Namespace.DATABASE); + try { + updater.export(connection, schema, databaseUpdateFile); + } finally { + dbLock2.releaseLock(); + } } @Override public Connection getConnection() { + SessionFactoryImpl entityManagerFactory = SessionFactoryImpl.class.cast(emf); + try { - DataSource dataSource = getDataSource(); - logger.tracev("CDI DataSource: {0}", dataSource); - return dataSource.getConnection(); - } catch (Exception e) { - throw new RuntimeException("Failed to connect to database", e); + return entityManagerFactory.getJdbcServices().getBootstrapJdbcConnectionAccess().obtainConnection(); + } catch (SQLException cause) { + throw new RuntimeException("Failed to obtain JDBC connection", cause); } } @@ -363,35 +265,38 @@ public class QuarkusJpaConnectionProviderFactory implements JpaConnectionProvide } } - private DataSource getDataSource() { - return CDI.current().select(DataSource.class).get(); - } + private void lazyInit() { + Instance instance = CDI.current().select(EntityManagerFactory.class); - private static void runJobInTransaction(KeycloakSessionFactory factory, KeycloakSessionTask task, KeycloakTransactionManager.JTAPolicy jtaPolicy) { - KeycloakSession session = factory.create(); - KeycloakTransactionManager tx = session.getTransactionManager(); - if (jtaPolicy != null) - tx.setJTAPolicy(jtaPolicy); - - try { - tx.begin(); - task.run(session); + if (!instance.isResolvable()) { + throw new RuntimeException("Failed to resolve " + EntityManagerFactory.class + " from Quarkus runtime"); + } - if (tx.isActive()) { - if (tx.getRollbackOnly()) { - tx.rollback(); - } else { - tx.commit(); - } + emf = instance.get(); + + try (Connection connection = getConnection()) { + if (jtaEnabled) { + KeycloakModelUtils.suspendJtaTransaction(factory, () -> { + KeycloakSession session = factory.create(); + try { + migration(getSchema(), connection, session); + } finally { + session.close(); + } + }); + } else { + KeycloakModelUtils.runJobInTransaction(factory, session -> { + migration(getSchema(), connection, session); + }); } - } catch (RuntimeException re) { - if (tx.isActive()) { - tx.rollback(); - } - throw re; - } finally { - session.close(); + prepareOperationalInfo(connection); + } catch (SQLException cause) { + throw new RuntimeException("Failed to migrate model", cause); } } + @Override + public int order() { + return 100; + } } diff --git a/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/FastServiceLocator.java b/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/FastServiceLocator.java new file mode 100644 index 0000000000..1b19de65ba --- /dev/null +++ b/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/FastServiceLocator.java @@ -0,0 +1,145 @@ +package org.keycloak.connections.liquibase; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import liquibase.exception.ServiceNotFoundException; +import liquibase.parser.core.xml.XMLChangeLogSAXParser; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.IndexReader; +import org.keycloak.connections.jpa.updater.liquibase.lock.CustomInsertLockRecordGenerator; +import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockDatabaseChangeLogGenerator; +import org.keycloak.connections.jpa.updater.liquibase.lock.DummyLockService; + +import liquibase.database.Database; +import liquibase.lockservice.LockService; +import liquibase.logging.Logger; +import liquibase.parser.ChangeLogParser; +import liquibase.servicelocator.DefaultPackageScanClassResolver; +import liquibase.servicelocator.LiquibaseService; +import liquibase.servicelocator.ServiceLocator; +import liquibase.sqlgenerator.SqlGenerator; + +public class FastServiceLocator extends ServiceLocator { + + private static Map> CLASS_INDEX = new HashMap<>(); + + static { + DotName liquibaseServiceName = DotName.createSimple(LiquibaseService.class.getName()); + + try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream("META-INF/liquibase.idx")) { + IndexReader reader = new IndexReader(in); + Index index = reader.read(); + for (Class c : Arrays.asList(liquibase.diff.compare.DatabaseObjectComparator.class, + liquibase.parser.NamespaceDetails.class, + liquibase.precondition.Precondition.class, + Database.class, + ChangeLogParser.class, + liquibase.change.Change.class, + liquibase.snapshot.SnapshotGenerator.class, + liquibase.changelog.ChangeLogHistoryService.class, + liquibase.datatype.LiquibaseDataType.class, + liquibase.executor.Executor.class, + LockService.class, + SqlGenerator.class)) { + List impls = new ArrayList<>(); + CLASS_INDEX.put(c.getName(), impls); + Set classes = new HashSet<>(); + if (c.isInterface()) { + classes.addAll(index.getAllKnownImplementors(DotName.createSimple(c.getName()))); + } else { + classes.addAll(index.getAllKnownSubclasses(DotName.createSimple(c.getName()))); + } + for (ClassInfo found : classes) { + if (Modifier.isAbstract(found.flags()) || + Modifier.isInterface(found.flags()) || + !found.hasNoArgsConstructor() || + !Modifier.isPublic(found.flags())) { + continue; + } + AnnotationInstance annotationInstance = found.classAnnotation(liquibaseServiceName); + if (annotationInstance == null || !annotationInstance.value("skip").asBoolean()) { + impls.add(found.name().toString()); + } + } + } + } catch (IOException cause) { + throw new RuntimeException("Failed to get liquibase jandex index", cause); + } + + CLASS_INDEX.put(Logger.class.getName(), Arrays.asList(KeycloakLogger.class.getName())); + CLASS_INDEX.put(LockService.class.getName(), Arrays.asList(DummyLockService.class.getName())); + CLASS_INDEX.put(ChangeLogParser.class.getName(), Arrays.asList(XMLChangeLogSAXParser.class.getName())); + CLASS_INDEX.get(SqlGenerator.class.getName()).add(CustomInsertLockRecordGenerator.class.getName()); + CLASS_INDEX.get(SqlGenerator.class.getName()).add(CustomLockDatabaseChangeLogGenerator.class.getName()); + } + + protected FastServiceLocator() { + super(new DefaultPackageScanClassResolver() { + @Override + public Set> findImplementations(Class parent, String... packageNames) { + List found = CLASS_INDEX.get(parent.getName()); + + if (found == null) { + return super.findImplementations(parent, packageNames); + } + + Set> ret = new HashSet<>(); + for (String i : found) { + try { + ret.add(Class.forName(i, false, Thread.currentThread().getContextClassLoader())); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + return ret; + } + }); + + if (!System.getProperties().containsKey("liquibase.scan.packages")) { + if (getPackages().remove("liquibase.core")) { + addPackageToScan("liquibase.core.xml"); + } + + if (getPackages().remove("liquibase.parser")) { + addPackageToScan("liquibase.parser.core.xml"); + } + + if (getPackages().remove("liquibase.serializer")) { + addPackageToScan("liquibase.serializer.core.xml"); + } + + getPackages().remove("liquibase.ext"); + getPackages().remove("liquibase.sdk"); + } + + // we only need XML parsers + getPackages().remove("liquibase.parser.core.yaml"); + getPackages().remove("liquibase.serializer.core.yaml"); + getPackages().remove("liquibase.parser.core.json"); + getPackages().remove("liquibase.serializer.core.json"); + } + + @Override + public Object newInstance(Class requiredInterface) throws ServiceNotFoundException { + if (Logger.class.equals(requiredInterface)) { + return new KeycloakLogger(); + } + return super.newInstance(requiredInterface); + } + + public void register(Class type) { + CLASS_INDEX.put(Database.class.getName(), Arrays.asList(type.getName())); + } +} diff --git a/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/KeycloakLogger.java b/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/KeycloakLogger.java new file mode 100644 index 0000000000..45922b4734 --- /dev/null +++ b/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/KeycloakLogger.java @@ -0,0 +1,103 @@ +package org.keycloak.connections.liquibase; + +import liquibase.changelog.ChangeSet; +import liquibase.changelog.DatabaseChangeLog; +import liquibase.logging.LogLevel; +import liquibase.logging.Logger; + +public class KeycloakLogger implements Logger { + + private static final org.jboss.logging.Logger logger = org.jboss.logging.Logger.getLogger(QuarkusLiquibaseConnectionProvider.class); + + @Override + public void setName(String name) { + } + + @Override + public void setLogLevel(String logLevel, String logFile) { + } + + @Override + public void severe(String message) { + logger.error(message); + } + + @Override + public void severe(String message, Throwable e) { + logger.error(message, e); + } + + @Override + public void warning(String message) { + // Ignore this warning as cascaded drops doesn't work anyway with all DBs, which we need to support + if ("Database does not support drop with cascade".equals(message)) { + logger.debug(message); + } else { + logger.warn(message); + } + } + + @Override + public void warning(String message, Throwable e) { + logger.warn(message, e); + } + + @Override + public void info(String message) { + logger.debug(message); + } + + @Override + public void info(String message, Throwable e) { + logger.debug(message, e); + } + + @Override + public void debug(String message) { + if (logger.isTraceEnabled()) { + logger.trace(message); + } + } + + @Override + public LogLevel getLogLevel() { + if (logger.isTraceEnabled()) { + return LogLevel.DEBUG; + } else if (logger.isDebugEnabled()) { + return LogLevel.INFO; + } else { + return LogLevel.WARNING; + } + } + + @Override + public void setLogLevel(String level) { + } + + @Override + public void setLogLevel(LogLevel level) { + } + + @Override + public void debug(String message, Throwable e) { + logger.trace(message, e); + } + + @Override + public void setChangeLog(DatabaseChangeLog databaseChangeLog) { + } + + @Override + public void setChangeSet(ChangeSet changeSet) { + } + + @Override + public int getPriority() { + return 0; + } + + @Override + public void closeLogFile() { + } + +} diff --git a/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/QuarkusJpaUpdaterProvider.java b/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/QuarkusJpaUpdaterProvider.java new file mode 100755 index 0000000000..d3859a4670 --- /dev/null +++ b/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/QuarkusJpaUpdaterProvider.java @@ -0,0 +1,325 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.connections.liquibase; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.reflections.Reflections; +import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider; +import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; +import org.keycloak.connections.jpa.updater.liquibase.ThreadLocalSessionContext; +import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider; +import org.keycloak.connections.jpa.util.JpaUtils; +import org.keycloak.models.KeycloakSession; + +import liquibase.Contexts; +import liquibase.LabelExpression; +import liquibase.Liquibase; +import liquibase.changelog.ChangeLogHistoryService; +import liquibase.changelog.ChangeLogHistoryServiceFactory; +import liquibase.changelog.ChangeSet; +import liquibase.changelog.RanChangeSet; +import liquibase.database.Database; +import liquibase.exception.DatabaseException; +import liquibase.exception.LiquibaseException; +import liquibase.executor.Executor; +import liquibase.executor.ExecutorService; +import liquibase.executor.LoggingExecutor; +import liquibase.snapshot.SnapshotControl; +import liquibase.snapshot.SnapshotGeneratorFactory; +import liquibase.statement.SqlStatement; +import liquibase.statement.core.AddColumnStatement; +import liquibase.statement.core.CreateDatabaseChangeLogTableStatement; +import liquibase.statement.core.SetNullableStatement; +import liquibase.statement.core.UpdateStatement; +import liquibase.structure.core.Column; +import liquibase.structure.core.Table; +import liquibase.util.StreamUtil; + +public class QuarkusJpaUpdaterProvider implements JpaUpdaterProvider { + + private static final Logger logger = Logger.getLogger(QuarkusJpaUpdaterProvider.class); + + public static final String CHANGELOG = "META-INF/jpa-changelog-master.xml"; + private static final String DEPLOYMENT_ID_COLUMN = "DEPLOYMENT_ID"; + public static final String VERIFY_AND_RUN_MASTER_CHANGELOG = "VERIFY_AND_RUN_MASTER_CHANGELOG"; + + private final KeycloakSession session; + private Map> changeSets = new HashMap<>(); + + public QuarkusJpaUpdaterProvider(KeycloakSession session) { + this.session = session; + } + + @Override + public void update(Connection connection, String defaultSchema) { + update(connection, null, defaultSchema); + } + + @Override + public void export(Connection connection, String defaultSchema, File file) { + update(connection, file, defaultSchema); + } + + private void update(Connection connection, File file, String defaultSchema) { + logger.debug("Starting database update"); + + // Need ThreadLocal as liquibase doesn't seem to have API to inject custom objects into tasks + ThreadLocalSessionContext.setCurrentSession(session); + + Writer exportWriter = null; + try { + if (needVerifyMasterChangelog()) { + // Run update with keycloak master changelog first + Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema); + if (file != null) { + exportWriter = new FileWriter(file); + } + updateChangeSet(liquibase, exportWriter); + } + + // Run update for each custom JpaEntityProvider + Set jpaProviders = session.getAllProviders(JpaEntityProvider.class); + for (JpaEntityProvider jpaProvider : jpaProviders) { + String customChangelog = jpaProvider.getChangelogLocation(); + if (customChangelog != null) { + String factoryId = jpaProvider.getFactoryId(); + String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId); + Liquibase liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName); + updateChangeSet(liquibase, exportWriter); + } + } + } catch (LiquibaseException | IOException e) { + throw new RuntimeException("Failed to update database", e); + } finally { + ThreadLocalSessionContext.removeCurrentSession(); + if (exportWriter != null) { + try { + exportWriter.close(); + } catch (IOException ioe) { + // ignore + } + } + } + } + + private Boolean needVerifyMasterChangelog() { + return session.getAttributeOrDefault(VERIFY_AND_RUN_MASTER_CHANGELOG, Boolean.TRUE); + } + + protected void updateChangeSet(Liquibase liquibase, Writer exportWriter) throws LiquibaseException { + String changelog = liquibase.getChangeLogFile(); + Database database = liquibase.getDatabase(); + Table changelogTable = SnapshotGeneratorFactory.getInstance().getDatabaseChangeLogTable(new SnapshotControl(database, false, Table.class, Column.class), database); + + if (changelogTable != null) { + boolean hasDeploymentIdColumn = changelogTable.getColumn(DEPLOYMENT_ID_COLUMN) != null; + + // create DEPLOYMENT_ID column if it doesn't exist + if (!hasDeploymentIdColumn) { + ChangeLogHistoryService changelogHistoryService = ChangeLogHistoryServiceFactory.getInstance().getChangeLogService(database); + changelogHistoryService.generateDeploymentId(); + String deploymentId = changelogHistoryService.getDeploymentId(); + + logger.debugv("Adding missing column {0}={1} to {2} table", DEPLOYMENT_ID_COLUMN, deploymentId,changelogTable.getName()); + + List statementsToExecute = new ArrayList<>(); + statementsToExecute.add(new AddColumnStatement(database.getLiquibaseCatalogName(), database.getLiquibaseSchemaName(), + changelogTable.getName(), DEPLOYMENT_ID_COLUMN, "VARCHAR(10)", null)); + statementsToExecute.add(new UpdateStatement(database.getLiquibaseCatalogName(), database.getLiquibaseSchemaName(), changelogTable.getName()) + .addNewColumnValue(DEPLOYMENT_ID_COLUMN, deploymentId)); + statementsToExecute.add(new SetNullableStatement(database.getLiquibaseCatalogName(), database.getLiquibaseSchemaName(), + changelogTable.getName(), DEPLOYMENT_ID_COLUMN, "VARCHAR(10)", false)); + + ExecutorService executorService = ExecutorService.getInstance(); + Executor executor = executorService.getExecutor(liquibase.getDatabase()); + + for (SqlStatement sql : statementsToExecute) { + executor.execute(sql); + database.commit(); + } + } + } + + List changeSets = getLiquibaseUnrunChangeSets(liquibase); + if (!changeSets.isEmpty()) { + List ranChangeSets = liquibase.getDatabase().getRanChangeSetList(); + if (ranChangeSets.isEmpty()) { + logger.infov("Initializing database schema. Using changelog {0}", changelog); + } else { + if (logger.isDebugEnabled()) { + logger.debugv("Updating database from {0} to {1}. Using changelog {2}", ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog); + } else { + logger.infov("Updating database. Using changelog {0}", changelog); + } + } + + if (exportWriter != null) { + if (ranChangeSets.isEmpty()) { + outputChangeLogTableCreationScript(liquibase, exportWriter); + } + liquibase.update((Contexts) null, new LabelExpression(), exportWriter, false); + } else { + liquibase.update((Contexts) null); + } + + logger.debugv("Completed database update for changelog {0}", changelog); + } else { + logger.debugv("Database is up to date for changelog {0}", changelog); + } + + // Needs to restart liquibase services to clear ChangeLogHistoryServiceFactory.getInstance(). + // See https://issues.jboss.org/browse/KEYCLOAK-3769 for discussion relevant to why reset needs to be here + resetLiquibaseServices(liquibase); + } + + private void outputChangeLogTableCreationScript(Liquibase liquibase, final Writer exportWriter) throws DatabaseException { + Database database = liquibase.getDatabase(); + + Executor oldTemplate = ExecutorService.getInstance().getExecutor(database); + LoggingExecutor executor = new LoggingExecutor(ExecutorService.getInstance().getExecutor(database), exportWriter, database); + ExecutorService.getInstance().setExecutor(database, executor); + + executor.comment("*********************************************************************"); + executor.comment("* Keycloak database creation script - apply this script to empty DB *"); + executor.comment("*********************************************************************" + StreamUtil.getLineSeparator()); + + executor.execute(new CreateDatabaseChangeLogTableStatement()); + // DatabaseChangeLogLockTable is created before this code is executed and recreated if it does not exist automatically + // in org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockService.init() called indirectly from + // KeycloakApplication constructor (search for waitForLock() call). Hence it is not included in the creation script. + + executor.comment("*********************************************************************" + StreamUtil.getLineSeparator()); + + ExecutorService.getInstance().setExecutor(database, oldTemplate); + } + + @Override + public Status validate(Connection connection, String defaultSchema) { + logger.debug("Validating if database is updated"); + ThreadLocalSessionContext.setCurrentSession(session); + + try { + if (needVerifyMasterChangelog()) { + // Validate with keycloak master changelog first + Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema); + + Status status = validateChangeSet(liquibase, liquibase.getChangeLogFile()); + if (status != Status.VALID) { + return status; + } + } + + // Validate each custom JpaEntityProvider + Set jpaProviders = session.getAllProviders(JpaEntityProvider.class); + for (JpaEntityProvider jpaProvider : jpaProviders) { + String customChangelog = jpaProvider.getChangelogLocation(); + if (customChangelog != null) { + String factoryId = jpaProvider.getFactoryId(); + String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId); + Liquibase liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName); + if (validateChangeSet(liquibase, liquibase.getChangeLogFile()) != Status.VALID) { + return Status.OUTDATED; + } + } + } + } catch (LiquibaseException e) { + throw new RuntimeException("Failed to validate database", e); + } + + return Status.VALID; + } + + protected Status validateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException { + final Status result; + List changeSets = getLiquibaseUnrunChangeSets(liquibase); + + if (!changeSets.isEmpty()) { + if (changeSets.size() == liquibase.getDatabaseChangeLog().getChangeSets().size()) { + result = Status.EMPTY; + } else { + logger.debugf("Validation failed. Database is not up-to-date for changelog %s", changelog); + result = Status.OUTDATED; + } + } else { + logger.debugf("Validation passed. Database is up-to-date for changelog %s", changelog); + result = Status.VALID; + } + + // Needs to restart liquibase services to clear ChangeLogHistoryServiceFactory.getInstance(). + // See https://issues.jboss.org/browse/KEYCLOAK-3769 for discussion relevant to why reset needs to be here + resetLiquibaseServices(liquibase); + + return result; + } + + private void resetLiquibaseServices(Liquibase liquibase) { + Method resetServices = Reflections.findDeclaredMethod(Liquibase.class, "resetServices"); + Reflections.invokeMethod(true, resetServices, liquibase); + } + + @SuppressWarnings("unchecked") + private List getLiquibaseUnrunChangeSets(Liquibase liquibase) { + // we don't need to fetch change sets if they were previously obtained + return changeSets.computeIfAbsent(liquibase.getChangeLogFile(), new Function>() { + @Override + public List apply(String s) { + // TODO tracked as: https://issues.jboss.org/browse/KEYCLOAK-3730 + // TODO: When https://liquibase.jira.com/browse/CORE-2919 is resolved, replace the following two lines with: + // List changeSets = liquibase.listUnrunChangeSets((Contexts) null, new LabelExpression(), false); + Method listUnrunChangeSets = Reflections.findDeclaredMethod(Liquibase.class, "listUnrunChangeSets", Contexts.class, LabelExpression.class, boolean.class); + + return Reflections + .invokeMethod(true, listUnrunChangeSets, List.class, liquibase, (Contexts) null, new LabelExpression(), false); + } + }); + } + + private Liquibase getLiquibaseForKeycloakUpdate(Connection connection, String defaultSchema) throws LiquibaseException { + LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class); + return liquibaseProvider.getLiquibase(connection, defaultSchema); + } + + private Liquibase getLiquibaseForCustomProviderUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException { + LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class); + return liquibaseProvider.getLiquibaseForCustomUpdate(connection, defaultSchema, changelogLocation, classloader, changelogTableName); + } + + @Override + public void close() { + changeSets.clear(); + changeSets = null; + } + + public static String getTable(String table, String defaultSchema) { + return defaultSchema != null ? defaultSchema + "." + table : table; + } + +} diff --git a/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/QuarkusJpaUpdaterProviderFactory.java b/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/QuarkusJpaUpdaterProviderFactory.java new file mode 100755 index 0000000000..d7c4b49486 --- /dev/null +++ b/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/QuarkusJpaUpdaterProviderFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.connections.liquibase; + +import org.keycloak.Config; +import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; +import org.keycloak.connections.jpa.updater.JpaUpdaterProviderFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +/** + * @author Stian Thorgersen + */ +public class QuarkusJpaUpdaterProviderFactory implements JpaUpdaterProviderFactory { + + @Override + public JpaUpdaterProvider create(KeycloakSession session) { + return new QuarkusJpaUpdaterProvider(session); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "liquibase"; + } + +} diff --git a/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/QuarkusLiquibaseConnectionProvider.java b/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/QuarkusLiquibaseConnectionProvider.java new file mode 100644 index 0000000000..51ae6c3031 --- /dev/null +++ b/quarkus/extensions/src/main/java/org/keycloak/connections/liquibase/QuarkusLiquibaseConnectionProvider.java @@ -0,0 +1,189 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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 org.keycloak.connections.liquibase; + +import java.lang.reflect.Method; +import java.sql.Connection; + +import javax.xml.parsers.SAXParserFactory; + +import liquibase.database.core.MariaDBDatabase; +import liquibase.database.core.MySQLDatabase; +import liquibase.database.core.PostgresDatabase; +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.connections.jpa.JpaConnectionProviderFactory; +import org.keycloak.connections.jpa.updater.liquibase.MySQL8VarcharType; +import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase; +import org.keycloak.connections.jpa.updater.liquibase.UpdatedMariaDBDatabase; +import org.keycloak.connections.jpa.updater.liquibase.UpdatedMySqlDatabase; +import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider; +import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +import liquibase.Liquibase; +import liquibase.database.Database; +import liquibase.database.DatabaseFactory; +import liquibase.database.jvm.JdbcConnection; +import liquibase.datatype.DataTypeFactory; +import liquibase.exception.LiquibaseException; +import liquibase.logging.LogFactory; +import liquibase.parser.ChangeLogParser; +import liquibase.parser.ChangeLogParserFactory; +import liquibase.parser.core.xml.XMLChangeLogSAXParser; +import liquibase.resource.ClassLoaderResourceAccessor; +import liquibase.resource.ResourceAccessor; +import liquibase.servicelocator.ServiceLocator; + +public class QuarkusLiquibaseConnectionProvider implements LiquibaseConnectionProviderFactory, LiquibaseConnectionProvider { + + private static final Logger logger = Logger.getLogger(QuarkusLiquibaseConnectionProvider.class); + + private volatile boolean initialized = false; + private ClassLoaderResourceAccessor resourceAccessor; + + @Override + public LiquibaseConnectionProvider create(KeycloakSession session) { + if (!initialized) { + synchronized (this) { + if (!initialized) { + baseLiquibaseInitialization(session); + initialized = true; + } + } + } + return this; + } + + protected void baseLiquibaseInitialization(KeycloakSession session) { + LogFactory.setInstance(new LogFactory() { + KeycloakLogger logger = new KeycloakLogger(); + + @Override + public liquibase.logging.Logger getLog(String name) { + return logger; + } + + @Override + public liquibase.logging.Logger getLog() { + return logger; + } + }); + + resourceAccessor = new ClassLoaderResourceAccessor(getClass().getClassLoader()); + FastServiceLocator locator = new FastServiceLocator(); + ServiceLocator.setInstance(locator); + + JpaConnectionProviderFactory jpaConnectionProvider = (JpaConnectionProviderFactory) session + .getKeycloakSessionFactory().getProviderFactory(JpaConnectionProvider.class); + + // registers only the database we are using + try (Connection connection = jpaConnectionProvider.getConnection()) { + Database database = DatabaseFactory.getInstance() + .findCorrectDatabaseImplementation(new JdbcConnection(connection)); + DatabaseFactory.getInstance().clearRegistry(); + if (database.getDatabaseProductName().equals(PostgresDatabase.PRODUCT_NAME)) { + // Adding PostgresPlus support to liquibase + DatabaseFactory.getInstance().register(new PostgresPlusDatabase()); + } else if (database.getDatabaseProductName().equals(MySQLDatabase.PRODUCT_NAME)) { + // Adding newer version of MySQL/MariaDB support to liquibase + DatabaseFactory.getInstance().register(new UpdatedMySqlDatabase()); + // Adding CustomVarcharType for MySQL 8 and newer + DataTypeFactory.getInstance().register(MySQL8VarcharType.class); + } else if (database.getDatabaseProductName().equals(MariaDBDatabase.PRODUCT_NAME)) { + DatabaseFactory.getInstance().register(new UpdatedMariaDBDatabase()); + // Adding CustomVarcharType for MySQL 8 and newer + DataTypeFactory.getInstance().register(MySQL8VarcharType.class); + } else { + DatabaseFactory.getInstance().register(database); + } + + locator.register(database.getClass()); + + // disables XML validation + for (ChangeLogParser parser : ChangeLogParserFactory.getInstance().getParsers()) { + if (parser instanceof XMLChangeLogSAXParser) { + Method getSaxParserFactory = null; + try { + getSaxParserFactory = XMLChangeLogSAXParser.class.getDeclaredMethod("getSaxParserFactory"); + getSaxParserFactory.setAccessible(true); + SAXParserFactory saxParserFactory = (SAXParserFactory) getSaxParserFactory.invoke(parser); + saxParserFactory.setValidating(false); + } catch (Exception e) { + logger.warnf("Failed to disable liquibase XML validations"); + } finally { + if (getSaxParserFactory != null) { + getSaxParserFactory.setAccessible(false); + } + } + } + } + } catch (Exception cause) { + throw new RuntimeException("Failed to configure Liquibase database", cause); + } + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "default"; + } + + @Override + public Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException { + Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection)); + if (defaultSchema != null) { + database.setDefaultSchemaName(defaultSchema); + } + + String changelog = QuarkusJpaUpdaterProvider.CHANGELOG; + + logger.debugf("Using changelog file %s and changelogTableName %s", changelog, database.getDatabaseChangeLogTableName()); + + return new Liquibase(changelog, resourceAccessor, database); + } + + @Override + public Liquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException { + Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection)); + if (defaultSchema != null) { + database.setDefaultSchemaName(defaultSchema); + } + + ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(classloader); + database.setDatabaseChangeLogTableName(changelogTableName); + + logger.debugf("Using changelog file %s and changelogTableName %s", changelogLocation, database.getDatabaseChangeLogTableName()); + + return new Liquibase(changelogLocation, resourceAccessor, database); + } +} diff --git a/quarkus/extensions/src/main/resources/META-INF/beans.xml b/quarkus/extensions/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/quarkus/extensions/src/main/resources/META-INF/services/org.keycloak.connections.jpa.updater.JpaUpdaterProviderFactory b/quarkus/extensions/src/main/resources/META-INF/services/org.keycloak.connections.jpa.updater.JpaUpdaterProviderFactory new file mode 100644 index 0000000000..11e008eec2 --- /dev/null +++ b/quarkus/extensions/src/main/resources/META-INF/services/org.keycloak.connections.jpa.updater.JpaUpdaterProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2016 Red Hat, Inc. and/or its affiliates +# and other contributors as indicated by the @author tags. +# +# 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. +# + +org.keycloak.connections.liquibase.QuarkusJpaUpdaterProviderFactory \ No newline at end of file diff --git a/quarkus/extensions/src/main/resources/META-INF/services/org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory b/quarkus/extensions/src/main/resources/META-INF/services/org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory new file mode 100644 index 0000000000..1985efd4f5 --- /dev/null +++ b/quarkus/extensions/src/main/resources/META-INF/services/org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2016 Red Hat, Inc. and/or its affiliates +# and other contributors as indicated by the @author tags. +# +# 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. +# + +org.keycloak.connections.liquibase.QuarkusLiquibaseConnectionProvider \ No newline at end of file diff --git a/quarkus/pom.xml b/quarkus/pom.xml index ae73ce0942..e753e7b0e5 100755 --- a/quarkus/pom.xml +++ b/quarkus/pom.xml @@ -31,10 +31,11 @@ pom - 999-SNAPSHOT + 1.4.2.Final 4.4.2.Final 2.10.2 ${jackson.version} + 5.4.14.Final 2.22.0 diff --git a/quarkus/server/pom.xml b/quarkus/server/pom.xml index 580180dde9..82ce895299 100644 --- a/quarkus/server/pom.xml +++ b/quarkus/server/pom.xml @@ -23,6 +23,18 @@ io.quarkus quarkus-resteasy-jackson + + io.quarkus + quarkus-hibernate-orm + + + io.quarkus + quarkus-agroal + + + jakarta.persistence + jakarta.persistence-api + io.quarkus quarkus-jdbc-h2 @@ -35,6 +47,10 @@ io.quarkus quarkus-jdbc-mariadb + + org.liquibase + liquibase-core + @@ -261,17 +277,6 @@ - - org.liquibase - liquibase-core - - - * - * - - - - jakarta.persistence jakarta.persistence-api @@ -279,6 +284,7 @@ org.hibernate hibernate-core + ${hibernate.version} org.apache.httpcomponents diff --git a/quarkus/server/src/main/java/org/keycloak/services/resources/Dummy.java b/quarkus/server/src/main/java/org/keycloak/services/resources/Dummy.java index 2d73733351..d68eb4c2f5 100644 --- a/quarkus/server/src/main/java/org/keycloak/services/resources/Dummy.java +++ b/quarkus/server/src/main/java/org/keycloak/services/resources/Dummy.java @@ -1,5 +1,7 @@ package org.keycloak.services.resources; +import javax.inject.Inject; +import javax.persistence.EntityManagerFactory; import javax.ws.rs.GET; import javax.ws.rs.Path; @@ -9,6 +11,9 @@ import javax.ws.rs.Path; @Path("/dummy") public class Dummy { + @Inject + EntityManagerFactory entityManagerFactory; + // ...and doesn't load Resteasy providers unless there is at least one resource method @GET public String hello() { diff --git a/quarkus/server/src/main/resources/META-INF/keycloak.properties b/quarkus/server/src/main/resources/META-INF/keycloak.properties index 282a6372fd..0ad66cffc4 100644 --- a/quarkus/server/src/main/resources/META-INF/keycloak.properties +++ b/quarkus/server/src/main/resources/META-INF/keycloak.properties @@ -1,8 +1,9 @@ -# Datasource -datasource.url = jdbc:h2:mem:test;DB_CLOSE_DELAY=-1 -datasource.driver = org.h2.Driver -datasource.username = sa -datasource.password = keycloak - hostname.default.frontendUrl = ${keycloak.frontendUrl:} +# Datasource +datasource.driver=org.h2.jdbcx.JdbcDataSource +datasource.url = jdbc:h2:mem:test;DB_CLOSE_DELAY=-1 +#datasource.url = jdbc:h2:file:/tmp/keycloak-server-quarkus +datasource.username = sa +datasource.password = keycloak +datasource.jdbc.transactions=xa \ No newline at end of file diff --git a/quarkus/server/src/main/resources/META-INF/persistence.xml b/quarkus/server/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000000..8a7b5fd5b5 --- /dev/null +++ b/quarkus/server/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,93 @@ + + + + + org.keycloak.models.jpa.entities.ClientEntity + org.keycloak.models.jpa.entities.ClientAttributeEntity + org.keycloak.models.jpa.entities.CredentialEntity + org.keycloak.models.jpa.entities.RealmEntity + org.keycloak.models.jpa.entities.RealmAttributeEntity + org.keycloak.models.jpa.entities.RequiredCredentialEntity + org.keycloak.models.jpa.entities.ComponentConfigEntity + org.keycloak.models.jpa.entities.ComponentEntity + org.keycloak.models.jpa.entities.UserFederationProviderEntity + org.keycloak.models.jpa.entities.UserFederationMapperEntity + org.keycloak.models.jpa.entities.RoleEntity + org.keycloak.models.jpa.entities.RoleAttributeEntity + org.keycloak.models.jpa.entities.FederatedIdentityEntity + org.keycloak.models.jpa.entities.MigrationModelEntity + org.keycloak.models.jpa.entities.UserEntity + org.keycloak.models.jpa.entities.UserRequiredActionEntity + org.keycloak.models.jpa.entities.UserAttributeEntity + org.keycloak.models.jpa.entities.UserRoleMappingEntity + org.keycloak.models.jpa.entities.IdentityProviderEntity + org.keycloak.models.jpa.entities.IdentityProviderMapperEntity + org.keycloak.models.jpa.entities.ProtocolMapperEntity + org.keycloak.models.jpa.entities.UserConsentEntity + org.keycloak.models.jpa.entities.UserConsentClientScopeEntity + org.keycloak.models.jpa.entities.AuthenticationFlowEntity + org.keycloak.models.jpa.entities.AuthenticationExecutionEntity + org.keycloak.models.jpa.entities.AuthenticatorConfigEntity + org.keycloak.models.jpa.entities.RequiredActionProviderEntity + org.keycloak.models.jpa.session.PersistentUserSessionEntity + org.keycloak.models.jpa.session.PersistentClientSessionEntity + org.keycloak.models.jpa.entities.GroupEntity + org.keycloak.models.jpa.entities.GroupAttributeEntity + org.keycloak.models.jpa.entities.GroupRoleMappingEntity + org.keycloak.models.jpa.entities.UserGroupMembershipEntity + org.keycloak.models.jpa.entities.ClientScopeEntity + org.keycloak.models.jpa.entities.ClientScopeAttributeEntity + org.keycloak.models.jpa.entities.ClientScopeRoleMappingEntity + org.keycloak.models.jpa.entities.ClientScopeClientMappingEntity + org.keycloak.models.jpa.entities.DefaultClientScopeRealmMappingEntity + org.keycloak.models.jpa.entities.ClientInitialAccessEntity + + + org.keycloak.events.jpa.EventEntity + org.keycloak.events.jpa.AdminEventEntity + + + org.keycloak.authorization.jpa.entities.ResourceServerEntity + org.keycloak.authorization.jpa.entities.ResourceEntity + org.keycloak.authorization.jpa.entities.ScopeEntity + org.keycloak.authorization.jpa.entities.PolicyEntity + org.keycloak.authorization.jpa.entities.PermissionTicketEntity + org.keycloak.authorization.jpa.entities.ResourceAttributeEntity + + + org.keycloak.storage.jpa.entity.BrokerLinkEntity + org.keycloak.storage.jpa.entity.FederatedUser + org.keycloak.storage.jpa.entity.FederatedUserAttributeEntity + org.keycloak.storage.jpa.entity.FederatedUserConsentEntity + org.keycloak.storage.jpa.entity.FederatedUserConsentClientScopeEntity + org.keycloak.storage.jpa.entity.FederatedUserCredentialEntity + org.keycloak.storage.jpa.entity.FederatedUserGroupMembershipEntity + org.keycloak.storage.jpa.entity.FederatedUserRequiredActionEntity + org.keycloak.storage.jpa.entity.FederatedUserRoleMappingEntity + + true + + + + + + + diff --git a/quarkus/server/src/main/resources/application.properties b/quarkus/server/src/main/resources/application.properties index 64d72d082b..db416e4a77 100644 --- a/quarkus/server/src/main/resources/application.properties +++ b/quarkus/server/src/main/resources/application.properties @@ -1,6 +1,4 @@ #quarkus.log.level = DEBUG quarkus.application.name=Keycloak -quarkus.datasource.driver=org.h2.Driver - resteasy.disable.html.sanitizer = true \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index f0e8570b76..a6292f2dba 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -100,8 +100,6 @@ public class KeycloakApplication extends Application { loadConfig(); - this.sessionFactory = createSessionFactory(); - Resteasy.pushDefaultContextObject(KeycloakApplication.class, this); Resteasy.pushContext(KeycloakApplication.class, this); // for injection @@ -128,6 +126,7 @@ public class KeycloakApplication extends Application { } protected void startup() { + this.sessionFactory = createSessionFactory(); ExportImportManager[] exportImportManager = new ExportImportManager[1]; diff --git a/testsuite/utils/src/main/java/org/keycloak/testsuite/TestPlatform.java b/testsuite/utils/src/main/java/org/keycloak/testsuite/TestPlatform.java index 152402a45d..9863dd3e50 100644 --- a/testsuite/utils/src/main/java/org/keycloak/testsuite/TestPlatform.java +++ b/testsuite/utils/src/main/java/org/keycloak/testsuite/TestPlatform.java @@ -28,10 +28,10 @@ public class TestPlatform implements PlatformProvider { @Override public void onStartup(Runnable startupHook) { + startupHook.run(); KeycloakApplication keycloakApplication = Resteasy.getContextData(KeycloakApplication.class); ServletContext context = Resteasy.getContextData(ServletContext.class); context.setAttribute(KeycloakSessionFactory.class.getName(), keycloakApplication.getSessionFactory()); - startupHook.run(); } @Override diff --git a/wildfly/extensions/src/main/java/org/keycloak/provider/wildfly/WildflyPlatform.java b/wildfly/extensions/src/main/java/org/keycloak/provider/wildfly/WildflyPlatform.java index fdfecbd97a..ad7fda04b9 100644 --- a/wildfly/extensions/src/main/java/org/keycloak/provider/wildfly/WildflyPlatform.java +++ b/wildfly/extensions/src/main/java/org/keycloak/provider/wildfly/WildflyPlatform.java @@ -31,10 +31,10 @@ public class WildflyPlatform implements PlatformProvider { @Override public void onStartup(Runnable startupHook) { + startupHook.run(); KeycloakApplication keycloakApplication = Resteasy.getContextData(KeycloakApplication.class); ServletContext context = Resteasy.getContextData(ServletContext.class); context.setAttribute(KeycloakSessionFactory.class.getName(), keycloakApplication.getSessionFactory()); - startupHook.run(); } @Override