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)
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.