This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 5d0170d465135d4cff56063d13a09816406006fa
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Jun 8 12:11:56 2022 +0200

    Generalize the mechanism for propagating an event from parent store to 
children resources.
    For now only `CloseEvent` uses it.
    
    https://issues.apache.org/jira/browse/SIS-549
---
 .../sis/storage/event/CascadedStoreEvent.java      | 130 +++++++++++++++++++
 .../org/apache/sis/storage/event/CloseEvent.java   |  53 +++-----
 .../org/apache/sis/storage/event/StoreEvent.java   |   6 +-
 .../apache/sis/storage/event/StoreListener.java    |   6 +-
 .../apache/sis/storage/event/StoreListeners.java   | 140 ++++++++++++---------
 .../sis/storage/event/StoreListenersTest.java      |   1 +
 6 files changed, 232 insertions(+), 104 deletions(-)

diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CascadedStoreEvent.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CascadedStoreEvent.java
new file mode 100644
index 0000000000..6a2c82f09a
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CascadedStoreEvent.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sis.storage.event;
+
+import java.lang.ref.WeakReference;
+import java.util.concurrent.ExecutionException;
+import org.apache.sis.storage.Resource;
+
+
+/**
+ * An event which, when occurring on a parent resource, is also fired by all 
children resources.
+ * For example when an {@link org.apache.sis.storage.Aggregate} (typically a 
data store) is closed,
+ * a {@link CloseEvent} is automatically fired by all resources that are 
components of the aggregate.
+ * This is similar to "cascade delete" in SQL databases.
+ *
+ * <h2>Difference between {@code StoreEvent} and {@code 
CascadedStoreEvent}</h2>
+ * By default {@link StoreEvent}s are propagated from children to parents.
+ * For example when a {@link WarningEvent} occurs in a child resource,
+ * all listeners registered on that resource are notified,
+ * then all listeners registered on the parent resource, and so forth until 
the root resource.
+ * All those listeners receive the same {@link WarningEvent} instance,
+ * i.e. the {@linkplain WarningEvent#getSource() event source} is always the 
resource where the warning occurred.
+ *
+ * <p>By contrast {@code CascadedStoreEvent} are fired in the opposite 
direction, from parent to children.
+ * Furthermore each child creates its own {@code CascadedStoreEvent}. For 
example if a {@link CloseEvent} is
+ * fired in a {@link org.apache.sis.storage.DataStore}, then it causes all 
resources of that data store to fire
+ * their own {@link CloseEvent} declaring themselves as the {@linkplain 
CloseEvent#getSource() event source}.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since 1.3
+ *
+ * @param <E> the type of the event subclass.
+ *
+ * @version 1.3
+ * @module
+ */
+public abstract class CascadedStoreEvent<E extends CascadedStoreEvent<E>> 
extends StoreEvent {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -1319167650150261418L;
+
+    /**
+     * Constructs an event that occurred in the given resource.
+     *
+     * @param  source  the resource where the event occurred.
+     * @throws IllegalArgumentException if the given source is null.
+     */
+    protected CascadedStoreEvent(Resource source) {
+        super(source);
+        super.consume(true);        // Necessary for avoiding never-ending 
loop.
+    }
+
+    /**
+     * Creates a new event of the same type than this event but with a 
different source.
+     * This method is invoked for creating the event to be fired by the 
children of the
+     * resource where the original event occurred.
+     *
+     * @param  child  the child resource for which to create the event to 
cascade.
+     * @return an event of the same type than this event but with the given 
resource.
+     */
+    protected abstract E forSource(Resource child);
+
+
+
+
+    /**
+     * A listener to register on the parent of a resource for cascading an 
event to the children.
+     *
+     * @see StoreListeners#cascadedListeners
+     */
+    static final class ParentListener<E extends CascadedStoreEvent<E>> 
implements StoreListener<E> {
+        /**
+         * The type of event to listen.
+         */
+        private final Class<E> eventType;
+
+        /**
+         * The parent resource to listen to.
+         */
+        private final StoreListeners parent;
+
+        /**
+         * The listeners to notify.
+         */
+        private final WeakReference<StoreListeners> listeners;
+
+        /**
+         * Creates a new listener to be registered on the parent of the given 
set of listeners.
+         *
+         * @param  eventType  the type of event to listen.
+         * @param  parent     the parent resource to listen to.
+         * @param  listeners  the child set of listeners.
+         */
+        ParentListener(final Class<E> eventType, final StoreListeners parent, 
final StoreListeners listeners) {
+            this.eventType = eventType;
+            this.parent    = parent;
+            this.listeners = new WeakReference<>(listeners);
+        }
+
+        /**
+         * Invoked when an event is fired on a parent resource.
+         * This method causes similar event to be fired on children resources.
+         */
+        @Override public void eventOccured(final E event) {
+            final StoreListeners r = listeners.get();
+            if (r == null) {
+                parent.removeListener(eventType, this);
+            } else try {
+                StoreListeners.fire(r, eventType, 
event.forSource(r.getSource()));
+            } catch (ExecutionException e) {
+                StoreListeners.canNotNotify("fire (cascade)", e);
+            }
+        }
+    }
+}
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CloseEvent.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CloseEvent.java
index 4ca37e07a2..6e56d532d1 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CloseEvent.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CloseEvent.java
@@ -21,14 +21,19 @@ import org.apache.sis.storage.Resource;
 
 /**
  * Notifies listeners that a resource or a data store is being closed and 
should no longer be used.
- * Resources are automatically considered closed when a parent resource or 
data store is closed.
+ * Firing a {@code CloseEvent} on a parent resource (typically a data store)
+ * automatically fires a {@code CloseEvent} in all children resources.
+ * See {@link CascadedStoreEvent} javadoc for more information.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @since   1.3
+ * @since 1.3
+ *
+ * @see StoreListeners#close()
+ *
  * @version 1.3
  * @module
  */
-public class CloseEvent extends StoreEvent {
+public class CloseEvent extends CascadedStoreEvent<CloseEvent> {
     /**
      * For cross-version compatibility.
      */
@@ -44,44 +49,14 @@ public class CloseEvent extends StoreEvent {
         super(source);
     }
 
-
-
-
     /**
-     * A listener to register on the parent of a resource for closing the 
resource
-     * automatically if the parent is closed.
+     * Creates a new event of the same type than this event but with a 
different source.
      *
-     * @see StoreListeners#closeListener
+     * @param  child  the child resource for which to create the event to 
cascade.
+     * @return an event of the same type than this event but with the given 
resource.
      */
-    static final class ParentListener implements StoreListener<CloseEvent> {
-        /**
-         * The parent resource to listen to.
-         */
-        private final Resource parent;
-
-        /**
-         * The listeners to notify.
-         */
-        private final StoreListeners listeners;
-
-        /**
-         * Creates a new listener to be registered on the parent of the given 
set of listeners.
-         *
-         * @param  parent     the parent resource to listen to.
-         * @param  listeners  the child set of listeners.
-         */
-        ParentListener(final Resource parent, final StoreListeners listeners) {
-            this.parent    = parent;
-            this.listeners = listeners;
-        }
-
-        /**
-         * Invoked when a parent resource or data store is closed.
-         */
-        @Override public void eventOccured(final CloseEvent event) {
-            if (event.getSource() == parent) {      // Necessary check for 
avoiding never-ending loop.
-                listeners.close();
-            }
-        }
+    @Override
+    protected CloseEvent forSource(final Resource child) {
+        return new CloseEvent(child);
     }
 }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreEvent.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreEvent.java
index a7f50abfd1..5b976f129d 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreEvent.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreEvent.java
@@ -123,14 +123,14 @@ public abstract class StoreEvent extends EventObject 
implements Localized {
     /**
      * Marks this event as consumed. This stops its further propagation to 
other listeners.
      *
-     * @param  later  {@code true} for consuming now, or {@code false} for 
consuming after all listeners
+     * @param  later  {@code false} for consuming now, or {@code true} for 
consuming after all listeners
      *         registered on the {@linkplain #getSource() source} resource but 
before listeners registered
      *         on the parent resource or data store.
      *
      * @since 1.3
      */
     public void consume(final boolean later) {
-        if (later) consumed = true;
-        else consumeLater = true;
+        if (later) consumeLater = true;
+        else consumed = true;
     }
 }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListener.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListener.java
index 9be960bffc..23c438bd3c 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListener.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListener.java
@@ -35,7 +35,7 @@ import org.apache.sis.storage.Resource;
  * @author  Johann Sorel (Geomatys)
  * @version 1.0
  *
- * @param  <T>  the type of events of interest to this listener.
+ * @param  <E>  the type of events of interest to this listener.
  *
  * @see StoreEvent
  * @see Resource#addListener(Class, StoreListener)
@@ -43,7 +43,7 @@ import org.apache.sis.storage.Resource;
  * @since 1.0
  * @module
  */
-public interface StoreListener<T extends StoreEvent> extends EventListener {
+public interface StoreListener<E extends StoreEvent> extends EventListener {
     /**
      * Invoked <em>after</em> a warning or a change occurred in a resource.
      * The {@link StoreEvent#getSource()} method gives the resource where the 
event occurred.
@@ -52,5 +52,5 @@ public interface StoreListener<T extends StoreEvent> extends 
EventListener {
      *
      * @param  event  description of the change or warning that occurred in a 
resource. Shall not be {@code null}.
      */
-    void eventOccured(T event);
+    void eventOccured(E event);
 }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
index 924d1f66cd..a580155f55 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java
@@ -123,30 +123,30 @@ public class StoreListeners implements Localized {
                          JDK9.setOf(WarningEvent.class, CloseEvent.class);
 
     /**
-     * The {@link CloseEvent.ParentListener} registered on {@link #parent}, or 
{@code null} if not yet created.
-     * This is created the first time that a {@link CloseEvent} listener is 
registered on a resource which is
-     * not the root resource. Those listeners are handled in a special way, 
because a close event on the root
-     * should propagate to all children.
+     * The {@link CascadedStoreEvent.ParentListener}s registered on {@link 
#parent}.
+     * This is created the first time that a {@link CascadedStoreEvent} 
listener is registered on a resource
+     * which is not the root resource. Those listeners are handled in a 
special way, because a closing event
+     * on the root resource should cause all children to also fire their own 
{@link CloseEvent}.
      */
-    private StoreListener<CloseEvent> closeListener;
+    private Map<Class<?>, StoreListener<?>> cascadedListeners;
 
     /**
      * All listeners for a given even type.
      *
-     * @param  <T>  the type of events of interest to the listeners.
+     * @param  <E>  the type of events of interest to the listeners.
      */
-    private static final class ForType<T extends StoreEvent> {
+    private static final class ForType<E extends StoreEvent> {
         /**
          * The types for which listeners have been registered.
          */
-        final Class<T> type;
+        final Class<E> type;
 
         /**
          * The listeners for the {@linkplain #type event type}, or {@code 
null} if none.
          * This is a <cite>copy on write</cite> array: no elements are 
modified after an array has been created.
          */
         @SuppressWarnings("VolatileArrayField")
-        private volatile StoreListener<? super T>[] listeners;
+        private volatile StoreListener<? super E>[] listeners;
 
         /**
          * Next element in the chain of listeners. Intentionally final; if we 
want to remove an element
@@ -161,7 +161,7 @@ public class StoreListeners implements Localized {
          * @param type  type of events of interest for listeners in this 
element.
          * @param next  the next element in the chained list, or {@code null} 
if none.
          */
-        ForType(final Class<T> type, final ForType<?> next) {
+        ForType(final Class<E> type, final ForType<?> next) {
             this.type = type;
             this.next = next;
         }
@@ -173,11 +173,11 @@ public class StoreListeners implements Localized {
          *
          * <p>It is caller responsibility to perform synchronization and to 
verify that the listener is non-null.</p>
          */
-        final void add(final StoreListener<? super T> listener) {
-            final StoreListener<? super T>[] list = listeners;
+        final void add(final StoreListener<? super E> listener) {
+            final StoreListener<? super E>[] list = listeners;
             final int length = (list != null) ? list.length : 0;
             @SuppressWarnings({"unchecked", "rawtypes"}) // Generic array 
creation.
-            final StoreListener<? super T>[] copy = new StoreListener[length + 
1];
+            final StoreListener<? super E>[] copy = new StoreListener[length + 
1];
             if (list != null) {
                 System.arraycopy(list, 0, copy, 0, length);
             }
@@ -194,8 +194,8 @@ public class StoreListeners implements Localized {
          * @param  listener  the listener to remove.
          * @return {@code true} if the list of listeners is empty after this 
method call.
          */
-        final boolean remove(final StoreListener<? super T> listener) {
-            StoreListener<? super T>[] list = listeners;
+        final boolean remove(final StoreListener<? super E> listener) {
+            StoreListener<? super E>[] list = listeners;
             if (list != null) {
                 for (int i=list.length; --i >= 0;) {
                     if (list[i] == listener) {
@@ -243,16 +243,16 @@ public class StoreListeners implements Localized {
          * @return the {@code done} map, created when first needed.
          * @throws ExecutionException if at least one listener failed to 
execute.
          */
-        final Map<StoreListener<?>,Boolean> eventOccured(final T event, 
Map<StoreListener<?>,Boolean> done)
+        final Map<StoreListener<?>,Boolean> eventOccured(final E event, 
Map<StoreListener<?>,Boolean> done)
                 throws ExecutionException
         {
             RuntimeException error = null;
-            final StoreListener<? super T>[] list = listeners;
+            final StoreListener<? super E>[] list = listeners;
             if (list != null) {
                 if (done == null) {
                     done = new IdentityHashMap<>(list.length);
                 }
-                for (final StoreListener<? super T> listener : list) {
+                for (final StoreListener<? super E> listener : list) {
                     if (event.isConsumed()) break;
                     if (done.put(listener, Boolean.TRUE) == null) try {
                         listener.eventOccured(event);
@@ -576,7 +576,7 @@ public class StoreListeners implements Localized {
      */
     @SuppressWarnings("unchecked")
     public void warning(final LogRecord description, final Filter onUnhandled) 
{
-        if (!fire(new WarningEvent(source, description), WarningEvent.class) &&
+        if (!fire(WarningEvent.class, new WarningEvent(source, description)) &&
                 (onUnhandled == null || onUnhandled.isLoggable(description)))
         {
             final String name = description.getLoggerName();
@@ -604,10 +604,19 @@ public class StoreListeners implements Localized {
      * @param  method  name of the method invoking this method.
      * @param  error   the exception that occurred.
      */
-    private static void canNotNotify(final String method, final 
ExecutionException error) {
+    static void canNotNotify(final String method, final ExecutionException 
error) {
         Logging.unexpectedException(Logger.getLogger(Modules.STORAGE), 
StoreListeners.class, method, error);
     }
 
+    /**
+     * @deprecated Replaced by {@link #fire(Class, StoreEvent)} for 
consistency with the argument order
+     *             in all other methods of this class.
+     */
+    @Deprecated
+    public <E extends StoreEvent> boolean fire(final E event, final Class<E> 
eventType) {
+        return fire(eventType, event);
+    }
+
     /**
      * Sends the given event to all listeners registered for the given type or 
for a super-type.
      * This method first notifies the listeners registered in this {@code 
StoreListeners}, then
@@ -619,16 +628,18 @@ public class StoreListeners implements Localized {
      * {@linkplain Logging#unexpectedException(Logger, Class, String, 
Throwable) log record}.
      * Runtime exceptions in listeners do not cause this method to fail.</p>
      *
-     * @param  <T>        compile-time value of the {@code eventType} argument.
-     * @param  event      the event to fire.
+     * @param  <E>        compile-time value of the {@code eventType} argument.
      * @param  eventType  the type of the event to be fired.
+     * @param  event      the event to fire.
      * @return {@code true} if the event has been sent to at least one 
listener.
      * @throws IllegalArgumentException if the given event type is not one of 
the types of events
      *         that this {@code StoreListeners} can fire.
      *
      * @see #close()
+     *
+     * @since 1.3
      */
-    public <T extends StoreEvent> boolean fire(final T event, final Class<T> 
eventType) {
+    public <E extends StoreEvent> boolean fire(final Class<E> eventType, final 
E event) {
         ArgumentChecks.ensureNonNull("event", event);
         ArgumentChecks.ensureNonNull("eventType", eventType);
         final Set<Class<? extends StoreEvent>> permittedEventTypes = 
this.permittedEventTypes;
@@ -636,7 +647,7 @@ public class StoreListeners implements Localized {
             throw illegalEventType(eventType);
         }
         try {
-            return fire(this, event, eventType);
+            return fire(this, eventType, event);
         } catch (ExecutionException ex) {
             canNotNotify("fire", ex);
             return true;
@@ -649,15 +660,16 @@ public class StoreListeners implements Localized {
      *
      * <p>This method does not need (and should not) be synchronized.</p>
      *
-     * @param  <T>        compile-time value of the {@code eventType} argument.
+     * @param  <E>        compile-time value of the {@code eventType} argument.
      * @param  m          the set of listeners that may be interested in the 
event.
-     * @param  event      the event to fire.
      * @param  eventType  the type of the event to be fired.
+     * @param  event      the event to fire.
      * @return {@code true} if the event has been sent to at least one 
listener.
-     * @throws ExecutionException
+     * @throws ExecutionException if an exception is thrown inside {@link 
StoreListener#eventOccured(StoreEvent)}.
+     *         All other listeners continue to receive the event before {@code 
ExecutionException} is thrown.
      */
     @SuppressWarnings("unchecked")
-    private static <T extends StoreEvent> boolean fire(StoreListeners m, final 
T event, final Class<T> eventType)
+    static <E extends StoreEvent> boolean fire(StoreListeners m, final 
Class<E> eventType, final E event)
             throws ExecutionException
     {
         Map<StoreListener<?>,Boolean> done = null;
@@ -665,13 +677,13 @@ public class StoreListeners implements Localized {
         do {
             for (ForType<?> e = m.listeners; e != null; e = e.next) {
                 if (e.type.isAssignableFrom(eventType)) try {
-                    done = ((ForType<? super T>) e).eventOccured(event, done);
+                    done = ((ForType<? super E>) e).eventOccured(event, done);
                 } catch (ExecutionException ex) {
                     if (error == null) error = ex;
                     else error.getCause().addSuppressed(ex.getCause());
                 }
-                if (event.isConsumedForParent()) break;
             }
+            if (event.isConsumedForParent()) break;
             m = m.parent;
         } while (m != null);
         if (error != null) {
@@ -722,21 +734,21 @@ public class StoreListeners implements Localized {
      * This side-effect is applied on the assumption that the registered 
listener will handle
      * warnings in its own way, for example by showing warnings in a widget.
      *
-     * @param  <T>        compile-time value of the {@code eventType} argument.
+     * @param  <E>        compile-time value of the {@code eventType} argument.
      * @param  eventType  type of {@link StoreEvent} to listen (can not be 
{@code null}).
      * @param  listener   listener to notify about events.
      *
      * @see Resource#addListener(Class, StoreListener)
      */
-    @SuppressWarnings("unchecked")
-    public synchronized <T extends StoreEvent> void addListener(final Class<T> 
eventType, final StoreListener<? super T> listener) {
+    @SuppressWarnings({"rawtypes","unchecked"})
+    public synchronized <E extends StoreEvent> void addListener(final Class<E> 
eventType, final StoreListener<? super E> listener) {
         ArgumentChecks.ensureNonNull("listener",  listener);
         ArgumentChecks.ensureNonNull("eventType", eventType);
         if (isPossibleEvent(permittedEventTypes, eventType)) {
-            ForType<T> ce = null;
+            ForType<E> ce = null;
             for (ForType<?> e = listeners; e != null; e = e.next) {
                 if (e.type.equals(eventType)) {
-                    ce = (ForType<T>) e;
+                    ce = (ForType<E>) e;
                     break;
                 }
             }
@@ -746,13 +758,18 @@ public class StoreListeners implements Localized {
             }
             ce.add(listener);
             /*
-             * If we are adding a listener for `CloseEvent`, we may need (as a 
special case)
-             * to register a listener to the parent for propagating the close 
events.
+             * If we are adding a listener for `CascadedStoreEvent`, we may 
need
+             * to register a listener in the parent for cascading the events.
              */
-            if (parent != null) {
-                if (closeListener == null && 
CloseEvent.class.isAssignableFrom(eventType)) {
-                    closeListener = new 
CloseEvent.ParentListener(parent.source, this);
-                    parent.addListener(CloseEvent.class, closeListener);
+            if (parent != null && 
CascadedStoreEvent.class.isAssignableFrom(eventType)) {
+                if (cascadedListeners == null) {
+                    cascadedListeners = new IdentityHashMap<>(4);
+                }
+                StoreListener cascade = cascadedListeners.get(eventType);
+                if (cascade == null) {
+                    cascade = new CascadedStoreEvent.ParentListener(eventType, 
parent, this);
+                    cascadedListeners.put(eventType, cascade);
+                    parent.addListener(eventType, cascade);
                 }
             }
         }
@@ -774,27 +791,22 @@ public class StoreListeners implements Localized {
      * there are no remaining listeners for warning events, then this {@code 
StoreListeners} will send future
      * warnings to the loggers.
      *
-     * @param  <T>        compile-time value of the {@code eventType} argument.
+     * @param  <E>        compile-time value of the {@code eventType} argument.
      * @param  eventType  type of {@link StoreEvent} which were listened (can 
not be {@code null}).
      * @param  listener   listener to stop notifying about events.
      *
      * @see Resource#removeListener(Class, StoreListener)
      */
-    @SuppressWarnings("unchecked")
-    public synchronized <T extends StoreEvent> void removeListener(Class<T> 
eventType, StoreListener<? super T> listener) {
+    @SuppressWarnings({"rawtypes","unchecked"})
+    public synchronized <E extends StoreEvent> void removeListener(Class<E> 
eventType, StoreListener<? super E> listener) {
         ArgumentChecks.ensureNonNull("listener",  listener);
         ArgumentChecks.ensureNonNull("eventType", eventType);
         for (ForType<?> e = listeners; e != null; e = e.next) {
             if (e.type.equals(eventType)) {
-                if (((ForType<T>) e).remove(listener) && parent != null) {
-                    /*
-                     * If the list of listeners become empty and if the event 
type was `CloseEvent`,
-                     * cleanup the parent list of listeners too. We do a 
special case for close events
-                     * because closing a parent data store implicitly closes 
the child resources.
-                     */
-                    if (closeListener != null && 
CloseEvent.class.isAssignableFrom(eventType)) {
-                        parent.removeListener(CloseEvent.class, closeListener);
-                        closeListener = null;
+                if (((ForType<E>) e).remove(listener) && cascadedListeners != 
null) {
+                    final StoreListener cascade = 
cascadedListeners.remove(eventType);
+                    if (cascade != null) {
+                        parent.removeListener(eventType, cascade);
                     }
                 }
                 break;
@@ -902,26 +914,36 @@ public class StoreListeners implements Localized {
      * Sends a {@link CloseEvent} to all listeners registered for that kind of 
event,
      * then discards listeners in this instance (but not in parents).
      * Because listeners are discarded, invoking this method many times
-     * on the same instance have no effect after the first invocation.
+     * on the same instance has no effect after the first invocation.
      *
      * <p>If one or many {@link StoreListener#eventOccured(StoreEvent)} 
implementations throw
      * a {@link RuntimeException}, those exceptions will be collected and 
reported in a single
      * {@linkplain Logging#unexpectedException(Logger, Class, String, 
Throwable) log record}.
      * Runtime exceptions in listeners do not cause this method to fail.</p>
      *
-     * @see #fire(StoreEvent, Class)
+     * @see #fire(Class, StoreEvent)
      * @see DataStore#close()
+     * @see CloseEvent
      *
      * @since 1.3
      */
+    @SuppressWarnings({"rawtypes","unchecked"})
     public void close() {
         try {
-            fire(this, new CloseEvent(source), CloseEvent.class);
+            /*
+             * We use the private static method instead of `fire(Class, 
StoreEvent)` public method
+             * because calls to `close()` should never fail (except with 
`java.lang.Error` because
+             * we do not want to hide serious errors), so we bypass argument 
validation and method
+             * overriding as a safety.
+             */
+            fire(this, CloseEvent.class, new CloseEvent(source));
         } catch (ExecutionException ex) {
             canNotNotify("close", ex);
         }
-        closeListener = null;
-        listeners     = null;       // Volatile field should be last.
-        // Do not remove parent listeners; maybe parent will be closed next.
+        listeners = null;
+        /*
+         * No need to cleanup `cascadedListeners`. It does not hurt (those 
listeners practically
+         * become no-op) and the objects are probably going to be garbage 
collected soon anyway.
+         */
     }
 }
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
index f353231094..140b54a305 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
@@ -143,6 +143,7 @@ public final strictfp class StoreListenersTest extends 
TestCase implements Store
 
             @Override public void eventOccured(CloseEvent event) {
                 assertSame(resource, event.getSource());
+                assertFalse(isClosed);
                 isClosed = true;
             }
         }

Reply via email to