keycloak-uncached

Details

diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
index 9c93214..b5621ef 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
@@ -20,25 +20,17 @@ package org.keycloak.connections.jpa;
 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;
@@ -182,7 +174,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
                         }
 
 	                    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) {
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java
new file mode 100644
index 0000000..c64c965
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java
@@ -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();
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProviderFactory.java
new file mode 100644
index 0000000..8c08bf9
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProviderFactory.java
@@ -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> {
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntitySpi.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntitySpi.java
new file mode 100644
index 0000000..d89389f
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntitySpi.java
@@ -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;
+	}
+	
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/ProxyClassLoader.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/ProxyClassLoader.java
new file mode 100644
index 0000000..e9b82a3
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/ProxyClassLoader.java
@@ -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);
+    }
+
+}
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
index b9018f8..ca6d722 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
@@ -18,8 +18,25 @@
 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;
@@ -27,23 +44,14 @@ 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>
@@ -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) {
@@ -134,7 +145,61 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
         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 {
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
index de07fd5..d93c02b 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
@@ -21,12 +21,18 @@ import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
 import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
 import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
 import org.hibernate.jpa.boot.spi.Bootstrap;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
+import org.keycloak.connections.jpa.entityprovider.ProxyClassLoader;
+import org.keycloak.models.KeycloakSession;
 
 import javax.persistence.EntityManager;
 import javax.persistence.EntityManagerFactory;
 import javax.persistence.spi.PersistenceUnitTransactionType;
+
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -40,14 +46,40 @@ public class JpaUtils {
         return (schema==null) ? tableName : schema + "." + tableName;
     }
 
-    public static EntityManagerFactory createEntityManagerFactory(String unitName, Map<String, Object> properties, ClassLoader classLoader) {
+    public static EntityManagerFactory createEntityManagerFactory(KeycloakSession session, String unitName, Map<String, Object> properties, ClassLoader classLoader) {
         PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(classLoader), PersistenceUnitTransactionType.RESOURCE_LOCAL);
         List<ParsedPersistenceXmlDescriptor> persistenceUnits = parser.doResolve(properties);
         for (ParsedPersistenceXmlDescriptor persistenceUnit : persistenceUnits) {
             if (persistenceUnit.getName().equals(unitName)) {
-                return Bootstrap.getEntityManagerFactoryBuilder(persistenceUnit, properties, classLoader).build();
+                List<Class<?>> providedEntities = getProvidedEntities(session);
+                for (Class<?> entityClass : providedEntities) {
+                    // Add all extra entity classes to the persistence unit.
+                    persistenceUnit.addClasses(entityClass.getName());
+                }
+                // Now build the entity manager factory, supplying a proxy classloader, so Hibernate will be able
+                // to find and load the extra provided entities. Set the provided classloader as parent classloader.
+                return Bootstrap.getEntityManagerFactoryBuilder(persistenceUnit, properties,
+                        new ProxyClassLoader(providedEntities, classLoader)).build();
             }
         }
         throw new RuntimeException("Persistence unit '" + unitName + "' not found");
     }
+
+    /**
+     * Get a list of all provided entities by looping over all configured entity providers.
+     * 
+     * @param session the keycloak session
+     * @return a list of all provided entities (can be an empty list)
+     */
+    public static List<Class<?>> getProvidedEntities(KeycloakSession session) {
+        List<Class<?>> providedEntityClasses = new ArrayList<>();
+        // Get all configured entity providers.
+        Set<JpaEntityProvider> entityProviders = session.getAllProviders(JpaEntityProvider.class);
+        // For every provider, add all entity classes to the list.
+        for (JpaEntityProvider entityProvider : entityProviders) {
+            providedEntityClasses.addAll(entityProvider.getEntities());
+        }
+        return providedEntityClasses;
+    }
+
 }
diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 94c6512..5aba7ba 100644
--- a/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -17,4 +17,5 @@
 
 org.keycloak.connections.jpa.JpaConnectionSpi
 org.keycloak.connections.jpa.updater.JpaUpdaterSpi
-org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi
\ No newline at end of file
+org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi
+org.keycloak.connections.jpa.entityprovider.JpaEntitySpi
diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderLoader.java b/server-spi/src/main/java/org/keycloak/provider/ProviderLoader.java
index 76c2950..2d7a07a 100644
--- a/server-spi/src/main/java/org/keycloak/provider/ProviderLoader.java
+++ b/server-spi/src/main/java/org/keycloak/provider/ProviderLoader.java
@@ -24,6 +24,19 @@ import java.util.List;
  */
 public interface ProviderLoader {
 
+    /**
+     * Load the SPI definitions themselves.
+     *
+     * @return a list of Spi definition objects
+     */
+    List<Spi> loadSpis();
+
+    /**
+     * Load all provider factories of a specific SPI.
+     *
+     * @param spi the Spi definition
+     * @return a list of provider factories
+     */
     List<ProviderFactory> load(Spi spi);
 
 }
diff --git a/services/src/main/java/org/keycloak/provider/DefaultProviderLoader.java b/services/src/main/java/org/keycloak/provider/DefaultProviderLoader.java
index 26aa19d..5f4b19d 100644
--- a/services/src/main/java/org/keycloak/provider/DefaultProviderLoader.java
+++ b/services/src/main/java/org/keycloak/provider/DefaultProviderLoader.java
@@ -33,6 +33,15 @@ public class DefaultProviderLoader implements ProviderLoader {
     }
 
     @Override
+    public List<Spi> loadSpis() {
+        LinkedList<Spi> list = new LinkedList<>();
+        for (Spi spi : ServiceLoader.load(Spi.class, classLoader)) {
+            list.add(spi);
+        }
+        return list;
+    }
+
+    @Override
     public List<ProviderFactory> load(Spi spi) {
         LinkedList<ProviderFactory> list = new LinkedList<ProviderFactory>();
         for (ProviderFactory f : ServiceLoader.load(spi.getProviderFactoryClass(), classLoader)) {
diff --git a/services/src/main/java/org/keycloak/provider/ProviderManager.java b/services/src/main/java/org/keycloak/provider/ProviderManager.java
index 997c68a..e906df9 100644
--- a/services/src/main/java/org/keycloak/provider/ProviderManager.java
+++ b/services/src/main/java/org/keycloak/provider/ProviderManager.java
@@ -65,6 +65,20 @@ public class ProviderManager {
         }
     }
 
+    public synchronized List<Spi> loadSpis() {
+        // Use a map to prevent duplicates, since the loaders may have overlapping classpaths.
+        Map<String, Spi> spiMap = new HashMap<>();
+        for (ProviderLoader loader : loaders) {
+            List<Spi> spis = loader.loadSpis();
+            if (spis != null) {
+                for (Spi spi : spis) {
+                    spiMap.put(spi.getName(), spi);
+                }
+            }
+        }
+        return new LinkedList<>(spiMap.values());
+    }
+
     public synchronized List<ProviderFactory> load(Spi spi) {
         List<ProviderFactory> factories = cache.get(spi.getName());
         if (factories == null) {
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
index 90b495c..172de6e 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java
@@ -70,8 +70,9 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
 
         ProviderManager pm = new ProviderManager(getClass().getClassLoader(), Config.scope().getArray("providers"));
 
-        ServiceLoader<Spi> load = ServiceLoader.load(Spi.class, getClass().getClassLoader());
-        loadSPIs(pm, load);
+        // Load the SPI classes through the provider manager, so both Keycloak internal SPI's and
+        // the ones defined in deployed modules will be found.
+        loadSPIs(pm, pm.loadSpis());
         for ( Map<String, ProviderFactory> factories : factoriesMap.values()) {
             for (ProviderFactory factory : factories.values()) {
                 factory.postInit(this);
@@ -79,8 +80,8 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory {
         }
     }
 
-    protected void loadSPIs(ProviderManager pm, ServiceLoader<Spi> load) {
-        for (Spi spi : load) {
+    protected void loadSPIs(ProviderManager pm, List<Spi> spiList) {
+        for (Spi spi : spiList) {
             spis.add(spi);
 
             Map<String, ProviderFactory> factories = new HashMap<String, ProviderFactory>();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index b26d95a..1eee46b 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -496,9 +496,9 @@ public class RealmAdminResource {
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
     public List<EventRepresentation> getEvents(@QueryParam("type") List<String> types, @QueryParam("client") String client,
-            @QueryParam("user") String user, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo,
-            @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult,
-            @QueryParam("max") Integer maxResults) {
+                                               @QueryParam("user") String user, @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo,
+                                               @QueryParam("ipAddress") String ipAddress, @QueryParam("first") Integer firstResult,
+                                               @QueryParam("max") Integer maxResults) {
         auth.init(RealmAuth.Resource.EVENTS).requireView();
 
         EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
@@ -585,10 +585,10 @@ public class RealmAdminResource {
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
     public List<AdminEventRepresentation> getEvents(@QueryParam("operationTypes") List<String> operationTypes, @QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient,
-            @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
-            @QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
-            @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
-            @QueryParam("max") Integer maxResults) {
+                                                    @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
+                                                    @QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
+                                                    @QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
+                                                    @QueryParam("max") Integer maxResults) {
         auth.init(RealmAuth.Resource.EVENTS).requireView();
 
         EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index 4161763..53c9aaf 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -101,7 +101,7 @@ public class RealmsResource {
 
     @Path("{realm}/protocol/{protocol}")
     public Object getProtocol(final @PathParam("realm") String name,
-                                            final @PathParam("protocol") String protocol) {
+                              final @PathParam("protocol") String protocol) {
         RealmModel realm = init(name);
 
         LoginProtocolFactory factory = (LoginProtocolFactory)session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, protocol);
@@ -239,7 +239,7 @@ public class RealmsResource {
     @Path("{realm}/.well-known/{provider}")
     @Produces(MediaType.APPLICATION_JSON)
     public Response getWellKnown(final @PathParam("realm") String name,
-                              final @PathParam("provider") String providerName) {
+                                 final @PathParam("provider") String providerName) {
         init(name);
 
         WellKnownProvider wellKnown = session.getProvider(WellKnownProvider.class, providerName);
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 50bb346..55b31a0 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -18,3 +18,4 @@
 org.keycloak.exportimport.ClientDescriptionConverterSpi
 org.keycloak.wellknown.WellKnownSpi
 org.keycloak.services.clientregistration.ClientRegistrationSpi
+