[KEYCLOAK-11784] - Using Hibernate Extension

This commit is contained in:
Pedro Igor 2020-05-13 16:56:09 -03:00 committed by Stian Thorgersen
parent 8c9b7b05ac
commit bae802bcfa
21 changed files with 1148 additions and 252 deletions

View file

@ -43,6 +43,21 @@
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-jpa</artifactId>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
@ -75,7 +90,16 @@
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-agroal</artifactId>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-common</artifactId>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
@ -91,6 +115,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
@ -100,16 +128,68 @@
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>extract-liquibase-for-indexing</id>
<phase>generate-sources</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includeGroupIds>org.liquibase</includeGroupIds>
<outputDirectory>${project.build.directory}/liquibase-extracted</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jboss.jandex</groupId>
<artifactId>jandex-maven-plugin</artifactId>
<version>1.0.6</version>
<executions>
<execution>
<id>make-index</id>
<goals>
<goal>jandex</goal>
</goals>
<phase>process-sources</phase>
<configuration>
<indexName>liquibase.idx</indexName>
<processDefaultFileSet>false</processDefaultFileSet>
<fileSets>
<fileSet>
<directory>${project.build.directory}/liquibase-extracted</directory>
</fileSet>
</fileSets>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-liquibase-index</id>
<phase>process-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/classes/</outputDirectory>
<resources>
<resource>
<directory>${project.build.directory}/liquibase-extracted</directory>
<includes>
<include>META-INF/liquibase.idx</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>

View file

@ -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();
}
}

View file

@ -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<String, Object> 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<ClassLoader> classLoaders = new ArrayList<>();
if (properties.containsKey(AvailableSettings.CLASSLOADERS)) {
classLoaders.addAll((Collection<ClassLoader>) 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();
String version = null;
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();
// 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 (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());
} catch (SQLException ignore) {
// migration model probably does not exist so we assume the database is empty
}
return null;
}
}
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,8 +208,7 @@ 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);
DBLockManager dbLockManager = new DBLockManager(session);
DBLockProvider dbLock2 = dbLockManager.getDBLock();
dbLock2.waitForLock(DBLockProvider.Namespace.DATABASE);
try {
@ -312,12 +216,11 @@ public class QuarkusJpaConnectionProviderFactory implements JpaConnectionProvide
} finally {
dbLock2.releaseLock();
}
}, KeycloakTransactionManager.JTAPolicy.NOT_SUPPORTED);
}
protected void export(Connection connection, String schema, File databaseUpdateFile, KeycloakSession session, JpaUpdaterProvider updater) {
runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession lockSession) -> {
DBLockManager dbLockManager = new DBLockManager(lockSession);
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 {
@ -325,17 +228,16 @@ public class QuarkusJpaConnectionProviderFactory implements JpaConnectionProvide
} finally {
dbLock2.releaseLock();
}
}, KeycloakTransactionManager.JTAPolicy.NOT_SUPPORTED);
}
@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<EntityManagerFactory> instance = CDI.current().select(EntityManagerFactory.class);
if (!instance.isResolvable()) {
throw new RuntimeException("Failed to resolve " + EntityManagerFactory.class + " from Quarkus runtime");
}
private static void runJobInTransaction(KeycloakSessionFactory factory, KeycloakSessionTask task, KeycloakTransactionManager.JTAPolicy jtaPolicy) {
emf = instance.get();
try (Connection connection = getConnection()) {
if (jtaEnabled) {
KeycloakModelUtils.suspendJtaTransaction(factory, () -> {
KeycloakSession session = factory.create();
KeycloakTransactionManager tx = session.getTransactionManager();
if (jtaPolicy != null)
tx.setJTAPolicy(jtaPolicy);
try {
tx.begin();
task.run(session);
if (tx.isActive()) {
if (tx.getRollbackOnly()) {
tx.rollback();
} else {
tx.commit();
}
}
} catch (RuntimeException re) {
if (tx.isActive()) {
tx.rollback();
}
throw re;
migration(getSchema(), connection, session);
} finally {
session.close();
}
});
} else {
KeycloakModelUtils.runJobInTransaction(factory, session -> {
migration(getSchema(), connection, session);
});
}
prepareOperationalInfo(connection);
} catch (SQLException cause) {
throw new RuntimeException("Failed to migrate model", cause);
}
}
@Override
public int order() {
return 100;
}
}

View file

@ -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<String, List<String>> 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<String> impls = new ArrayList<>();
CLASS_INDEX.put(c.getName(), impls);
Set<ClassInfo> 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<Class<?>> findImplementations(Class parent, String... packageNames) {
List<String> found = CLASS_INDEX.get(parent.getName());
if (found == null) {
return super.findImplementations(parent, packageNames);
}
Set<Class<?>> 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()));
}
}

View file

@ -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() {
}
}

View file

@ -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<String, List<ChangeSet>> 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<JpaEntityProvider> 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<SqlStatement> 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<ChangeSet> changeSets = getLiquibaseUnrunChangeSets(liquibase);
if (!changeSets.isEmpty()) {
List<RanChangeSet> 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<JpaEntityProvider> 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<ChangeSet> 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<ChangeSet> getLiquibaseUnrunChangeSets(Liquibase liquibase) {
// we don't need to fetch change sets if they were previously obtained
return changeSets.computeIfAbsent(liquibase.getChangeLogFile(), new Function<String, List<ChangeSet>>() {
@Override
public List<ChangeSet> 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<ChangeSet> 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;
}
}

View file

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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";
}
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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

View file

@ -31,10 +31,11 @@
<packaging>pom</packaging>
<properties>
<quarkus.version>999-SNAPSHOT</quarkus.version>
<quarkus.version>1.4.2.Final</quarkus.version>
<resteasy.version>4.4.2.Final</resteasy.version>
<jackson.version>2.10.2</jackson.version>
<jackson.databind.version>${jackson.version}</jackson.databind.version>
<hibernate.version>5.4.14.Final</hibernate.version>
<surefire-plugin.version>2.22.0</surefire-plugin.version>

View file

@ -23,6 +23,18 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-agroal</artifactId>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
@ -35,6 +47,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-mariadb</artifactId>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
<!-- Keycloak -->
<dependency>
@ -261,17 +277,6 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--TODO Should come from Hibernate extension -->
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
@ -279,6 +284,7 @@
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>

View file

@ -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() {

View file

@ -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

View file

@ -0,0 +1,93 @@
<!--
~ 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.
-->
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">
<persistence-unit name="keycloak-default" transaction-type="JTA">
<class>org.keycloak.models.jpa.entities.ClientEntity</class>
<class>org.keycloak.models.jpa.entities.ClientAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.CredentialEntity</class>
<class>org.keycloak.models.jpa.entities.RealmEntity</class>
<class>org.keycloak.models.jpa.entities.RealmAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.RequiredCredentialEntity</class>
<class>org.keycloak.models.jpa.entities.ComponentConfigEntity</class>
<class>org.keycloak.models.jpa.entities.ComponentEntity</class>
<class>org.keycloak.models.jpa.entities.UserFederationProviderEntity</class>
<class>org.keycloak.models.jpa.entities.UserFederationMapperEntity</class>
<class>org.keycloak.models.jpa.entities.RoleEntity</class>
<class>org.keycloak.models.jpa.entities.RoleAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.FederatedIdentityEntity</class>
<class>org.keycloak.models.jpa.entities.MigrationModelEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class>
<class>org.keycloak.models.jpa.entities.UserRequiredActionEntity</class>
<class>org.keycloak.models.jpa.entities.UserAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.IdentityProviderEntity</class>
<class>org.keycloak.models.jpa.entities.IdentityProviderMapperEntity</class>
<class>org.keycloak.models.jpa.entities.ProtocolMapperEntity</class>
<class>org.keycloak.models.jpa.entities.UserConsentEntity</class>
<class>org.keycloak.models.jpa.entities.UserConsentClientScopeEntity</class>
<class>org.keycloak.models.jpa.entities.AuthenticationFlowEntity</class>
<class>org.keycloak.models.jpa.entities.AuthenticationExecutionEntity</class>
<class>org.keycloak.models.jpa.entities.AuthenticatorConfigEntity</class>
<class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class>
<class>org.keycloak.models.jpa.session.PersistentUserSessionEntity</class>
<class>org.keycloak.models.jpa.session.PersistentClientSessionEntity</class>
<class>org.keycloak.models.jpa.entities.GroupEntity</class>
<class>org.keycloak.models.jpa.entities.GroupAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.GroupRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.UserGroupMembershipEntity</class>
<class>org.keycloak.models.jpa.entities.ClientScopeEntity</class>
<class>org.keycloak.models.jpa.entities.ClientScopeAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.ClientScopeRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ClientScopeClientMappingEntity</class>
<class>org.keycloak.models.jpa.entities.DefaultClientScopeRealmMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ClientInitialAccessEntity</class>
<!-- JpaAuditProviders -->
<class>org.keycloak.events.jpa.EventEntity</class>
<class>org.keycloak.events.jpa.AdminEventEntity</class>
<!-- Authorization -->
<class>org.keycloak.authorization.jpa.entities.ResourceServerEntity</class>
<class>org.keycloak.authorization.jpa.entities.ResourceEntity</class>
<class>org.keycloak.authorization.jpa.entities.ScopeEntity</class>
<class>org.keycloak.authorization.jpa.entities.PolicyEntity</class>
<class>org.keycloak.authorization.jpa.entities.PermissionTicketEntity</class>
<class>org.keycloak.authorization.jpa.entities.ResourceAttributeEntity</class>
<!-- User Federation Storage -->
<class>org.keycloak.storage.jpa.entity.BrokerLinkEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUser</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserAttributeEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserConsentEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserConsentClientScopeEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserCredentialEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserGroupMembershipEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserRequiredActionEntity</class>
<class>org.keycloak.storage.jpa.entity.FederatedUserRoleMappingEntity</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.query.startup_check" value="false"/>
</properties>
</persistence-unit>
</persistence>

View file

@ -1,6 +1,4 @@
#quarkus.log.level = DEBUG
quarkus.application.name=Keycloak
quarkus.datasource.driver=org.h2.Driver
resteasy.disable.html.sanitizer = true

View file

@ -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];

View file

@ -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

View file

@ -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