Skip to content

Commit

Permalink
KEYCLOAK-2474 Possibility to add custom SPI and extend the data model
Browse files Browse the repository at this point in the history
  • Loading branch information
ewjmulder authored and mposolda committed Jun 20, 2016
1 parent 111bcb7 commit f4ead48
Show file tree
Hide file tree
Showing 15 changed files with 380 additions and 38 deletions.
Expand Up @@ -20,25 +20,17 @@
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.naming.InitialContext;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.spi.PersistenceUnitTransactionType;
import javax.sql.DataSource;

import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
import org.hibernate.ejb.AvailableSettings;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
import org.hibernate.jpa.boot.spi.Bootstrap;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
Expand Down Expand Up @@ -182,7 +174,7 @@ private void lazyInit(KeycloakSession session) {
}

logger.trace("Creating EntityManagerFactory");
emf = JpaUtils.createEntityManagerFactory(unitName, properties, getClass().getClassLoader());
emf = JpaUtils.createEntityManagerFactory(session, unitName, properties, getClass().getClassLoader());
logger.trace("EntityManagerFactory created");

if (globalStatsInterval != -1) {
Expand Down
@@ -0,0 +1,47 @@
/*
* 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.jpa.entityprovider;

import java.util.List;

import org.keycloak.provider.Provider;

/**
* @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
*
* A JPA Entity Provider can supply extra JPA entities that the Keycloak system should include in it's entity manager. The
* entities should be provided as a list of Class objects.
*/
public interface JpaEntityProvider extends Provider {

/**
* Return the entities that should be added to the entity manager.
*
* @return list of class objects
*/
List<Class<?>> getEntities();

/**
* Return the location of the Liquibase changelog that facilitates the extra JPA entities.
* This should be a location that can be found on the same classpath as the entity classes.
*
* @return a changelog location or null if not needed
*/
String getChangelogLocation();

}
@@ -0,0 +1,29 @@
/*
* 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.jpa.entityprovider;

import org.keycloak.provider.ProviderFactory;

/**
* @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
*
* Extended interface for a provider factory for JpaEntityProvider's.
*/
public interface JpaEntityProviderFactory extends ProviderFactory<JpaEntityProvider> {

}
@@ -0,0 +1,51 @@
/*
* 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.jpa.entityprovider;

import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;

/**
* @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
*
* Spi that allows for adding extra JPA entity's to the Keycloak entity manager.
*/
public class JpaEntitySpi implements Spi {

@Override
public boolean isInternal() {
return false;
}

@Override
public String getName() {
return "jpa-entity-provider";
}

@Override
public Class<? extends Provider> getProviderClass() {
return JpaEntityProvider.class;
}

@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return JpaEntityProviderFactory.class;
}

}
@@ -0,0 +1,87 @@
/*
* 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.jpa.entityprovider;

import java.net.URL;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
* @author <a href="mailto:erik.mulder@docdatapayments.com">Erik Mulder</a>
*
* Classloader implementation to facilitate loading classes and resources from a collection of other classloaders.
* Effectively it forms a proxy to one or more other classloaders.
*
* The way it works:
* - Get all (unique) classloaders from all provided classes
* - For each class or resource that is 'requested':
* - First try all provided classloaders and if we have a match, return that
* - If no match was found: proceed with 'normal' classloading in 'current classpath' scope
*
* In this particular context: only loadClass and getResource overrides are needed, since those
* are the methods that a classloading and resource loading process will need.
*/
public class ProxyClassLoader extends ClassLoader {

private Set<ClassLoader> classloaders;

public ProxyClassLoader(Collection<Class<?>> classes, ClassLoader parentClassLoader) {
super(parentClassLoader);
init(classes);
}

public ProxyClassLoader(Collection<Class<?>> classes) {
init(classes);
}

private void init(Collection<Class<?>> classes) {
classloaders = new HashSet<>();
for (Class<?> clazz : classes) {
classloaders.add(clazz.getClassLoader());
}
}

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
for (ClassLoader classloader : classloaders) {
try {
return classloader.loadClass(name);
} catch (ClassNotFoundException e) {
// This particular class loader did not find the class. It's expected behavior that
// this can happen, so we'll just ignore the exception and let the next one try.
}
}
// We did not find the class in the proxy class loaders, so proceed with 'normal' behavior.
return super.loadClass(name);
}

@Override
public URL getResource(String name) {
for (ClassLoader classloader : classloaders) {
URL resource = classloader.getResource(name);
if (resource != null) {
return resource;
}
// Resource == null means not found, so let the next one try.
}
// We could not get the resource from the proxy class loaders, so proceed with 'normal' behavior.
return super.getResource(name);
}

}
Expand Up @@ -18,32 +18,40 @@
package org.keycloak.connections.jpa.updater.liquibase.conn;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
import org.keycloak.connections.jpa.entityprovider.ProxyClassLoader;
import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider;
import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase;
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 org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;

import liquibase.Liquibase;
import liquibase.changelog.ChangeLogParameters;
import liquibase.changelog.ChangeSet;
import liquibase.changelog.DatabaseChangeLog;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.core.DB2Database;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.lockservice.LockService;
import liquibase.lockservice.LockServiceFactory;
import liquibase.logging.LogFactory;
import liquibase.logging.LogLevel;
import liquibase.parser.ChangeLogParser;
import liquibase.parser.ChangeLogParserFactory;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.ResourceAccessor;
import liquibase.servicelocator.ServiceLocator;
import liquibase.sqlgenerator.SqlGeneratorFactory;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider;
import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase;
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.CustomLockService;
import org.keycloak.connections.jpa.updater.liquibase.lock.DummyLockService;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;

/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
Expand All @@ -54,8 +62,11 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr

private volatile boolean initialized = false;

private KeycloakSession keycloakSession;

@Override
public LiquibaseConnectionProvider create(KeycloakSession session) {
this.keycloakSession = session;
if (!initialized) {
synchronized (this) {
if (!initialized) {
Expand Down Expand Up @@ -134,7 +145,61 @@ public Liquibase getLiquibase(Connection connection, String defaultSchema) throw
String changelog = (database instanceof DB2Database) ? LiquibaseJpaUpdaterProvider.DB2_CHANGELOG : LiquibaseJpaUpdaterProvider.CHANGELOG;
logger.debugf("Using changelog file: %s", changelog);

return new Liquibase(changelog, new ClassLoaderResourceAccessor(getClass().getClassLoader()), database);
ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(getClass().getClassLoader());
DatabaseChangeLog databaseChangeLog = generateDynamicChangeLog(changelog, resourceAccessor, database);

return new Liquibase(databaseChangeLog, resourceAccessor, database);
}

/**
* We want to be able to provide extra changesets as an extension to the Keycloak data model.
* But we do not want users to be able to not execute certain parts of the Keycloak internal data model.
* Therefore, we generate a dynamic changelog here that always contains the keycloak changelog file
* and optionally include the user extension changelog files.
*
* @param changelog the changelog file location
* @param resourceAccessor the resource accessor
* @param database the database
* @return
*/
private DatabaseChangeLog generateDynamicChangeLog(String changelog, ResourceAccessor resourceAccessor, Database database) throws LiquibaseException {
ChangeLogParameters changeLogParameters = new ChangeLogParameters(database);
ChangeLogParser parser = ChangeLogParserFactory.getInstance().getParser(changelog, resourceAccessor);
DatabaseChangeLog keycloakDatabaseChangeLog = parser.parse(changelog, changeLogParameters, resourceAccessor);

List<String> locations = new ArrayList<>();
Set<JpaEntityProvider> entityProviders = keycloakSession.getAllProviders(JpaEntityProvider.class);
for (JpaEntityProvider entityProvider : entityProviders) {
String location = entityProvider.getChangelogLocation();
if (location != null) {
locations.add(location);
}
}

final DatabaseChangeLog dynamicMasterChangeLog;
if (locations.isEmpty()) {
// If there are no extra changelog locations, we'll just use the keycloak one.
dynamicMasterChangeLog = keycloakDatabaseChangeLog;
} else {
// A change log is essentially not much more than a (big) collection of changesets.
// The original (file) destination is not important. So we can just make one big dynamic change log that include all changesets.
dynamicMasterChangeLog = new DatabaseChangeLog();
dynamicMasterChangeLog.setChangeLogParameters(changeLogParameters);
for (ChangeSet changeSet : keycloakDatabaseChangeLog.getChangeSets()) {
dynamicMasterChangeLog.addChangeSet(changeSet);
}
ProxyClassLoader proxyClassLoader = new ProxyClassLoader(JpaUtils.getProvidedEntities(keycloakSession));
for (String location : locations) {
ResourceAccessor proxyResourceAccessor = new ClassLoaderResourceAccessor(proxyClassLoader);
ChangeLogParser locationParser = ChangeLogParserFactory.getInstance().getParser(location, proxyResourceAccessor);
DatabaseChangeLog locationDatabaseChangeLog = locationParser.parse(location, changeLogParameters, proxyResourceAccessor);
for (ChangeSet changeSet : locationDatabaseChangeLog.getChangeSets()) {
dynamicMasterChangeLog.addChangeSet(changeSet);
}
}
}

return dynamicMasterChangeLog;
}

private static class LogWrapper extends LogFactory {
Expand Down

0 comments on commit f4ead48

Please sign in to comment.