parent
b7ba9f9af0
commit
eb59fdb772
8 changed files with 331 additions and 3 deletions
|
@ -21,6 +21,7 @@ import static org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider.
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.DatabaseMetaData;
|
import java.sql.DatabaseMetaData;
|
||||||
import java.sql.DriverManager;
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
@ -165,6 +166,7 @@ public class JpaMapStorageProviderFactory implements
|
||||||
private Config.Scope config;
|
private Config.Scope config;
|
||||||
private final String sessionProviderKey;
|
private final String sessionProviderKey;
|
||||||
private final String sessionTxKey;
|
private final String sessionTxKey;
|
||||||
|
private String databaseShortName;
|
||||||
|
|
||||||
// Object instances for each single JpaMapStorageProviderFactory instance per model type.
|
// Object instances for each single JpaMapStorageProviderFactory instance per model type.
|
||||||
// Used to synchronize on when validating the model type area.
|
// Used to synchronize on when validating the model type area.
|
||||||
|
@ -278,7 +280,23 @@ public class JpaMapStorageProviderFactory implements
|
||||||
}
|
}
|
||||||
|
|
||||||
protected EntityManager getEntityManager() {
|
protected EntityManager getEntityManager() {
|
||||||
return emf.createEntityManager();
|
EntityManager em = emf.createEntityManager();
|
||||||
|
|
||||||
|
// This is a workaround for Hibernate not supporting javax.persistence.lock.timeout
|
||||||
|
// config option for Postgresql/CockroachDB - https://hibernate.atlassian.net/browse/HHH-16071
|
||||||
|
if ("postgresql".equals(databaseShortName) || "cockroachdb".equals(databaseShortName)) {
|
||||||
|
Long lockTimeout = config.getLong("lockTimeout");
|
||||||
|
if (lockTimeout != null) {
|
||||||
|
em.unwrap(SessionImpl.class)
|
||||||
|
.doWork(connection -> {
|
||||||
|
PreparedStatement preparedStatement = connection.prepareStatement("SET LOCAL lock_timeout = '" + lockTimeout + "';");
|
||||||
|
preparedStatement.execute();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warnf("Database %s used without lockTimeout option configured. This can result in deadlock where one connection waits for a pessimistic write lock forever.", databaseShortName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -370,6 +388,11 @@ public class JpaMapStorageProviderFactory implements
|
||||||
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
|
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
|
||||||
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
|
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
|
||||||
properties.put("hibernate.dialect", config.get("driverDialect"));
|
properties.put("hibernate.dialect", config.get("driverDialect"));
|
||||||
|
Integer lockTimeout = config.getInt("lockTimeout");
|
||||||
|
if (lockTimeout != null) {
|
||||||
|
// This property does not work for PostgreSQL/CockroachDB - https://hibernate.atlassian.net/browse/HHH-16071
|
||||||
|
properties.put("javax.persistence.lock.timeout", lockTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
logger.trace("Creating EntityManagerFactory");
|
logger.trace("Creating EntityManagerFactory");
|
||||||
ParsedPersistenceXmlDescriptor descriptor = PersistenceXmlParser.locateIndividualPersistenceUnit(
|
ParsedPersistenceXmlDescriptor descriptor = PersistenceXmlParser.locateIndividualPersistenceUnit(
|
||||||
|
@ -416,6 +439,7 @@ public class JpaMapStorageProviderFactory implements
|
||||||
|
|
||||||
MapJpaUpdaterProvider updater = session.getProvider(MapJpaUpdaterProvider.class);
|
MapJpaUpdaterProvider updater = session.getProvider(MapJpaUpdaterProvider.class);
|
||||||
MapJpaUpdaterProvider.Status status = updater.validate(modelType, connection, config.get("schema"));
|
MapJpaUpdaterProvider.Status status = updater.validate(modelType, connection, config.get("schema"));
|
||||||
|
databaseShortName = updater.getDatabaseShortName();
|
||||||
|
|
||||||
if (!status.equals(VALID)) {
|
if (!status.equals(VALID)) {
|
||||||
update(modelType, connection, session);
|
update(modelType, connection, session);
|
||||||
|
|
|
@ -45,6 +45,7 @@ public class MapJpaLiquibaseUpdaterProvider implements MapJpaUpdaterProvider {
|
||||||
private static final Logger logger = Logger.getLogger(MapJpaLiquibaseUpdaterProvider.class);
|
private static final Logger logger = Logger.getLogger(MapJpaLiquibaseUpdaterProvider.class);
|
||||||
|
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
|
private String databaseShortName;
|
||||||
|
|
||||||
public MapJpaLiquibaseUpdaterProvider(KeycloakSession session) {
|
public MapJpaLiquibaseUpdaterProvider(KeycloakSession session) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
@ -187,6 +188,7 @@ public class MapJpaLiquibaseUpdaterProvider implements MapJpaUpdaterProvider {
|
||||||
try (Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnectionFromPool(connection.unwrap(Connection.class)))) {
|
try (Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnectionFromPool(connection.unwrap(Connection.class)))) {
|
||||||
// if the database is cockroachdb, use the aggregate changelog (see GHI #11230).
|
// if the database is cockroachdb, use the aggregate changelog (see GHI #11230).
|
||||||
String changelog = database instanceof CockroachDatabase ? "META-INF/jpa-aggregate-changelog.xml" : "META-INF/jpa-" + modelName + "-changelog.xml";
|
String changelog = database instanceof CockroachDatabase ? "META-INF/jpa-aggregate-changelog.xml" : "META-INF/jpa-" + modelName + "-changelog.xml";
|
||||||
|
databaseShortName = database.getShortName();
|
||||||
return liquibaseProvider.getLiquibaseForCustomUpdate(connection, defaultSchema, changelog, this.getClass().getClassLoader(), "databasechangelog");
|
return liquibaseProvider.getLiquibaseForCustomUpdate(connection, defaultSchema, changelog, this.getClass().getClassLoader(), "databasechangelog");
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new LiquibaseException(e);
|
throw new LiquibaseException(e);
|
||||||
|
@ -197,4 +199,8 @@ public class MapJpaLiquibaseUpdaterProvider implements MapJpaUpdaterProvider {
|
||||||
public void close() {
|
public void close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDatabaseShortName() {
|
||||||
|
return databaseShortName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,4 +68,8 @@ public interface MapJpaUpdaterProvider extends Provider {
|
||||||
*/
|
*/
|
||||||
void export(Class<?> modelType, Connection connection, String defaultSchema, File file);
|
void export(Class<?> modelType, Connection connection, String defaultSchema, File file);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an all-lower-case short name of the used database.
|
||||||
|
*/
|
||||||
|
String getDatabaseShortName();
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,8 @@ public class JpaMapStorage extends KeycloakModelParameters {
|
||||||
.config("user", POSTGRES_DB_USER)
|
.config("user", POSTGRES_DB_USER)
|
||||||
.config("password", POSTGRES_DB_PASSWORD)
|
.config("password", POSTGRES_DB_PASSWORD)
|
||||||
.config("driver", "org.postgresql.Driver")
|
.config("driver", "org.postgresql.Driver")
|
||||||
.config("driverDialect", "org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect");
|
.config("driverDialect", "org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect")
|
||||||
|
.config("lockTimeout", "1000");
|
||||||
|
|
||||||
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
||||||
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
||||||
|
|
|
@ -96,7 +96,8 @@ public class JpaMapStorageCockroachdb extends KeycloakModelParameters {
|
||||||
.config("user", COCKROACHDB_DB_USER)
|
.config("user", COCKROACHDB_DB_USER)
|
||||||
.config("password", COCKROACHDB_DB_PASSWORD)
|
.config("password", COCKROACHDB_DB_PASSWORD)
|
||||||
.config("driver", "org.postgresql.Driver")
|
.config("driver", "org.postgresql.Driver")
|
||||||
.config("driverDialect", "org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect");
|
.config("driverDialect", "org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect")
|
||||||
|
.config("lockTimeout", "1000");
|
||||||
|
|
||||||
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
||||||
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
|
||||||
|
|
|
@ -0,0 +1,185 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.testsuite.model.transaction;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.ModelException;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.RealmProvider;
|
||||||
|
import org.keycloak.models.map.storage.MapStorageProvider;
|
||||||
|
import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory;
|
||||||
|
import org.keycloak.testsuite.model.KeycloakModelTest;
|
||||||
|
import org.keycloak.testsuite.model.RequireProvider;
|
||||||
|
import org.keycloak.testsuite.model.util.TransactionController;
|
||||||
|
import org.keycloak.utils.LockObjectsForModification;
|
||||||
|
|
||||||
|
import javax.persistence.OptimisticLockException;
|
||||||
|
import javax.persistence.PessimisticLockException;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.allOf;
|
||||||
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
|
import static org.hamcrest.CoreMatchers.instanceOf;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.junit.internal.matchers.ThrowableCauseMatcher.hasCause;
|
||||||
|
import static org.keycloak.testsuite.model.util.KeycloakAssertions.assertException;
|
||||||
|
|
||||||
|
@RequireProvider(RealmProvider.class)
|
||||||
|
public class StorageTransactionTest extends KeycloakModelTest {
|
||||||
|
|
||||||
|
private String realmId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createEnvironment(KeycloakSession s) {
|
||||||
|
RealmModel r = s.realms().createRealm("1");
|
||||||
|
r.setDefaultRole(s.roles().addRealmRole(r, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + r.getName()));
|
||||||
|
r.setAttribute("k1", "v1");
|
||||||
|
realmId = r.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanEnvironment(KeycloakSession s) {
|
||||||
|
s.realms().removeRealm(realmId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isUseSameKeycloakSessionFactoryForAllThreads() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTwoTransactionsSequentially() throws Exception {
|
||||||
|
TransactionController tx1 = new TransactionController(getFactory());
|
||||||
|
TransactionController tx2 = new TransactionController(getFactory());
|
||||||
|
|
||||||
|
tx1.begin();
|
||||||
|
assertThat(
|
||||||
|
tx1.runStep(session -> {
|
||||||
|
session.realms().getRealm(realmId).setAttribute("k2", "v1");
|
||||||
|
return session.realms().getRealm(realmId).getAttribute("k2");
|
||||||
|
}), equalTo("v1"));
|
||||||
|
tx1.commit();
|
||||||
|
|
||||||
|
tx2.begin();
|
||||||
|
assertThat(
|
||||||
|
tx2.runStep(session -> session.realms().getRealm(realmId).getAttribute("k2")),
|
||||||
|
equalTo("v1"));
|
||||||
|
tx2.commit();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRepeatableRead() {
|
||||||
|
TransactionController tx1 = new TransactionController(getFactory());
|
||||||
|
TransactionController tx2 = new TransactionController(getFactory());
|
||||||
|
TransactionController tx3 = new TransactionController(getFactory());
|
||||||
|
|
||||||
|
tx1.begin();
|
||||||
|
tx2.begin();
|
||||||
|
tx3.begin();
|
||||||
|
|
||||||
|
// Read original value in tx1
|
||||||
|
assertThat(
|
||||||
|
tx1.runStep(session -> session.realms().getRealm(realmId).getAttribute("k1")),
|
||||||
|
equalTo("v1"));
|
||||||
|
|
||||||
|
// change value to new in tx2
|
||||||
|
tx2.runStep(session -> {
|
||||||
|
session.realms().getRealm(realmId).setAttribute("k1", "v2");
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
tx2.commit();
|
||||||
|
|
||||||
|
// tx1 should still return the value that already read
|
||||||
|
assertThat(
|
||||||
|
tx1.runStep(session -> session.realms().getRealm(realmId).getAttribute("k1")),
|
||||||
|
equalTo("v1"));
|
||||||
|
|
||||||
|
// tx3 should return the new value
|
||||||
|
assertThat(
|
||||||
|
tx3.runStep(session -> session.realms().getRealm(realmId).getAttribute("k1")),
|
||||||
|
equalTo("v2"));
|
||||||
|
tx1.commit();
|
||||||
|
tx3.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// LockObjectForModification is currently used only in map-jpa
|
||||||
|
@RequireProvider(value = MapStorageProvider.class, only = JpaMapStorageProviderFactory.PROVIDER_ID)
|
||||||
|
public void testLockObjectForModification() {
|
||||||
|
TransactionController tx1 = new TransactionController(getFactory());
|
||||||
|
TransactionController tx2 = new TransactionController(getFactory());
|
||||||
|
TransactionController tx3 = new TransactionController(getFactory());
|
||||||
|
|
||||||
|
tx1.begin();
|
||||||
|
tx2.begin();
|
||||||
|
|
||||||
|
// tx1 acquires lock
|
||||||
|
tx1.runStep(session -> LockObjectsForModification.lockRealmsForModification(session, () -> session.realms().getRealm(realmId)));
|
||||||
|
|
||||||
|
// tx2 should fail as tx1 locked the realm
|
||||||
|
assertException(() -> tx2.runStep(session -> LockObjectsForModification.lockRealmsForModification(session, () -> session.realms().getRealm(realmId))),
|
||||||
|
allOf(instanceOf(ModelException.class),
|
||||||
|
hasCause(instanceOf(PessimisticLockException.class))));
|
||||||
|
|
||||||
|
// end both transactions
|
||||||
|
tx2.rollback();
|
||||||
|
tx1.commit();
|
||||||
|
|
||||||
|
// start new transaction and read again, it should be successful
|
||||||
|
tx3.begin();
|
||||||
|
tx3.runStep(session -> LockObjectsForModification.lockRealmsForModification(session, () -> session.realms().getRealm(realmId)));
|
||||||
|
tx3.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// Optimistic locking works only with map-jpa
|
||||||
|
@RequireProvider(value = MapStorageProvider.class, only = JpaMapStorageProviderFactory.PROVIDER_ID)
|
||||||
|
public void testOptimisticLockingException() {
|
||||||
|
withRealm(realmId, (session, realm) -> {
|
||||||
|
realm.setDisplayName("displayName1");
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
TransactionController tx1 = new TransactionController(getFactory());
|
||||||
|
TransactionController tx2 = new TransactionController(getFactory());
|
||||||
|
|
||||||
|
// tx1 acquires lock
|
||||||
|
tx1.begin();
|
||||||
|
tx2.begin();
|
||||||
|
|
||||||
|
// both transactions touch the same entity
|
||||||
|
tx1.runStep(session -> {
|
||||||
|
session.realms().getRealm(realmId).setDisplayName("displayName2");
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
tx2.runStep(session -> {
|
||||||
|
session.realms().getRealm(realmId).setDisplayName("displayName3");
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// tx1 transaction should be successful
|
||||||
|
tx1.commit();
|
||||||
|
|
||||||
|
// tx2 should fail as tx1 already changed the value
|
||||||
|
assertException(tx2::commit,
|
||||||
|
allOf(instanceOf(ModelException.class),
|
||||||
|
hasCause(instanceOf(OptimisticLockException.class))));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.testsuite.model.util;
|
||||||
|
|
||||||
|
import org.hamcrest.Matcher;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.allOf;
|
||||||
|
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
|
||||||
|
public class KeycloakAssertions {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs {@code task} and checks whether the execution resulted in
|
||||||
|
* an exception that matches {@code matcher}. The method fails also
|
||||||
|
* when no exception is thrown.
|
||||||
|
*
|
||||||
|
* @param task task
|
||||||
|
* @param matcher matcher that the exception should match
|
||||||
|
*/
|
||||||
|
public static void assertException(Runnable task, Matcher<? super Throwable> matcher) {
|
||||||
|
Throwable ex = catchException(task);
|
||||||
|
assertThat(ex, allOf(notNullValue(), matcher));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the {@code task} and returns any throwable that is thrown.
|
||||||
|
* If not exception is thrown, the method returns {@code null}
|
||||||
|
*
|
||||||
|
* @param task task
|
||||||
|
*/
|
||||||
|
public static Throwable catchException(Runnable task) {
|
||||||
|
try {
|
||||||
|
task.run();
|
||||||
|
return null;
|
||||||
|
} catch (Throwable ex) {
|
||||||
|
return ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 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.testsuite.model.util;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.KeycloakTransactionManager;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public class TransactionController {
|
||||||
|
private final KeycloakSession session;
|
||||||
|
|
||||||
|
public TransactionController(KeycloakSessionFactory sessionFactory) {
|
||||||
|
session = sessionFactory.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void begin() {
|
||||||
|
getTransactionManager().begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void commit() {
|
||||||
|
getTransactionManager().commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rollback() {
|
||||||
|
getTransactionManager().rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
public <R> R runStep(Function<KeycloakSession, R> task) {
|
||||||
|
return task.apply(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
private KeycloakTransactionManager getTransactionManager() {
|
||||||
|
return session.getTransactionManager();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue