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 489bf1c33fbf3512198ce75a53b70a63cff04ca1
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Mar 26 15:13:35 2022 +0100

    Allow subclasses to specify that they will not fire any kind of event other 
than warnings.
    This commit reproduces a slight optimization which has been removed in 
previous commit.
    That optimization was avoiding strong references to unused listeners, but 
we can not keep
    that arbitrary filtering anymore if the API goes public. We had to make it 
explicit opt-in.
---
 .../apache/sis/storage/landsat/LandsatStore.java   |   3 +
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |   3 +
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |   3 +
 .../java/org/apache/sis/storage/sql/SQLStore.java  |   3 +
 .../sis/internal/storage/AbstractResource.java     |   6 -
 .../org/apache/sis/internal/storage/Resources.java |   5 +
 .../sis/internal/storage/Resources.properties      |   1 +
 .../sis/internal/storage/Resources_fr.properties   |   1 +
 .../org/apache/sis/internal/storage/csv/Store.java |   1 +
 .../apache/sis/internal/storage/folder/Store.java  |   1 +
 .../org/apache/sis/internal/storage/wkt/Store.java |   1 +
 .../org/apache/sis/internal/storage/xml/Store.java |   1 +
 .../apache/sis/storage/event/StoreListeners.java   | 193 ++++++++++++++++++---
 .../sis/storage/event/StoreListenersTest.java      |  24 ++-
 .../org/apache/sis/internal/storage/gpx/Store.java |   1 +
 15 files changed, 214 insertions(+), 33 deletions(-)

diff --git 
a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/LandsatStore.java
 
b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/LandsatStore.java
index d2e9508..30fa5eb 100644
--- 
a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/LandsatStore.java
+++ 
b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/LandsatStore.java
@@ -152,6 +152,9 @@ public class LandsatStore extends DataStore implements 
Aggregate {
             throw new UnsupportedStorageException(super.getLocale(), 
LandsatStoreProvider.NAME,
                     connector.getStorage(), 
connector.getOption(OptionKey.OPEN_OPTIONS));
         }
+        if (getClass() == LandsatStore.class) {
+            listeners.useWarningEventsOnly();
+        }
     }
 
     /**
diff --git 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
index 8c935fa..e71c44f 100644
--- 
a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ 
b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -208,6 +208,9 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
         } catch (IOException e) {
             throw new DataStoreException(e);
         }
+        if (getClass() == GeoTiffStore.class) {
+            listeners.useWarningEventsOnly();
+        }
     }
 
     /**
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStore.java
 
b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStore.java
index 921315c..407aa55 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStore.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStore.java
@@ -128,6 +128,9 @@ public class NetcdfStore extends DataStore implements 
Aggregate {
             final NameFactory f = decoder.nameFactory;
             decoder.namespace = f.createNameSpace(f.createLocalName(null, id), 
null);
         }
+        if (getClass() == NetcdfStore.class) {
+            listeners.useWarningEventsOnly();
+        }
     }
 
     /**
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java
index 7d2dd94..0b5bdf1 100644
--- 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/SQLStore.java
@@ -162,6 +162,9 @@ public class SQLStore extends DataStore implements 
Aggregate {
         }
         this.tableNames = ArraysExt.resize(tableNames, tableCount);
         this.queries    = ArraysExt.resize(queries,    queryCount);
+        if (getClass() == SQLStore.class) {
+            listeners.useWarningEventsOnly();
+        }
     }
 
     /**
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
index ef5b9cf..007322b 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractResource.java
@@ -46,7 +46,6 @@ import org.apache.sis.xml.NilReason;
  *   <li>{@link #getEnvelope()} (recommended)</li>
  *   <li>{@link #createMetadata()} (optional)</li>
  *   <li>{@link #getSynchronizationLock()} (optional)</li>
- *   <li>{@link #addListener(Class, StoreListener)} (if this resource is 
writable)</li>
  * </ul>
  *
  * <h2>Thread safety</h2>
@@ -59,11 +58,6 @@ import org.apache.sis.xml.NilReason;
  * @module
  */
 public abstract class AbstractResource implements Resource {
-    /*
-     * Warning: do not implement `org.apache.sis.util.Localized` as it
-     * may cause an infinite loop in calls to `listeners.getLocale()`.
-     */
-
     /**
      * The set of registered {@link StoreListener}s for this resources.
      * This {@code StoreListeners} while typically have {@link 
DataStore#listeners} has a parent.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
index d7d24a0..d222f51 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
@@ -226,6 +226,11 @@ public final class Resources extends IndexedResourceBundle 
{
         public static final short FoliationRepresentation = 38;
 
         /**
+         * This resource should not fire events of type “{0}”.
+         */
+        public static final short IllegalEventType_1 = 65;
+
+        /**
          * The {0} data store does not accept features of type “{1}”.
          */
         public static final short IllegalFeatureType_2 = 7;
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
index ec8ebc2..b450bc4 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
@@ -52,6 +52,7 @@ FeatureNotFound_2                 = Feature \u201c{1}\u201d 
has not been found i
 FileAlreadyExists_2               = A {1,choice,0#file|1#directory} already 
exists at \u201c{0}\u201d.
 FileIsNotAResourceDirectory_1     = The \u201c{0}\u201d file is not a 
directory of resources.
 FoliationRepresentation           = Whether to assemble trajectory fragments 
(lines in CSV file) in a single feature instance.
+IllegalEventType_1                = This resource should not fire events of 
type \u201c{0}\u201d.
 IllegalFeatureType_2              = The {0} data store does not accept 
features of type \u201c{1}\u201d.
 IllegalInputTypeForReader_2       = The {0} reader does not accept inputs of 
type \u2018{1}\u2019.
 IllegalOutputTypeForWriter_2      = The {0} writer does not accept outputs of 
type \u2018{1}\u2019.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
index 6cbf683..9c37915 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
@@ -57,6 +57,7 @@ FeatureNotFound_2                 = L\u2019entit\u00e9 
\u00ab\u202f{1}\u202f\u00
 FileAlreadyExists_2               = Un {1,choice,0#fichier|1#r\u00e9pertoire} 
existe d\u00e9j\u00e0 \u00e0 l\u2019emplacement \u00ab\u202f{0}\u202f\u00bb.
 FileIsNotAResourceDirectory_1     = Le fichier \u00ab\u202f{0}\u202f\u00bb 
n\u2019est pas un r\u00e9pertoire de ressources.
 FoliationRepresentation           = Indique s\u2019il faut assembler les 
fragments de trajectoires (lignes dans un fichier CSV) dans une entit\u00e9 
unique.
+IllegalEventType_1                = Cette ressource ne devrait pas lancer des 
\u00e9v\u00e9nements de type \u00ab\u202f{0}\u202f\u00bb.
 IllegalFeatureType_2              = Le format {0} ne stocke pas de 
donn\u00e9es de type \u00ab\u202f{1}\u202f\u00bb.
 IllegalInputTypeForReader_2       = Le lecteur {0} n\u2019accepte pas des 
entr\u00e9s de type \u2018{1}\u2019.
 IllegalOutputTypeForWriter_2      = L\u2019encodeur {0} n\u2019accepte pas des 
sorties de type \u2018{1}\u2019.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
index 7d369ce..7bb6765 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
@@ -305,6 +305,7 @@ final class Store extends URIDataStore implements 
FeatureSet {
         this.featureType = featureType;
         this.foliation   = foliation;
         this.dissociate |= (timeEncoding == null);
+        listeners.useWarningEventsOnly();
     }
 
     /**
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java
index d689bcb..2042aac 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/Store.java
@@ -178,6 +178,7 @@ class Store extends DataStore implements StoreResource, 
Aggregate, DirectoryStre
         children   = new ConcurrentHashMap<>();
         children.put(path.toRealPath(), this);
         componentProvider = format;
+        listeners.useWarningEventsOnly();
     }
 
     /**
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/Store.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/Store.java
index 9fd77e3..1c346dc 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/Store.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/Store.java
@@ -86,6 +86,7 @@ final class Store extends URIDataStore {
         objects = new ArrayList<>();
         source  = connector.commit(Reader.class, StoreProvider.NAME);
         library = connector.getOption(OptionKey.GEOMETRY_LIBRARY);
+        listeners.useWarningEventsOnly();
     }
 
     /**
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/xml/Store.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/xml/Store.java
index 1036f27..659a52f 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/xml/Store.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/xml/Store.java
@@ -102,6 +102,7 @@ final class Store extends URIDataStore implements Filter {
             throw new UnsupportedStorageException(super.getLocale(), 
StoreProvider.NAME,
                     connector.getStorage(), 
connector.getOption(OptionKey.OPEN_OPTIONS));
         }
+        listeners.useWarningEventsOnly();
     }
 
     /**
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 97cfbc3..48d6eea 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
@@ -17,7 +17,10 @@
 package org.apache.sis.storage.event;
 
 import java.util.Map;
+import java.util.Set;
+import java.util.HashSet;
 import java.util.Locale;
+import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.logging.Level;
 import java.util.logging.Logger;
@@ -30,8 +33,11 @@ import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.collection.Containers;
+import org.apache.sis.internal.storage.Resources;
 import org.apache.sis.internal.storage.StoreResource;
 import org.apache.sis.internal.storage.StoreUtilities;
+import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.Resource;
@@ -98,6 +104,19 @@ public class StoreListeners implements Localized {
     private volatile ForType<?> listeners;
 
     /**
+     * All types of of events that may be fired, or {@code null} if no 
restriction.
+     * This is a <cite>copy on write</cite> set: no elements are modified 
after a set has been created.
+     *
+     * @see #setUsableEventTypes(Class...)
+     */
+    private volatile Set<Class<? extends StoreEvent>> permittedEventTypes;
+
+    /**
+     * Frequently used value for {@link #permittedEventTypes}.
+     */
+    private static final Set<Class<? extends StoreEvent>> WARNING_EVENT_TYPE = 
Collections.singleton(WarningEvent.class);
+
+    /**
      * All listeners for a given even type.
      *
      * @param  <T>  the type of events of interest to the listeners.
@@ -176,6 +195,22 @@ public class StoreListeners implements Localized {
         }
 
         /**
+         * Removes all listeners which will never receive any kind of events.
+         *
+         * Note: ideally we would remove the whole {@code ForType} object, but 
it would require to rebuild the whole
+         * {@link #listeners} chain. It is not worth because this method 
should never be invoked if callers invoked
+         * the {@link #setUsableEventTypes(Class...)} at construction time (a 
recommended practice).
+         */
+        static void removeUnreachables(ForType<?> listeners, final Set<Class<? 
extends StoreEvent>> permittedEventTypes) {
+            while (listeners != null) {
+                if (!isPossibleEvent(permittedEventTypes, listeners.type)) {
+                    listeners.listeners = null;
+                }
+                listeners = listeners.next;
+            }
+        }
+
+        /**
          * Returns {@code true} if this element has at least one listener.
          */
         final boolean hasListeners() {
@@ -213,6 +248,11 @@ public class StoreListeners implements Localized {
      * will be notified as well as listeners registered in this {@code 
StoreListeners}.
      * Each listener will be notified only once even if it has been registered 
in two places.
      *
+     * <h4>Permitted even types</h4>
+     * If the parent restricts the usable event types to a subset of {@link 
StoreEvent} subtypes,
+     * then this {@code StoreListeners} inherits those restrictions. The list 
of usable types can
+     * be {@linkplain #setUsableEventTypes rectricted more} but can not be 
relaxed.
+     *
      * @param parent  the manager to notify in addition to this manager, or 
{@code null} if none.
      * @param source  the source of events. Can not be null.
      */
@@ -220,6 +260,9 @@ public class StoreListeners implements Localized {
         ArgumentChecks.ensureNonNull("source", source);
         this.source = source;
         this.parent = parent;
+        if (parent != null) {
+            permittedEventTypes = parent.permittedEventTypes;
+        }
     }
 
     /**
@@ -290,16 +333,8 @@ public class StoreListeners implements Localized {
      */
     @Override
     public Locale getLocale() {
-        StoreListeners m = this;
-        do {
-            final Resource src = m.source;
-            if (src != this && src != m && src instanceof Localized) {
-                final Locale locale = ((Localized) src).getLocale();
-                if (locale != null) return locale;
-            }
-            m = m.parent;
-        } while (m != null);
-        return null;
+        final DataStore ds = getDataStore(this);
+        return (ds != null) ? ds.getLocale() : null;
     }
 
     /**
@@ -336,10 +371,30 @@ public class StoreListeners implements Localized {
     }
 
     /**
+     * Notifies this {@code StoreListeners} that it will fire only {@link 
WarningEvent}s. This method is a
+     * shortcut for <code>{@linkplain setUsableEventTypes 
setUsableEventTypes}(WarningEvent.class)}</code>,
+     * provided because frequently used by read-only data store 
implementations.
+     *
+     * @see #setUsableEventTypes(Class...)
+     * @see WarningEvent
+     *
+     * @since 1.2
+     */
+    public synchronized void useWarningEventsOnly() {
+        final Set<Class<? extends StoreEvent>> current = permittedEventTypes;
+        if (current == null) {
+            permittedEventTypes = WARNING_EVENT_TYPE;
+        } else if (!WARNING_EVENT_TYPE.equals(current)) {
+            throw illegalEventType(WarningEvent.class);
+        }
+        ForType.removeUnreachables(listeners, WARNING_EVENT_TYPE);
+    }
+
+    /**
      * Reports a warning described by the given message.
      *
      * <p>This method is a shortcut for <code>{@linkplain #warning(Level, 
String, Exception)
-     * warning}({@linkplain Level#WARNING}, message, null)</code>.
+     * warning}({@linkplain Level#WARNING}, null, exception)</code>.</p>
      *
      * @param  message  the warning message to report.
      */
@@ -355,7 +410,7 @@ public class StoreListeners implements Localized {
      * See {@linkplain #warning(Level, String, Exception) below} for more 
explanation.
      *
      * <p>This method is a shortcut for <code>{@linkplain #warning(Level, 
String, Exception)
-     * warning}({@linkplain Level#WARNING}, null, exception)</code>.
+     * warning}({@linkplain Level#WARNING}, null, exception)</code>.</p>
      *
      * @param  exception  the exception to report.
      */
@@ -373,7 +428,7 @@ public class StoreListeners implements Localized {
      * warnings). See {@linkplain #warning(Level, String, Exception) below} 
for more explanation.
      *
      * <p>This method is a shortcut for <code>{@linkplain #warning(Level, 
String, Exception)
-     * warning}({@linkplain Level#WARNING}, message, exception)</code>.
+     * warning}({@linkplain Level#WARNING}, message, exception)</code>.</p>
      *
      * @param  message    the warning message to report, or {@code null} if 
none.
      * @param  exception  the exception to report, or {@code null} if none.
@@ -483,7 +538,8 @@ public class StoreListeners implements Localized {
      *
      * @param  description  warning details provided as a log record.
      * @param  onUnhandled  filter invoked if the record has not been handled 
by a {@link StoreListener},
-     *                      or {@code null} if none.
+     *         or {@code null} if none. This filter determines whether the 
record should be sent to the
+     *         logger returned by {@link #getLogger()}.
      *
      * @since 1.2
      */
@@ -514,11 +570,17 @@ public class StoreListeners implements Localized {
      * @param  event      the event to fire.
      * @param  eventType  the type of events to be fired.
      * @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.
      */
     @SuppressWarnings("unchecked")
     public <T extends StoreEvent> boolean fire(final T event, final Class<T> 
eventType) {
         ArgumentChecks.ensureNonNull("event", event);
         ArgumentChecks.ensureNonNull("eventType", eventType);
+        final Set<Class<? extends StoreEvent>> permittedEventTypes = 
this.permittedEventTypes;
+        if (permittedEventTypes != null && 
!permittedEventTypes.contains(eventType)) {
+            throw illegalEventType(eventType);
+        }
         Map<StoreListener<?>,Boolean> done = null;
         StoreListeners m = this;
         do {
@@ -533,6 +595,35 @@ public class StoreListeners implements Localized {
     }
 
     /**
+     * Returns the exception to throw for an event type which is not in the 
set of permitted types.
+     */
+    private IllegalArgumentException illegalEventType(final Class<?> type) {
+        return new IllegalArgumentException(Resources.forLocale(getLocale())
+                .getString(Resources.Keys.IllegalEventType_1, type));
+    }
+
+    /**
+     * Verifies if a listener interested in the specified type of events could 
receive some events
+     * from this {@code StoreListeners}.
+     *
+     * @param  eventType  type of events to listen.
+     * @return whether a listener could receive events of the specified type.
+     *
+     * @see #setUsableEventTypes(Class...)
+     */
+    private static boolean isPossibleEvent(final Set<Class<? extends 
StoreEvent>> permittedEventTypes, final Class<?> eventType) {
+        if (permittedEventTypes == null) {
+            return true;
+        }
+        for (final Class<? extends StoreEvent> type : permittedEventTypes) {
+            if (eventType.isAssignableFrom(type)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
      * Registers a listener to notify when the specified kind of event occurs.
      * Registering a listener for a given {@code eventType} also register the 
listener for all event sub-types.
      * The same listener can be registered many times, but its {@link 
StoreListener#eventOccured(StoreEvent)}
@@ -555,18 +646,20 @@ public class StoreListeners implements Localized {
     public synchronized <T extends StoreEvent> void addListener(final Class<T> 
eventType, final StoreListener<? super T> listener) {
         ArgumentChecks.ensureNonNull("listener",  listener);
         ArgumentChecks.ensureNonNull("eventType", eventType);
-        ForType<T> ce = null;
-        for (ForType<?> e = listeners; e != null; e = e.next) {
-            if (e.type.equals(eventType)) {
-                ce = (ForType<T>) e;
-                break;
+        if (isPossibleEvent(permittedEventTypes, eventType)) {
+            ForType<T> ce = null;
+            for (ForType<?> e = listeners; e != null; e = e.next) {
+                if (e.type.equals(eventType)) {
+                    ce = (ForType<T>) e;
+                    break;
+                }
             }
+            if (ce == null) {
+                ce = new ForType<>(eventType, listeners);
+                listeners = ce;
+            }
+            ce.add(listener);
         }
-        if (ce == null) {
-            ce = new ForType<>(eventType, listeners);
-            listeners = ce;
-        }
-        ce.add(listener);
     }
 
     /**
@@ -604,7 +697,10 @@ public class StoreListeners implements Localized {
     }
 
     /**
-     * Returns {@code true} if this object or its parent contains at least one 
listener for the given type of event.
+     * Returns {@code true} if at least one listener is registered for the 
given type or a super-type.
+     * This method may unconditionally return {@code false} if the given type 
of event is never fired
+     * by this {@code StoreListeners}, because calls to {@code 
addListener(eventType, …)} are free to
+     * ignore the listeners for those types.
      *
      * @param  eventType  the type of event for which to check listener 
presence.
      * @return {@code true} if this object contains at least one listener for 
given event type, {@code false} otherwise.
@@ -618,11 +714,56 @@ public class StoreListeners implements Localized {
                     if (e.hasListeners()) {
                         return true;
                     }
-                    break;
                 }
             }
             m = m.parent;
         } while (m != null);
         return false;
     }
+
+    /**
+     * Notifies this {@code StoreListeners} that only events of the specified 
types will be fired.
+     * With this knowledge, {@code StoreListeners} will not retain any 
reference to listeners that
+     * are not listening to events of those types or to events of a parent 
type.
+     * This restriction allows the garbage collector to dispose unnecessary 
listeners.
+     *
+     * <div class="note"><b>Example:</b>
+     * an application may unconditionally register listeners for being 
notified about additions of new data.
+     * If a {@link DataStore} implementation is read-only, then such listeners 
would never receive any notification.
+     * As a slight optimization, the {@code DataStore} constructor can invoke 
this method for example as below:
+     *
+     * {@preformat java
+     *     listeners.setUsableEventTypes(WarningEvent.class);
+     * }
+     *
+     * With this configuration, calls to {@code 
addListener(DataAddedEvent.class, foo)} will be ignored,
+     * thus avoiding this instance to retain a never-used reference to the 
{@code foo} listener.
+     * </div>
+     *
+     * The argument shall enumerate all permitted types, including sub-types 
(they are not automatically accepted).
+     * All types given in argument must be types that were accepted before the 
invocation of this method.
+     * In other words, this method can be invoked for reducing the set of 
permitted types but not for expanding it.
+     *
+     * @param  permitted  type of events that are permitted. Permitted 
sub-types shall be explicitly enumerated as well.
+     * @throws IllegalArgumentException if one of the given types was not 
permitted before invocation of this method.
+     *
+     * @see #useWarningEventsOnly()
+     *
+     * @since 1.2
+     */
+    @SuppressWarnings("unchecked")
+    public synchronized void setUsableEventTypes(final Class<?>... permitted) {
+        ArgumentChecks.ensureNonEmpty("permitted", permitted);
+        final Set<Class<? extends StoreEvent>> current = permittedEventTypes;
+        final Set<Class<? extends StoreEvent>> types = new 
HashSet<>(Containers.hashMapCapacity(permitted.length));
+        for (final Class<?> type : permitted) {
+            if (current != null ? current.contains(type) : 
StoreEvent.class.isAssignableFrom(type)) {
+                types.add((Class<? extends StoreEvent>) type);
+            } else {
+                throw illegalEventType(type);
+            }
+        }
+        permittedEventTypes = WARNING_EVENT_TYPE.equals(types) ? 
WARNING_EVENT_TYPE : CollectionsExt.compact(types);
+        ForType.removeUnreachables(listeners, types);
+    }
 }
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 7d1fc6d..f4af553 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
@@ -30,7 +30,7 @@ import static org.junit.Assert.*;
  * Tests the {@link StoreListeners} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.2
  * @since   1.0
  * @module
  */
@@ -81,6 +81,28 @@ public final strictfp class StoreListenersTest extends 
TestCase implements Store
     }
 
     /**
+     * Verifies that {@link StoreListeners#addListener(Class, StoreListener)} 
ignore the given listener
+     * when the specified type of event is never fired.
+     */
+    @Test
+    public void testListenerFiltering() {
+        final StoreListeners listeners = store.listeners();
+        listeners.addListener(StoreEvent.class, (event) -> {});
+        listeners.addListener(WarningEvent.class, this);
+        assertTrue(listeners.hasListeners(StoreEvent.class));
+        assertTrue(listeners.hasListeners(WarningEvent.class));
+        listeners.setUsableEventTypes(StoreEvent.class, WarningEvent.class);
+        assertTrue(listeners.hasListeners(StoreEvent.class));
+        assertTrue(listeners.hasListeners(WarningEvent.class));
+        listeners.setUsableEventTypes(StoreEvent.class);
+        assertTrue (listeners.hasListeners(StoreEvent.class));
+        assertFalse(listeners.hasListeners(WarningEvent.class));
+        listeners.addListener(WarningEvent.class, this);
+        assertTrue (listeners.hasListeners(StoreEvent.class));
+        assertFalse(listeners.hasListeners(WarningEvent.class));
+    }
+
+    /**
      * Tests {@link StoreListeners#warning(String, Exception)} with a 
registered listener.
      */
     @Test
diff --git 
a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java
 
b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java
index 5957a0c..451059e 100644
--- 
a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java
+++ 
b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java
@@ -102,6 +102,7 @@ public final class Store extends StaxDataStore implements 
FeatureSet {
         } catch (FactoryException e) {
             throw new DataStoreException(e);
         }
+        listeners.useWarningEventsOnly();
     }
 
     /**

Reply via email to