Search code examples
jpakeycloakentitylistenerskeycloak-plugin

Keycloak providers' better technique of registering entity listener


I'm developing Keycloak providers that listen to updates to user profiles and perform some data processing logics.

The technique that I currently use for my 1st provider is to create a class with @PrePersist, @PreUpdate, @PostLoad annotations and register that class inside META-INF/orm.xml file like below:

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd"
                 version="2.1">
    <persistence-unit-metadata>
        <persistence-unit-defaults>
            <entity-listeners>
                <entity-listener class="my.keycloak.provider.EntityListener" />
            </entity-listeners>
        </persistence-unit-defaults>
    </persistence-unit-metadata>
</entity-mappings>

Registering that provider by dropping its JAR file into my Keycloak installation providers folder works flawlessly.

The issue began when I developed my 2nd provider using the same technique and dropping its JAR file into the same Keycloak installation. The following errors appeared during bin/kc.sh build:

ERROR: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
  [error]: Build step io.quarkus.hibernate.orm.deployment.HibernateOrmProcessor#defineJpaEntities threw an exception: java.lang.IllegalStateException: Persistence unit '<default>' references mapping file 'META-INF/orm.xml', but multiple resources with this path exist in the classpath, and it is not possible to resolve the ambiguity. URLs of matching resources found in the classpath: [jar:file:///opt/keycloak/lib/../providers/keycloak-pii-data-encryption-1.0.jar!/META-INF/orm.xml, jar:file:///opt/keycloak/lib/../providers/keycloak-attributes-verification-1.0.jar!/META-INF/orm.xml]
  at io.quarkus.hibernate.orm.deployment.xml.QuarkusMappingFileParser.locateMappingFile(QuarkusMappingFileParser.java:98)
  at io.quarkus.hibernate.orm.deployment.xml.QuarkusMappingFileParser.parse(QuarkusMappingFileParser.java:57)
  at io.quarkus.hibernate.orm.deployment.JpaJandexScavenger.enlistExplicitMappings(JpaJandexScavenger.java:162)
  at io.quarkus.hibernate.orm.deployment.JpaJandexScavenger.discoverModelAndRegisterForReflection(JpaJandexScavenger.java:104)
  at io.quarkus.hibernate.orm.deployment.HibernateOrmProcessor.defineJpaEntities(HibernateOrmProcessor.java:409)
  at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
  at java.base/java.lang.reflect.Method.invoke(Method.java:580)
  at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:849)
  at io.quarkus.builder.BuildContext.run(BuildContext.java:256)
  at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
  at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
  at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
  at java.base/java.lang.Thread.run(Thread.java:1583)
  at org.jboss.threads.JBossThread.run(JBossThread.java:501)

From the errors, it is apparent that I can't have two providers that each has its own META-INF/orm.xml file registered under the same Keycloak installation.

I have tried to rename the orm.xml files to different names but the entity listeners are not being triggered thus I concluded that the file names must be exactly that.

Reading the documentation website of Quarkus (the Java runtime Keycloak is built on), it seems like I need to modify either application.properties or persistence.xml but that means I need to mess with the core Keycloak, and hence defeating the purpose of developing providers as plug-and-play enhancements to Keycloak.

Are there any other techniques for multiple Keycloak providers to register entity listeners? (Other than combining those functionalities into a single provider which is less than ideal especially in scenarios where separate developers develop separate providers)


Solution

  • Using Hibernate Integrator SPI:

    First, the entity listener class needs to implement one or more xxxEventListener interfaces under the package org.hibernate.event.spi for example org.hibernate.event.spi.PostInsertEventListener.

    Obviously the class needs to implement the interface(s) method(s), for example:

    @Override
    public void onPostInsert(PostInsertEvent pie) {
        if (pie.getEntity() instanceof UserAttributeEntity uae) {
                // Do something with uae variable
            }
        }
    }
    

    Next, we need a class that implements org.hibernate.integrator.spi.Integrator interface. For my case, I use the same event listener class.

    @Override
    public void integrate(Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor sessionFactory) {
        EventListenerRegistry eventListenerRegistry = sessionFactory
                .getServiceRegistry()
                .getService(EventListenerRegistry.class);
        eventListenerRegistry.appendListeners(EventType.POST_INSERT, this);
    }
    
    @Override
    public void disintegrate(SessionFactoryImplementor sfi, SessionFactoryServiceRegistry sfsr) {
    }
    

    Finally, to activate this class, we need to register the class that implements the Integrator interface inside:

    META-INF/services/org.hibernate.integrator.spi.Integrator
    

    With this technique, each provider can implement its own Integrator to register the event listeners and not conflict with each other.