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 extends Database> 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