From 2a4c10a663a23bebbf1f0cfdceb440d57e37a25e Mon Sep 17 00:00:00 2001 From: Gail Badner Date: Wed, 4 Mar 2020 12:47:37 -0800 Subject: [PATCH] HHH-13890 Add support for custom event types and listeners --- .../internal/EventListenerRegistryImpl.java | 57 ++- .../org/hibernate/event/spi/EventType.java | 49 ++- .../hibernate/event/CustomEventTypeTest.java | 71 ++++ .../CustomEventTypeRegisterListenerTest.java | 396 ++++++++++++++++++ ...ntTypeListenerRegistryConcurrencyTest.java | 168 ++++++++ 5 files changed, 733 insertions(+), 8 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/event/CustomEventTypeTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/test/events/CustomEventTypeRegisterListenerTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/test/events/EventTypeListenerRegistryConcurrencyTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/event/service/internal/EventListenerRegistryImpl.java b/hibernate-core/src/main/java/org/hibernate/event/service/internal/EventListenerRegistryImpl.java index 3a847044a3c9..3531be529865 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/service/internal/EventListenerRegistryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/event/service/internal/EventListenerRegistryImpl.java @@ -97,7 +97,7 @@ public class EventListenerRegistryImpl implements EventListenerRegistry, Stoppab private final SessionFactoryImplementor sessionFactory; private final CallbackRegistryImplementor callbackRegistry; - private final EventListenerGroupImpl[] registeredEventListeners; + private volatile EventListenerGroupImpl[] registeredEventListeners; private CallbackBuilder callbackBuilder; /** @@ -160,9 +160,56 @@ public void prepare(MetadataImplementor metadata) { } } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private synchronized EventListenerGroupImpl getOrCreateEventListenerGroup(EventType eventType) { + final int sizeOriginal = this.registeredEventListeners.length; + final EventListenerGroupImpl[] registeredEventListenersNew; + if ( eventType.ordinal() < sizeOriginal ) { + final EventListenerGroupImpl registeredEventListener = registeredEventListeners[ eventType.ordinal() ]; + if ( registeredEventListener != null ) { + // eventType has already been registered; + return registeredEventListener; // EARLY RETURN + } + // eventType has not been registered yet. + // Its EventListenerGroupImpl will be created and added to registeredEventListeners below. + // There is already space for the new EventType in this.registeredEventListeners. + registeredEventListenersNew = this.registeredEventListeners; + } + else { + // eventType is a custom EventType, and there is not enough space in + // registeredEventListeners to accommodate it. + + // Allocate a new array to hold listener groups for *all* EventType values that currently exist. + // This way an existing, unregistered EventType with a larger ordinal will not require another + // allocation when it gets registered in the future. + final int sizeNew = Math.max( eventType.ordinal() + 1, EventType.values().size() ); + registeredEventListenersNew = new EventListenerGroupImpl[sizeNew]; + + // First copy the existing listeners to registeredEventListenersNew. + System.arraycopy( this.registeredEventListeners, 0, registeredEventListenersNew, 0, sizeOriginal ); + } + + final EventListenerGroupImpl listenerGroup = new EventListenerGroupImpl( + eventType, + EventListenerRegistryImpl.this + ); + registeredEventListenersNew[eventType.ordinal()] = listenerGroup; + + // Now update the reference. + this.registeredEventListeners = registeredEventListenersNew; + + return listenerGroup; + } + @SuppressWarnings({ "unchecked" }) public EventListenerGroupImpl getEventListenerGroup(EventType eventType) { - EventListenerGroupImpl listeners = registeredEventListeners[ eventType.ordinal() ]; + if ( registeredEventListeners.length < eventType.ordinal() + 1 ) { + // eventTpe is a custom EventType that has not been registered. + // registeredEventListeners array was not allocated enough space to + // accommodate it. + throw new HibernateException( "Unable to find listeners for type [" + eventType.eventName() + "]" ); + } + final EventListenerGroupImpl listeners = registeredEventListeners[ eventType.ordinal() ]; if ( listeners == null ) { throw new HibernateException( "Unable to find listeners for type [" + eventType.eventName() + "]" ); } @@ -218,7 +265,7 @@ private T instantiateListener(Class listenerClass) { @Override @SafeVarargs public final void setListeners(EventType type, T... listeners) { - EventListenerGroupImpl registeredListeners = getEventListenerGroup( type ); + EventListenerGroupImpl registeredListeners = getOrCreateEventListenerGroup( type ); registeredListeners.clear(); if ( listeners != null ) { for ( T listener : listeners ) { @@ -236,7 +283,7 @@ public final void appendListeners(EventType type, Class... l @Override @SafeVarargs public final void appendListeners(EventType type, T... listeners) { - getEventListenerGroup( type ).appendListeners( listeners ); + getOrCreateEventListenerGroup( type ).appendListeners( listeners ); } @Override @@ -248,7 +295,7 @@ public final void prependListeners(EventType type, Class... @Override @SafeVarargs public final void prependListeners(EventType type, T... listeners) { - getEventListenerGroup( type ).prependListeners( listeners ); + getOrCreateEventListenerGroup( type ).prependListeners( listeners ); } private EventListenerGroupImpl[] buildListenerGroups() { diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/EventType.java b/hibernate-core/src/main/java/org/hibernate/event/spi/EventType.java index ea50235f9316..9cda48fc2ca6 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/EventType.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/EventType.java @@ -10,11 +10,14 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Collection; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import org.hibernate.HibernateException; +import org.hibernate.internal.CoreLogging; + +import org.jboss.logging.Logger; /** * Enumeration of the recognized types of events, including meta-information about each. @@ -22,7 +25,7 @@ * @author Steve Ebersole */ public final class EventType { - + private static final Logger LOG = CoreLogging.logger( EventType.class ); private static AtomicInteger typeCounter = new AtomicInteger( 0 ); public static final EventType LOAD = create( "load", LoadEventListener.class ); @@ -76,6 +79,46 @@ public final class EventType { public static final EventType POST_COLLECTION_REMOVE = create( "post-collection-remove", PostCollectionRemoveEventListener.class ); public static final EventType POST_COLLECTION_UPDATE = create( "post-collection-update", PostCollectionUpdateEventListener.class ); + /** + * Add a new event type. + * + * @param name - name of the custom event + * @param listenerClass - the base listener class or interface associated with the entity type + * @param - listenerClass + * @return the custom {@link EventType} + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static synchronized EventType addCustomEventType(String name, Class listenerClass) { + if ( name == null || listenerClass == null ) { + throw new HibernateException( "Custom EventType name and associated class must be non-null." ); + } + + final EventType eventType = EVENT_TYPE_BY_NAME_MAP.computeIfAbsent( + name, + ( e -> { + final EventType eventTypeNew = EventType.create( name, listenerClass ); + LOG.debugf( + "Added custom EventType: [%s], ordinal=[%d], listener=[%s].", + name, + eventTypeNew.ordinal, + listenerClass.toString() + ); + return eventTypeNew; + } ) + ); + // There's no way to know if there was a pre-existing EventType with + // the same name and listener, so ignore that case. + // Just check that listener is the same as listenerClass + if ( !listenerClass.equals( eventType.baseListenerInterface ) ) { + throw new HibernateException( + "Could not add EventType [" + name + "] with listener Class [" + + "]. An EventType with that name already exists with listener [" + + listenerClass.getName() + + "]." + ); + } + return eventType; + } private static EventType create(String name, Class listenerClass) { return new EventType( name, listenerClass ); @@ -89,7 +132,7 @@ private static EventType create(String name, Class listenerClass) { new PrivilegedAction>() { @Override public Map run() { - final Map typeByNameMap = new HashMap(); + final Map typeByNameMap = new ConcurrentHashMap<>(); for ( Field field : EventType.class.getDeclaredFields() ) { if ( EventType.class.isAssignableFrom( field.getType() ) ) { try { diff --git a/hibernate-core/src/test/java/org/hibernate/event/CustomEventTypeTest.java b/hibernate-core/src/test/java/org/hibernate/event/CustomEventTypeTest.java new file mode 100644 index 000000000000..5bbbc8dbe883 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/event/CustomEventTypeTest.java @@ -0,0 +1,71 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.event; + +import org.hibernate.HibernateException; +import org.hibernate.event.spi.EventType; +import org.hibernate.event.spi.LoadEventListener; + +import org.hibernate.testing.TestForIssue; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Gail Badner + */ + +@TestForIssue( jiraKey = "HHH-13890" ) +public class CustomEventTypeTest { + private final String EVENT_TYPE_NAME = "operation"; + private final String OTHER_EVENT_TYPE_NAME = "other-operation"; + + @Test + public void testAddCustomEventType() { + final int numberOfEventTypesOriginal = EventType.values().size(); + + try { + EventType.resolveEventTypeByName( EVENT_TYPE_NAME ); + fail( "Should have thrown HibernateException" ); + } + catch(HibernateException expected) { + } + + final EventType eventType = EventType.addCustomEventType( EVENT_TYPE_NAME, CustomListener.class ); + assertEquals( EVENT_TYPE_NAME, eventType.eventName() ); + assertEquals( CustomListener.class, eventType.baseListenerInterface() ); + assertEquals( numberOfEventTypesOriginal, eventType.ordinal() ); + assertTrue( EventType.values().contains( eventType ) ); + assertEquals( numberOfEventTypesOriginal + 1, EventType.values().size() ); + + final EventType otherEventType = EventType.addCustomEventType( OTHER_EVENT_TYPE_NAME, OtherCustomListener.class ); + assertEquals( OTHER_EVENT_TYPE_NAME, otherEventType.eventName() ); + assertEquals( OtherCustomListener.class, otherEventType.baseListenerInterface() ); + assertEquals( numberOfEventTypesOriginal + 1, otherEventType.ordinal() ); + assertEquals( numberOfEventTypesOriginal + 2, EventType.values().size() ); + + // Adding an event type with the same name and base listener as one that exists, should be OK. + EventType.addCustomEventType( "load", LoadEventListener.class ); + + // Adding an event type with the same name but different listener as one that exists, should fail. + try { + EventType.addCustomEventType( "load", CustomListener.class ); + fail( "Should have thrown HibernateException" ); + } + catch (HibernateException expected) { + } + } + + public interface CustomListener { + } + + public interface OtherCustomListener { + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/events/CustomEventTypeRegisterListenerTest.java b/hibernate-core/src/test/java/org/hibernate/test/events/CustomEventTypeRegisterListenerTest.java new file mode 100644 index 000000000000..0480cfe7aa21 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/events/CustomEventTypeRegisterListenerTest.java @@ -0,0 +1,396 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.events; + +import java.util.concurrent.atomic.AtomicInteger; +import javax.persistence.Entity; +import javax.persistence.Id; + +import org.hibernate.HibernateException; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.service.spi.EventListenerGroup; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.jpa.event.spi.CallbackRegistry; +import org.hibernate.jpa.event.spi.CallbackRegistryConsumer; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +@TestForIssue( jiraKey = "HHH-13890") +public class CustomEventTypeRegisterListenerTest extends BaseCoreFunctionalTestCase { + public enum Category { + CLOTHING, + FURNITURE + } + + private final TheIntegrator theIntegrator = new TheIntegrator(); + + @Override + protected void prepareBootstrapRegistryBuilder(BootstrapServiceRegistryBuilder builder) { + builder.applyIntegrator( theIntegrator ); + } + + @Test + public void testSetListenerClasses() { + testNormalUsage( theIntegrator.eventTypeForSetListenerClasses(), UsesSetClasses.class ); + } + + @Test + public void testSetListenerObjects() { + testNormalUsage( theIntegrator.eventTypeForSetListenerObjects(), UsesSetObjects.class ); + } + + @Test + public void testAppendListenerClasses() { + testNormalUsage( theIntegrator.eventTypeForAppendListenerClasses(), UsesAppendClasses.class ); + } + + @Test + public void testAppendListenerObjects() { + testNormalUsage( theIntegrator.eventTypeForAppendListenerObjects(), UsesAppendObjects.class ); + } + + @Test + public void testPrependListenerClasses() { + testNormalUsage( theIntegrator.eventTypeForPrependListenerClasses(), UsesPrependClasses.class ); + } + + @Test + public void testPrependListenerObjects() { + testNormalUsage( theIntegrator.eventTypeForPrependListenerObjects(), UsesPrependObjects.class ); + } + + @Test + public void testUnregisteredEventType() { + final EventListenerRegistry eventListenerRegistry = + sessionFactory().getServiceRegistry().getService( EventListenerRegistry.class ); + try { + eventListenerRegistry.getEventListenerGroup( theIntegrator.eventTypeUnregistered() ); + fail( "HibernateException should have been thrown." ); + } + catch (HibernateException expected) { + } + } + + private void testNormalUsage(EventType eventType, Class baseListenerClass) { + final Item clothing = new Item( Category.CLOTHING ); + final Item furniture = new Item( Category.FURNITURE ); + final Item other = new Item(); + + final EventListenerRegistry eventListenerRegistry = + sessionFactory().getServiceRegistry().getService( EventListenerRegistry.class ); + final EventListenerGroup group = + eventListenerRegistry.getEventListenerGroup( eventType ); + for ( Object listener : group.listeners() ) { + assertNotNull( ( (ItemNameGeneratorListener) listener).getCallbackRegistry() ); + } + + final ItemNameGeneratorEvent clothingEvent = new ItemNameGeneratorEvent( clothing ); + group.fireEventOnEachListener( clothingEvent, Listener::onGenerateItemName ); + assertEquals( "C100", clothing.name ); + + final ItemNameGeneratorEvent furnitureEvent = new ItemNameGeneratorEvent( furniture ); + group.fireEventOnEachListener( furnitureEvent, Listener::onGenerateItemName ); + assertEquals( "F200", furniture.name ); + + final ItemNameGeneratorEvent otherEvent = new ItemNameGeneratorEvent( other ); + group.fireEventOnEachListener( otherEvent, Listener::onGenerateItemName ); + assertEquals( "O300", other.name ); + } + + @Entity(name = "Item") + public static class Item { + + @Id + private int id; + + private Category category; + + private String name; + + Item() { + } + Item(Category category) { + this.category = category; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + public static class ItemNameGeneratorEvent { + private Item item; + + public ItemNameGeneratorEvent(Item item) { + this.item = item; + } + + public Item getItem() { + return item; + } + } + + public interface Listener { + void onGenerateItemName(ItemNameGeneratorEvent event); + } + + public interface ItemNameGeneratorListener extends Listener, CallbackRegistryConsumer { + CallbackRegistry getCallbackRegistry(); + } + + public interface UsesSetClasses extends Listener { + } + public interface UsesSetObjects extends Listener { + } + public interface UsesAppendClasses extends Listener { + } + public interface UsesAppendObjects extends Listener{ + } + public interface UsesPrependClasses extends Listener { + } + public interface UsesPrependObjects extends Listener { + } + public interface Unregistered { + } + + public static abstract class AbstractItemNameGeneratorListener implements ItemNameGeneratorListener { + private AtomicInteger counter; + private CallbackRegistry callbackRegistry = null; + + protected AbstractItemNameGeneratorListener(int startValue) { + counter = new AtomicInteger( startValue ); + } + + public void onGenerateItemName(ItemNameGeneratorEvent event) { + if ( event.item.name == null && getCategory() == event.item.category ) { + event.item.name = getPrefix() + counter.getAndIncrement(); + } + } + + public abstract Category getCategory(); + public abstract String getPrefix(); + + @Override + public void injectCallbackRegistry(CallbackRegistry callbackRegistry) { + this.callbackRegistry = callbackRegistry; + } + + @Override + public CallbackRegistry getCallbackRegistry() { + return callbackRegistry; + } + } + + public static abstract class ClothingGeneratorListener extends AbstractItemNameGeneratorListener { + protected ClothingGeneratorListener() { + super( 100 ); + } + + @Override + public Category getCategory() { + return Category.CLOTHING; + } + + @Override + public String getPrefix() { + return "C"; + } + } + + public static class ClothingGeneratorListenerSetClasses extends ClothingGeneratorListener implements UsesSetClasses { + } + public static class ClothingGeneratorListenerSetObjects extends ClothingGeneratorListener implements UsesSetObjects { + } + public static class ClothingGeneratorListenerAppendClasses extends ClothingGeneratorListener implements UsesAppendClasses { + } + public static class ClothingGeneratorListenerAppendObjects extends ClothingGeneratorListener implements UsesAppendObjects { + } + public static class ClothingGeneratorListenerPrependClasses extends ClothingGeneratorListener implements UsesPrependClasses { + } + public static class ClothingGeneratorListenerPrependObjects extends ClothingGeneratorListener implements UsesPrependObjects { + } + + public static abstract class FurnitureGeneratorListener extends AbstractItemNameGeneratorListener { + protected FurnitureGeneratorListener() { + super( 200 ); + } + + @Override + public Category getCategory() { + return Category.FURNITURE; + } + + @Override + public String getPrefix() { + return "F"; + } + } + + public static class FurnitureGeneratorListenerSetClasses extends FurnitureGeneratorListener implements UsesSetClasses { + } + public static class FurnitureGeneratorListenerSetObjects extends FurnitureGeneratorListener implements UsesSetObjects { + } + public static class FurnitureGeneratorListenerAppendClasses extends FurnitureGeneratorListener implements UsesAppendClasses { + } + public static class FurnitureGeneratorListenerAppendObjects extends FurnitureGeneratorListener implements UsesAppendObjects { + } + public static class FurnitureGeneratorListenerPrependClasses extends FurnitureGeneratorListener implements UsesPrependClasses { + } + public static class FurnitureGeneratorListenerPrependObjects extends FurnitureGeneratorListener implements UsesPrependObjects { + } + + public static abstract class OtherGeneratorListener extends AbstractItemNameGeneratorListener { + protected OtherGeneratorListener() { + super( 300 ); + } + + @Override + public Category getCategory() { + return null; + } + + @Override + public String getPrefix() { + return "O"; + } + } + + public static class OtherGeneratorListenerSetClasses extends OtherGeneratorListener implements UsesSetClasses { + } + public static class OtherGeneratorListenerSetObjects extends OtherGeneratorListener implements UsesSetObjects { + } + public static class OtherGeneratorListenerAppendClasses extends OtherGeneratorListener implements UsesAppendClasses { + } + public static class OtherGeneratorListenerAppendObjects extends OtherGeneratorListener implements UsesAppendObjects { + } + public static class OtherGeneratorListenerPrependClasses extends OtherGeneratorListener implements UsesPrependClasses { + } + public static class OtherGeneratorListenerPrependObjects extends OtherGeneratorListener implements UsesPrependObjects { + } + + public static class TheIntegrator implements Integrator { + + private EventType eventTypeForSetListenerClasses; + private EventType eventTypeForSetListenerObjects; + + private EventType eventTypeForPrependListenerClasses; + private EventType eventTypeForPrependListenerObjects; + + private EventType eventTypeForAppendListenerClasses; + private EventType eventTypeForAppendListenerObjects; + + private EventType eventTypeUnregistered; + + @Override + public void integrate( + Metadata metadata, + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + eventTypeForSetListenerClasses = EventType.addCustomEventType( "eventTypeForSetListenerClasses", UsesSetClasses.class ); + eventTypeForSetListenerObjects = EventType.addCustomEventType( "eventTypeForSetListenerObjects", UsesSetObjects.class ); + + eventTypeForPrependListenerClasses = EventType.addCustomEventType( "eventTypeForPrependListenerClasses", UsesPrependClasses.class ); + eventTypeForPrependListenerObjects = EventType.addCustomEventType( "eventTypeForPrependListenerObjects", UsesPrependObjects.class ); + + eventTypeForAppendListenerClasses = EventType.addCustomEventType( "eventTypeForAppendListenerClasses", UsesAppendClasses.class ); + eventTypeForAppendListenerObjects = EventType.addCustomEventType( "eventTypeForAppendListenerObjects", UsesAppendObjects.class ); + + final EventListenerRegistry eventListenerRegistry = serviceRegistry.getService( EventListenerRegistry.class ); + + eventListenerRegistry.setListeners( + eventTypeForSetListenerClasses, + ClothingGeneratorListenerSetClasses.class, + FurnitureGeneratorListenerSetClasses.class, + OtherGeneratorListenerSetClasses.class + ); + eventListenerRegistry.setListeners( + eventTypeForSetListenerObjects, + new ClothingGeneratorListenerSetObjects(), + new FurnitureGeneratorListenerSetObjects(), + new OtherGeneratorListenerSetObjects() + ); + + eventListenerRegistry.prependListeners( + eventTypeForPrependListenerClasses, + ClothingGeneratorListenerPrependClasses.class, + FurnitureGeneratorListenerPrependClasses.class, + OtherGeneratorListenerPrependClasses.class + ); + eventListenerRegistry.prependListeners( + eventTypeForPrependListenerObjects, + new ClothingGeneratorListenerPrependObjects(), + new FurnitureGeneratorListenerPrependObjects(), + new OtherGeneratorListenerPrependObjects() + ); + + eventListenerRegistry.appendListeners( + eventTypeForAppendListenerClasses, + ClothingGeneratorListenerAppendClasses.class, + FurnitureGeneratorListenerAppendClasses.class, + OtherGeneratorListenerAppendClasses.class + ); + eventListenerRegistry.appendListeners( + eventTypeForAppendListenerObjects, + new ClothingGeneratorListenerAppendObjects(), + new FurnitureGeneratorListenerAppendObjects(), + new OtherGeneratorListenerAppendObjects() + ); + + // add an EventType that does not get registered + eventTypeUnregistered = EventType.addCustomEventType( "unregistered", Unregistered.class ); + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { + } + + public EventType eventTypeForSetListenerClasses() { + return eventTypeForSetListenerClasses; + } + + public EventType eventTypeForSetListenerObjects() { + return eventTypeForSetListenerObjects; + } + + public EventType eventTypeForPrependListenerClasses() { + return eventTypeForPrependListenerClasses; + } + + public EventType eventTypeForPrependListenerObjects() { + return eventTypeForPrependListenerObjects; + } + + public EventType eventTypeForAppendListenerClasses() { + return eventTypeForAppendListenerClasses; + } + + public EventType eventTypeForAppendListenerObjects() { + return eventTypeForAppendListenerObjects; + } + + public EventType eventTypeUnregistered() { + return eventTypeUnregistered; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/events/EventTypeListenerRegistryConcurrencyTest.java b/hibernate-core/src/test/java/org/hibernate/test/events/EventTypeListenerRegistryConcurrencyTest.java new file mode 100644 index 000000000000..7e1a72743735 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/events/EventTypeListenerRegistryConcurrencyTest.java @@ -0,0 +1,168 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.test.events; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.hibernate.boot.Metadata; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.BootstrapServiceRegistry; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.service.spi.EventListenerGroup; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.internal.CoreLogging; +import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; + +import org.hibernate.testing.TestForIssue; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Concurrency test that registering a custom EventType does not interfere with + * looking up a registered EventListenerGroup. + * + * @author Gail Badner + */ +@TestForIssue( jiraKey = "HHH-13890") +public class EventTypeListenerRegistryConcurrencyTest { + private static CoreMessageLogger LOG = CoreLogging.messageLogger( EventTypeListenerRegistryConcurrencyTest.class ); + + @Test + public void test() { + final TheConcurrencyIntegrator integrator = new TheConcurrencyIntegrator(); + BootstrapServiceRegistry bsr = new BootstrapServiceRegistryBuilder() + .applyIntegrator( integrator ) + .build(); + SessionFactoryImplementor sessionFactory = null; + try { + final StandardServiceRegistry ssr = new StandardServiceRegistryBuilder( bsr ).build(); + sessionFactory = (SessionFactoryImplementor) new MetadataSources( ssr ) + .buildMetadata() + .getSessionFactoryBuilder() + .build(); + integrator.checkResults( sessionFactory.getServiceRegistry() ); + } + finally { + if ( sessionFactory != null ) { + sessionFactory.close(); + } + bsr.close(); + } + } + + private static class TheConcurrencyIntegrator implements Integrator { + private final int NUMBER_OF_EVENT_TYPES_NEW = 10000; + private final int NUMBER_OF_THREADS = 10; + private final AtomicInteger START_VALUE = new AtomicInteger( 0 ); + private final List exceptions = new ArrayList<>(); + private final Set customEventTypes = new HashSet<>( NUMBER_OF_EVENT_TYPES_NEW ); + + // Capture number of "standard" event types (before adding custom event types). + private final int numberEventTypesBefore = EventType.values().size(); + + @Override + public void integrate( + Metadata metadata, + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + + final EventListenerRegistry eventListenerRegistry = serviceRegistry.getService( EventListenerRegistry.class ); + + final Runnable createAndRegisterEventTypes = () -> { + for ( int i = START_VALUE.getAndIncrement(); + i < NUMBER_OF_EVENT_TYPES_NEW; + i += NUMBER_OF_THREADS ) { + final EventType eventType = EventType.addCustomEventType( + "event" + i, + DummyListener.class + ); + try { + eventListenerRegistry.setListeners( eventType, new DummyListener() ); + eventListenerRegistry.getEventListenerGroup( eventType ); + } + catch (Exception ex) { + LOG.info( ex ); + exceptions.add( ex ); + } + } + }; + + final Runnable eventListenerGroupsGetter = () -> { + while( true ) { + try { + assertNotNull( eventListenerRegistry.getEventListenerGroup( EventType.AUTO_FLUSH ) ); + } + catch (Exception ex) { + exceptions.add( ex ); + } + } + }; + + final Thread[] threadsCreateAndRegisterEventTypes = new Thread[NUMBER_OF_THREADS]; + final Thread[] threadsEventListenerGroupsGetter = new Thread[NUMBER_OF_THREADS]; + for ( int i = 0 ; i < NUMBER_OF_THREADS; i++ ) { + threadsCreateAndRegisterEventTypes[i] = new Thread( createAndRegisterEventTypes ); + threadsEventListenerGroupsGetter[i] = new Thread( eventListenerGroupsGetter ); + } + + for ( int i = 0 ; i < NUMBER_OF_THREADS; i++ ) { + threadsCreateAndRegisterEventTypes[i].start(); + threadsEventListenerGroupsGetter[i].start(); + } + + try { + for ( int i = 0; i < NUMBER_OF_THREADS; i++ ) { + threadsCreateAndRegisterEventTypes[i].join(); + threadsEventListenerGroupsGetter[i].interrupt(); + } + } + catch (InterruptedException ex) { + LOG.info( ex ); + exceptions.add( ex ); + } + } + + @Override + public void disintegrate( + SessionFactoryImplementor sessionFactory, + SessionFactoryServiceRegistry serviceRegistry) { + } + + public void checkResults(ServiceRegistry serviceRegistry) { + LOG.info( exceptions ); + assertTrue( exceptions.isEmpty() ); + assertEquals( numberEventTypesBefore + NUMBER_OF_EVENT_TYPES_NEW, EventType.values().size() ); + final EventListenerRegistry eventListenerRegistry = serviceRegistry.getService( EventListenerRegistry.class ); + for ( EventType eventType : customEventTypes) { + final EventListenerGroup eventListenerGroup = eventListenerRegistry.getEventListenerGroup( eventType ); + final Iterator iterator = eventListenerGroup.listeners().iterator(); + assertTrue( iterator.hasNext() ); + assertTrue( DummyListener.class.isInstance( iterator.next() ) ); + assertFalse( iterator.hasNext() ); + } + } + } + + private static class DummyListener { + } +}