sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/03: Replace the use of WarningListeners by StoreListeners in DataStore implementations. https://issues.apache.org/jira/browse/SIS-421
Date Sat, 07 Sep 2019 16:04:31 GMT
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 237a0a028b0078cb28f4b97e72dc730300051c43
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sat Sep 7 15:03:11 2019 +0200

    Replace the use of WarningListeners by StoreListeners in DataStore implementations.
    https://issues.apache.org/jira/browse/SIS-421
---
 .../apache/sis/util/logging/WarningListeners.java  |   1 -
 .../sis/util/logging/EmptyWarningListeners.java    |  71 --------------
 .../sis/util/logging/WarningListenersTest.java     |   1 +
 .../storage/earthobservation/LandsatReader.java    |   7 +-
 .../sis/storage/earthobservation/LandsatStore.java |  24 ++---
 .../earthobservation/LandsatReaderTest.java        |   7 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  32 +++---
 .../sis/storage/geotiff/ImageFileDirectory.java    |   2 +-
 .../org/apache/sis/internal/netcdf/Decoder.java    |   7 +-
 .../sis/internal/netcdf/DiscreteSampling.java      |   7 +-
 .../apache/sis/internal/netcdf/NamedElement.java   |   4 +-
 .../sis/internal/netcdf/impl/ChannelDecoder.java   |   9 +-
 .../sis/internal/netcdf/ucar/DecoderWrapper.java   |   7 +-
 .../sis/internal/netcdf/ucar/FeaturesWrapper.java  |   5 +-
 .../sis/internal/netcdf/ucar/LogAdapter.java       |   9 +-
 .../apache/sis/storage/netcdf/MetadataReader.java  |   7 +-
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |  24 ++---
 .../sis/storage/netcdf/NetcdfStoreProvider.java    |   8 +-
 .../org/apache/sis/internal/netcdf/TestCase.java   |  11 +--
 .../internal/netcdf/impl/ChannelDecoderTest.java   |   3 +-
 .../storage/netcdf/NetcdfStoreProviderTest.java    |   9 +-
 .../apache/sis/internal/sql/feature/Analyzer.java  |   7 +-
 .../apache/sis/internal/sql/feature/Database.java  |   5 +-
 .../java/org/apache/sis/storage/sql/SQLStore.java  |  24 ++---
 .../sis/internal/storage/AbstractFeatureSet.java   |  22 ++---
 .../sis/internal/storage/AbstractGridResource.java |  22 +----
 .../sis/internal/storage/AbstractResource.java     |  89 +++++------------
 .../sis/internal/storage/AggregatedFeatureSet.java |  32 +-----
 .../internal/storage/ConcatenatedFeatureSet.java   |  23 +++--
 .../internal/storage/DocumentedStoreProvider.java  |   5 +-
 .../sis/internal/storage/JoinFeatureSet.java       |   9 +-
 .../sis/internal/storage/MemoryFeatureSet.java     |  11 +--
 .../apache/sis/internal/storage/URIDataStore.java  |  25 ++---
 .../apache/sis/internal/storage/folder/Store.java  |  24 ++---
 .../sis/internal/storage/io/ChannelFactory.java    |  33 +++----
 .../sis/internal/storage/query/FeatureSubset.java  |  13 +--
 .../sis/internal/storage/wkt/StoreFormat.java      |  14 ++-
 .../org/apache/sis/internal/storage/xml/Store.java |   3 +-
 .../java/org/apache/sis/storage/DataStore.java     |  82 ++++++++++++++--
 .../org/apache/sis/storage/event/StoreEvent.java   |  10 +-
 .../apache/sis/storage/event/StoreListeners.java   |  90 ++++++++++++++---
 .../internal/storage/AbstractGridResourceTest.java |   3 -
 .../java/org/apache/sis/storage/DataStoreMock.java |  40 ++++++--
 .../sis/storage/event/StoreListenersTest.java      | 109 +++++++++++++++++++++
 .../apache/sis/test/suite/StorageTestSuite.java    |   1 +
 .../internal/storage/xml/stream/StaxDataStore.java |   2 +-
 46 files changed, 483 insertions(+), 470 deletions(-)

diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/logging/WarningListeners.java b/core/sis-utility/src/main/java/org/apache/sis/util/logging/WarningListeners.java
index d7790cb..3f22af8 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/logging/WarningListeners.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/logging/WarningListeners.java
@@ -101,7 +101,6 @@ public class WarningListeners<S> implements Localized {
      *                but this is the source that the implementer wants to declare as public API.
      */
     public WarningListeners(final S source) {
-        ArgumentChecks.ensureNonNull("source", source);
         this.source = source;
     }
 
diff --git a/core/sis-utility/src/test/java/org/apache/sis/util/logging/EmptyWarningListeners.java b/core/sis-utility/src/test/java/org/apache/sis/util/logging/EmptyWarningListeners.java
deleted file mode 100644
index 7866034..0000000
--- a/core/sis-utility/src/test/java/org/apache/sis/util/logging/EmptyWarningListeners.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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.util.logging;
-
-import java.util.Locale;
-import java.util.logging.Logger;
-import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.resources.Errors;
-
-
-/**
- * A unmodifiable empty list of listeners. Calls to {@link #addWarningListener(WarningListener) addWarningListener(…)}
- * will throw {@link UnsupportedOperationException}. Since this listener list is empty, it does not need a source.
- *
- * <p>This class is used in some modules like {@code sis-netcdf}, when a JUnit test is testing some low-level
- * component where the real {@link WarningListeners} instance is not yet available.</p>
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 0.3
- *
- * @param <S>  if the listener list had a source, that would be type type of the source.
- *
- * @since 0.3
- * @module
- */
-public final strictfp class EmptyWarningListeners<S> extends WarningListeners<S> {
-    /**
-     * The locale to be returned by {@link #getLocale()}. Can be {@code null}.
-     */
-    private final Locale locale;
-
-    /**
-     * The logger to be returned by {@link #getLogger()}.
-     */
-    @SuppressWarnings("NonConstantLogger")
-    private final Logger logger;
-
-    /**
-     * Creates a new instance for the given locale and logger.
-     *
-     * @param locale  the locale to be returned by {@link #getLocale()}. Can be {@code null}.
-     * @param logger  the name of the logger to be returned by {@link #getLogger()}.
-     */
-    public EmptyWarningListeners(final Locale locale, final String logger) {
-        ArgumentChecks.ensureNonNull("logger", logger);
-        this.locale = locale;
-        this.logger = Logging.getLogger(logger);
-    }
-
-    /** @return the value given at construction time. */ @Override public Locale getLocale() {return locale;}
-    /** @return the value given at construction time. */ @Override public Logger getLogger() {return logger;}
-
-    /** Do not allow registration of warning listeners. */
-    @Override public void addWarningListener(WarningListener<? super S> listener) {
-        throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, "WarningListeners"));
-    }
-}
diff --git a/core/sis-utility/src/test/java/org/apache/sis/util/logging/WarningListenersTest.java b/core/sis-utility/src/test/java/org/apache/sis/util/logging/WarningListenersTest.java
index c2ff86f..42b7318 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/util/logging/WarningListenersTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/util/logging/WarningListenersTest.java
@@ -33,6 +33,7 @@ import static org.junit.Assert.*;
  * @since   0.3
  * @module
  */
+@Deprecated
 public final strictfp class WarningListenersTest extends TestCase implements WarningListener<String> {
     /**
      * The object to be tested. Its source will be set to the string {@code "source"}.
diff --git a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java
index cd7e610..333e6d2 100644
--- a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java
+++ b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatReader.java
@@ -51,14 +51,13 @@ import org.apache.sis.metadata.iso.content.DefaultCoverageDescription;
 import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.CommonCRS;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreReferencingException;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.Characters;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.util.iso.SimpleInternationalString;
 import org.apache.sis.internal.referencing.GeodeticObjectBuilder;
 import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
@@ -213,7 +212,7 @@ final class LandsatReader extends MetadataBuilder {
     /**
      * Where to send the warnings.
      */
-    private final WarningListeners<DataStore> listeners;
+    private final StoreListeners listeners;
 
     /**
      * Group in process of being parsed, or {@code null} if none.
@@ -294,7 +293,7 @@ final class LandsatReader extends MetadataBuilder {
      * @param  filename   an identifier of the file being read, or {@code null} if unknown.
      * @param  listeners  where to sent warnings that may occur during the parsing process.
      */
-    LandsatReader(final String filename, final WarningListeners<DataStore> listeners) {
+    LandsatReader(final String filename, final StoreListeners listeners) {
         this.filename  = filename;
         this.listeners = listeners;
         this.factories = new ReferencingFactoryContainer();
diff --git a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatStore.java b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatStore.java
index c23a061..37b8672 100644
--- a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatStore.java
+++ b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/earthobservation/LandsatStore.java
@@ -33,6 +33,7 @@ import org.apache.sis.storage.UnsupportedStorageException;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
+import org.apache.sis.storage.event.WarningEvent;
 import org.apache.sis.internal.storage.URIDataStore;
 import org.apache.sis.setup.OptionKey;
 
@@ -171,25 +172,16 @@ public class LandsatStore extends DataStore {
     }
 
     /**
-     * Ignored in current implementation, since this resource produces no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
+     * Registers a listener to notify when the specified kind of event occurs in this data store.
+     * The current implementation of this data store can emit only {@link WarningEvent}s;
+     * any listener specified for another kind of events will be ignored.
      */
     @Override
     public <T extends StoreEvent> void addListener(StoreListener<? super T> listener, Class<T> eventType) {
-    }
-
-    /**
-     * Ignored in current implementation, since this resource produces no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
-     */
-    @Override
-    public <T extends StoreEvent> void removeListener(StoreListener<? super T> listener, Class<T> eventType) {
+        // If an argument is null, we let the parent class throws (indirectly) NullArgumentException.
+        if (listener == null || eventType == null || eventType.isAssignableFrom(WarningEvent.class)) {
+            super.addListener(listener, eventType);
+        }
     }
 
     /**
diff --git a/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java b/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java
index c1c13f0..5e0567f 100644
--- a/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java
+++ b/storage/sis-earth-observation/src/test/java/org/apache/sis/storage/earthobservation/LandsatReaderTest.java
@@ -16,11 +16,11 @@
  */
 package org.apache.sis.storage.earthobservation;
 
-import java.util.Locale;
 import java.util.regex.Matcher;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import org.apache.sis.internal.storage.AbstractResource;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.acquisition.Context;
 import org.opengis.metadata.acquisition.OperationType;
@@ -34,8 +34,6 @@ import org.opengis.metadata.spatial.DimensionNameType;
 import org.opengis.util.FactoryException;
 import org.opengis.test.dataset.ContentVerifier;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.util.logging.EmptyWarningListeners;
-import org.apache.sis.internal.system.Modules;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
@@ -96,8 +94,7 @@ public class LandsatReaderTest extends TestCase {
         try (BufferedReader in = new BufferedReader(new InputStreamReader(
                 LandsatReaderTest.class.getResourceAsStream("LandsatTest.txt"), "UTF-8")))
         {
-            final LandsatReader reader = new LandsatReader("LandsatTest.txt",
-                    new EmptyWarningListeners<>(Locale.ENGLISH, Modules.EARTH_OBSERVATION));
+            final LandsatReader reader = new LandsatReader("LandsatTest.txt", new AbstractResource(null));
             reader.read(in);
             actual = reader.getMetadata();
         }
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 065dbdb..1689296 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
@@ -44,6 +44,8 @@ import org.apache.sis.storage.DataStoreClosedException;
 import org.apache.sis.storage.IllegalNameException;
 import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
+import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.storage.event.WarningEvent;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.storage.io.IOUtilities;
 import org.apache.sis.internal.storage.MetadataBuilder;
@@ -144,6 +146,13 @@ public class GeoTiffStore extends DataStore implements Aggregate {
     }
 
     /**
+     * Opens access to listeners for {@link ImageFileDirectory}.
+     */
+    final StoreListeners listeners() {
+        return listeners;
+    }
+
+    /**
      * Returns the parameters used to open this GeoTIFF data store.
      * If non-null, the parameters are described by {@link GeoTiffStoreProvider#getOpenParameters()} and contains at
      * least a parameter named {@value org.apache.sis.storage.DataStoreProvider#LOCATION} with a {@link URI} value.
@@ -333,25 +342,16 @@ public class GeoTiffStore extends DataStore implements Aggregate {
     }
 
     /**
-     * Ignored in current implementation, since this resource produces no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
+     * Registers a listener to notify when the specified kind of event occurs in this data store.
+     * The current implementation of this data store can emit only {@link WarningEvent}s;
+     * any listener specified for another kind of events will be ignored.
      */
     @Override
     public <T extends StoreEvent> void addListener(StoreListener<? super T> listener, Class<T> eventType) {
-    }
-
-    /**
-     * Ignored in current implementation, since this resource produces no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
-     */
-    @Override
-    public <T extends StoreEvent> void removeListener(StoreListener<? super T> listener, Class<T> eventType) {
+        // If an argument is null, we let the parent class throws (indirectly) NullArgumentException.
+        if (listener == null || eventType == null || eventType.isAssignableFrom(WarningEvent.class)) {
+            super.addListener(listener, eventType);
+        }
     }
 
     /**
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index 94b50ae..4e07595 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -364,7 +364,7 @@ final class ImageFileDirectory extends AbstractGridResource {
      * @param index   the image index as a sequence number starting with 0 for the first image.
      */
     ImageFileDirectory(final Reader reader, final int index) {
-        super(reader.owner);
+        super(reader.owner.listeners());
         this.reader = reader;
         identifier = reader.nameFactory.createLocalName(reader.owner.identifier, String.valueOf(index + 1));
     }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
index 63c55ca..95e3046 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
@@ -35,13 +35,12 @@ import org.opengis.referencing.datum.Datum;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.MathTransform;
 import org.apache.sis.setup.GeometryLibrary;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.logging.PerformanceLevel;
-import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.internal.util.StandardDateFormat;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.system.Modules;
@@ -136,7 +135,7 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
     /**
      * Where to send the warnings.
      */
-    public final WarningListeners<DataStore> listeners;
+    public final StoreListeners listeners;
 
     /**
      * Sets to {@code true} for canceling a reading process.
@@ -150,7 +149,7 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
      * @param  geomlib    the library for geometric objects, or {@code null} for the default.
      * @param  listeners  where to send the warnings.
      */
-    protected Decoder(final GeometryLibrary geomlib, final WarningListeners<DataStore> listeners) {
+    protected Decoder(final GeometryLibrary geomlib, final StoreListeners listeners) {
         Objects.requireNonNull(listeners);
         this.geomlib      = geomlib;
         this.listeners    = listeners;
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DiscreteSampling.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DiscreteSampling.java
index 1425b94..0e55aee 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DiscreteSampling.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DiscreteSampling.java
@@ -16,11 +16,10 @@
  */
 package org.apache.sis.internal.netcdf;
 
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.internal.feature.Geometries;
 import org.apache.sis.internal.storage.AbstractFeatureSet;
-import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.resources.Errors;
 
 
@@ -48,7 +47,7 @@ public abstract class DiscreteSampling extends AbstractFeatureSet {
      * @param  listeners  the set of registered warning listeners for the data store.
      * @throws IllegalArgumentException if the given library is non-null but not available.
      */
-    protected DiscreteSampling(final GeometryLibrary library, final WarningListeners<DataStore> listeners) {
+    protected DiscreteSampling(final GeometryLibrary library, final StoreListeners listeners) {
         super(listeners);
         factory = Geometries.implementation(library);
     }
@@ -59,6 +58,6 @@ public abstract class DiscreteSampling extends AbstractFeatureSet {
      * @return default error message to use in exceptions.
      */
     protected final String canNotReadFile() {
-        return Errors.getResources(getLocale()).getString(Errors.Keys.CanNotRead_1, getStoreName());
+        return Errors.getResources(getLocale()).getString(Errors.Keys.CanNotRead_1, getSourceName());
     }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/NamedElement.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/NamedElement.java
index b2a1ca3..9ac2622 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/NamedElement.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/NamedElement.java
@@ -21,9 +21,9 @@ import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import org.apache.sis.util.Characters;
 import org.apache.sis.util.CharSequences;
-import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.util.Strings;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.resources.IndexedResourceBundle;
 
 
@@ -101,7 +101,7 @@ public abstract class NamedElement {
      * @param  key        one or {@link Resources.Keys} constants.
      * @param  arguments  values to be formatted in the {@link java.text.MessageFormat} pattern.
      */
-    static void warning(final WarningListeners<?> listeners, final Class<?> caller, final String method,
+    static void warning(final StoreListeners listeners, final Class<?> caller, final String method,
             final Exception exception, IndexedResourceBundle resources, final short key, final Object... arguments)
     {
         if (resources == null) {
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java
index d3551e9..32f876d 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java
@@ -54,13 +54,12 @@ import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.internal.util.StandardDateFormat;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.measure.Units;
 import org.apache.sis.math.Vector;
@@ -229,7 +228,7 @@ public final class ChannelDecoder extends Decoder {
      * @throws ArithmeticException if a variable is too large.
      */
     public ChannelDecoder(final ChannelDataInput input, final Charset encoding, final GeometryLibrary geomlib,
-            final WarningListeners<DataStore> listeners) throws IOException, DataStoreException
+            final StoreListeners listeners) throws IOException, DataStoreException
     {
         super(geomlib, listeners);
         this.input = input;
@@ -336,7 +335,7 @@ public final class ChannelDecoder extends Decoder {
     }
 
     /**
-     * Returns the localized error resource bundle for the locale given by {@link WarningListeners#getLocale()}.
+     * Returns the localized error resource bundle for the locale given by {@link StoreListeners#getLocale()}.
      *
      * @return the localized error resource bundle.
      */
@@ -345,7 +344,7 @@ public final class ChannelDecoder extends Decoder {
     }
 
     /**
-     * Returns the netCDF-specific resource bundle for the locale given by {@link WarningListeners#getLocale()}.
+     * Returns the netCDF-specific resource bundle for the locale given by {@link StoreListeners#getLocale()}.
      *
      * @return the localized error resource bundle.
      */
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java
index 69e52a5..419af42 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java
@@ -40,7 +40,6 @@ import ucar.nc2.ft.FeatureDatasetPoint;
 import ucar.nc2.ft.FeatureDatasetFactoryManager;
 import ucar.nc2.ft.FeatureCollection;
 import org.apache.sis.util.ArraysExt;
-import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.internal.netcdf.Convention;
 import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.netcdf.Variable;
@@ -48,8 +47,8 @@ import org.apache.sis.internal.netcdf.Node;
 import org.apache.sis.internal.netcdf.Grid;
 import org.apache.sis.internal.netcdf.DiscreteSampling;
 import org.apache.sis.setup.GeometryLibrary;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.event.StoreListeners;
 
 
 /**
@@ -108,7 +107,7 @@ public final class DecoderWrapper extends Decoder implements CancelTask {
      * @param file       the netCDF file from which to read data.
      * @param listeners  where to send the warnings.
      */
-    public DecoderWrapper(final NetcdfFile file, final GeometryLibrary geomlib, final WarningListeners<DataStore> listeners) {
+    public DecoderWrapper(final NetcdfFile file, final GeometryLibrary geomlib, final StoreListeners listeners) {
         super(geomlib, listeners);
         this.file = file;
         groups = new Group[1];
@@ -124,7 +123,7 @@ public final class DecoderWrapper extends Decoder implements CancelTask {
      * @throws IOException if an error occurred while opening the netCDF file.
      */
     @SuppressWarnings("ThisEscapedInObjectConstruction")
-    public DecoderWrapper(final String filename, final GeometryLibrary geomlib, final WarningListeners<DataStore> listeners)
+    public DecoderWrapper(final String filename, final GeometryLibrary geomlib, final StoreListeners listeners)
             throws IOException
     {
         super(geomlib, listeners);
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/FeaturesWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/FeaturesWrapper.java
index 4072b5c..8d8b3cb 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/FeaturesWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/FeaturesWrapper.java
@@ -17,10 +17,9 @@
 package org.apache.sis.internal.netcdf.ucar;
 
 import java.util.stream.Stream;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.internal.netcdf.DiscreteSampling;
-import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.storage.event.StoreListeners;
 import ucar.nc2.ft.FeatureCollection;
 
 // Branch-dependent imports
@@ -49,7 +48,7 @@ final class FeaturesWrapper extends DiscreteSampling {
      * @param  listeners  the set of registered warning listeners for the data store.
      * @throws IllegalArgumentException if the given library is non-null but not available.
      */
-    FeaturesWrapper(final FeatureCollection features, final GeometryLibrary factory, final WarningListeners<DataStore> listeners) {
+    FeaturesWrapper(final FeatureCollection features, final GeometryLibrary factory, final StoreListeners listeners) {
         super(factory, listeners);
         this.features = features;
     }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/LogAdapter.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/LogAdapter.java
index ef3ebd9..97c0d7b 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/LogAdapter.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/LogAdapter.java
@@ -16,15 +16,14 @@
  */
 package org.apache.sis.internal.netcdf.ucar;
 
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.util.CharSequences;
-import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.storage.event.StoreListeners;
 
 
 /**
  * Forwards netCDF logging to the Apache SIS warning listeners.
  * NetCDF sends message to a user-specified {@link java.util.Formatter} with one message per line.
- * This class intercepts the characters and send them to the {@link WarningListeners} every time
+ * This class intercepts the characters and send them to the {@link StoreListeners} every time
  * that a complete line has been received.
  *
  * @author  Martin Desruisseaux (Geomatys)
@@ -41,12 +40,12 @@ final class LogAdapter implements Appendable {
     /**
      * Where to sends the warning messages.
      */
-    private final WarningListeners<DataStore> listeners;
+    private final StoreListeners listeners;
 
     /**
      * Creates a new adapter which will send lines to the given listeners.
      */
-    LogAdapter(final WarningListeners<DataStore> listeners) {
+    LogAdapter(final StoreListeners listeners) {
         this.listeners = listeners;
     }
 
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
index 5532b33..8d1a4c7 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
@@ -50,9 +50,8 @@ import org.opengis.referencing.crs.VerticalCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 
 import org.apache.sis.util.iso.Types;
-import org.apache.sis.util.logging.WarningListeners;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.metadata.iso.citation.*;
 import org.apache.sis.metadata.iso.identification.*;
@@ -203,12 +202,12 @@ final class MetadataReader extends MetadataBuilder {
 
     /**
      * Logs a warning using the localized error resource bundle for the locale given by
-     * {@link WarningListeners#getLocale()}.
+     * {@link StoreListeners#getLocale()}.
      *
      * @param  key  one of {@link Errors.Keys} values.
      */
     private void warning(final short key, final Object p1, final Object p2, final Exception e) {
-        final WarningListeners<DataStore> listeners = decoder.listeners;
+        final StoreListeners listeners = decoder.listeners;
         listeners.warning(Errors.getResources(listeners.getLocale()).getString(key, p1, p2), e);
     }
 
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 0e33f37..9eed48a 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
@@ -41,6 +41,7 @@ import org.apache.sis.setup.OptionKey;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
+import org.apache.sis.storage.event.WarningEvent;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.Version;
 import ucar.nc2.constants.ACDD;
@@ -215,25 +216,16 @@ public class NetcdfStore extends DataStore implements Aggregate {
     }
 
     /**
-     * Ignored in current implementation, since this resource produces no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
+     * Registers a listener to notify when the specified kind of event occurs in this data store.
+     * The current implementation of this data store can emit only {@link WarningEvent}s;
+     * any listener specified for another kind of events will be ignored.
      */
     @Override
     public <T extends StoreEvent> void addListener(StoreListener<? super T> listener, Class<T> eventType) {
-    }
-
-    /**
-     * Ignored in current implementation, since this resource produces no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
-     */
-    @Override
-    public <T extends StoreEvent> void removeListener(StoreListener<? super T> listener, Class<T> eventType) {
+        // If an argument is null, we let the parent class throws (indirectly) NullArgumentException.
+        if (listener == null || eventType == null || eventType.isAssignableFrom(WarningEvent.class)) {
+            super.addListener(listener, eventType);
+        }
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
index 70bd8d0..daee428 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
@@ -44,7 +44,7 @@ import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.ProbeResult;
-import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.Version;
 
@@ -265,7 +265,7 @@ public class NetcdfStoreProvider extends DataStoreProvider {
      * @throws IOException if an error occurred while opening the netCDF file.
      * @throws DataStoreException if a logical error (other than I/O) occurred.
      */
-    static Decoder decoder(final WarningListeners<DataStore> listeners, final StorageConnector connector)
+    static Decoder decoder(final StoreListeners listeners, final StorageConnector connector)
             throws IOException, DataStoreException
     {
         final GeometryLibrary geomlib = connector.getOption(OptionKey.GEOMETRY_LIBRARY);
@@ -311,7 +311,7 @@ public class NetcdfStoreProvider extends DataStoreProvider {
      * @throws DataStoreException if a logical error (other than I/O) occurred.
      */
     private static Decoder createByReflection(final Object input, final boolean isUCAR,
-            final GeometryLibrary geomlib, final WarningListeners<DataStore> listeners)
+            final GeometryLibrary geomlib, final StoreListeners listeners)
             throws IOException, DataStoreException
     {
         ensureInitialized(true);
@@ -371,7 +371,7 @@ public class NetcdfStoreProvider extends DataStoreProvider {
                          */
                         final Class<? extends Decoder> wrapper =
                                 Class.forName("org.apache.sis.internal.netcdf.ucar.DecoderWrapper").asSubclass(Decoder.class);
-                        final Class<?>[] parameterTypes = new Class<?>[] {netcdfFileClass, GeometryLibrary.class, WarningListeners.class};
+                        final Class<?>[] parameterTypes = new Class<?>[] {netcdfFileClass, GeometryLibrary.class, StoreListeners.class};
                         createFromUCAR = wrapper.getConstructor(parameterTypes);
                         parameterTypes[0] = String.class;
                         createFromPath = wrapper.getConstructor(parameterTypes);
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java
index 157922d..fc8365a 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java
@@ -22,11 +22,9 @@ import java.util.EnumMap;
 import java.util.Iterator;
 import java.io.IOException;
 import java.lang.reflect.UndeclaredThrowableException;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.util.logging.EmptyWarningListeners;
+import org.apache.sis.internal.storage.AbstractResource;
 import org.apache.sis.internal.netcdf.ucar.DecoderWrapper;
-import org.apache.sis.internal.system.Modules;
 import org.apache.sis.setup.GeometryLibrary;
 import org.opengis.test.dataset.TestData;
 import ucar.nc2.dataset.NetcdfDataset;
@@ -49,11 +47,6 @@ import static org.junit.Assert.*;
  */
 public abstract strictfp class TestCase extends org.apache.sis.test.TestCase {
     /**
-     * A dummy list of listeners which can be given to the {@link Decoder} constructor.
-     */
-    protected static EmptyWarningListeners<DataStore> LISTENERS = new EmptyWarningListeners<>(null, Modules.NETCDF);
-
-    /**
      * The {@code searchPath} argument value to be given to the {@link Decoder#setSearchPath(String[])}
      * method when the decoder shall search only in global attributes.
      */
@@ -117,7 +110,7 @@ public abstract strictfp class TestCase extends org.apache.sis.test.TestCase {
      * @throws DataStoreException if a logical error occurred.
      */
     protected Decoder createDecoder(final TestData file) throws IOException, DataStoreException {
-        return new DecoderWrapper(new NetcdfDataset(createUCAR(file)), GeometryLibrary.JAVA2D, LISTENERS);
+        return new DecoderWrapper(new NetcdfDataset(createUCAR(file)), GeometryLibrary.JAVA2D, new AbstractResource(null));
     }
 
     /**
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/ChannelDecoderTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/ChannelDecoderTest.java
index 4e3a61c..a18cf5e 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/ChannelDecoderTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/ChannelDecoderTest.java
@@ -22,6 +22,7 @@ import java.nio.ByteBuffer;
 import java.nio.channels.Channels;
 import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.netcdf.DecoderTest;
+import org.apache.sis.internal.storage.AbstractResource;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.setup.GeometryLibrary;
@@ -70,6 +71,6 @@ public final strictfp class ChannelDecoderTest extends DecoderTest {
         final InputStream in = file.open();
         final ChannelDataInput input = new ChannelDataInput(file.name(),
                 Channels.newChannel(in), ByteBuffer.allocate(4096), false);
-        return new ChannelDecoder(input, null, GeometryLibrary.JAVA2D, LISTENERS);
+        return new ChannelDecoder(input, null, GeometryLibrary.JAVA2D, new AbstractResource(null));
     }
 }
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/NetcdfStoreProviderTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/NetcdfStoreProviderTest.java
index a798cb5..8acae77 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/NetcdfStoreProviderTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/storage/netcdf/NetcdfStoreProviderTest.java
@@ -23,6 +23,7 @@ import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.netcdf.ucar.DecoderWrapper;
 import org.apache.sis.internal.netcdf.impl.ChannelDecoder;
 import org.apache.sis.internal.netcdf.impl.ChannelDecoderTest;
+import org.apache.sis.internal.storage.AbstractResource;
 import org.apache.sis.storage.ProbeResult;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.DataStoreException;
@@ -82,7 +83,7 @@ public final strictfp class NetcdfStoreProviderTest extends TestCase {
     }
 
     /**
-     * Tests {@link NetcdfStoreProvider#decoder(WarningListeners, StorageConnector)} for an input stream which
+     * Tests {@link NetcdfStoreProvider#decoder(StoreListeners, StorageConnector)} for an input stream which
      * shall be recognized as a classic netCDF file. The provider shall instantiate a {@link ChannelDecoder}.
      *
      * @throws IOException if an error occurred while opening the netCDF file.
@@ -91,13 +92,13 @@ public final strictfp class NetcdfStoreProviderTest extends TestCase {
     @Test
     public void testDecoderFromStream() throws IOException, DataStoreException {
         final StorageConnector c = new StorageConnector(TestData.NETCDF_2D_GEOGRAPHIC.open());
-        try (Decoder decoder = NetcdfStoreProvider.decoder(LISTENERS, c)) {
+        try (Decoder decoder = NetcdfStoreProvider.decoder(new AbstractResource(null), c)) {
             assertInstanceOf("decoder", ChannelDecoder.class, decoder);
         }
     }
 
     /**
-     * Tests {@link NetcdfStoreProvider#decoder(WarningListeners, StorageConnector)} for a UCAR
+     * Tests {@link NetcdfStoreProvider#decoder(StoreListeners, StorageConnector)} for a UCAR
      * {@link NetcdfFile} object. The provider shall instantiate a {@link DecoderWrapper}.
      *
      * @throws IOException if an error occurred while opening the netCDF file.
@@ -106,7 +107,7 @@ public final strictfp class NetcdfStoreProviderTest extends TestCase {
     @Test
     public void testDecoderFromUCAR() throws IOException, DataStoreException {
         final StorageConnector c = new StorageConnector(createUCAR(TestData.NETCDF_2D_GEOGRAPHIC));
-        try (Decoder decoder = NetcdfStoreProvider.decoder(LISTENERS, c)) {
+        try (Decoder decoder = NetcdfStoreProvider.decoder(new AbstractResource(null), c)) {
             assertInstanceOf("decoder", DecoderWrapper.class, decoder);
         }
     }
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
index a24d969..bb973e8 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Analyzer.java
@@ -38,10 +38,9 @@ import org.apache.sis.internal.metadata.sql.Dialect;
 import org.apache.sis.internal.metadata.sql.SQLUtilities;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.storage.sql.SQLStore;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.InternalDataStoreException;
-import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.resources.ResourceInternationalString;
 
 
@@ -116,7 +115,7 @@ final class Analyzer {
     /**
      * Where to send warnings after we finished to collect them, or when reading the feature instances.
      */
-    final WarningListeners<DataStore> listeners;
+    final StoreListeners listeners;
 
     /**
      * The locale for warning messages.
@@ -142,7 +141,7 @@ final class Analyzer {
      * @param  listeners  Value of {@code SQLStore.listeners}.
      * @param  locale     Value of {@code SQLStore.getLocale()}.
      */
-    Analyzer(final DataSource source, final DatabaseMetaData metadata, final WarningListeners<DataStore> listeners,
+    Analyzer(final DataSource source, final DatabaseMetaData metadata, final StoreListeners listeners,
              final Locale locale) throws SQLException
     {
         this.source      = source;
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
index 3f04da4..66ba910 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Database.java
@@ -31,13 +31,12 @@ import org.apache.sis.internal.metadata.sql.Reflection;
 import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.storage.sql.SQLStore;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.FeatureNaming;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.IllegalNameException;
-import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.Debug;
 
@@ -109,7 +108,7 @@ public final class Database {
      * @throws DataStoreException if a logical error occurred while analyzing the database structure.
      */
     public Database(final SQLStore store, final Connection connection, final DataSource source,
-            final GenericName[] tableNames, final WarningListeners<DataStore> listeners)
+            final GenericName[] tableNames, final StoreListeners listeners)
             throws SQLException, DataStoreException
     {
         final Analyzer analyzer = new Analyzer(source, connection.getMetaData(), listeners, store.getLocale());
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 0d83fd4..6ef63cf 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
@@ -34,6 +34,7 @@ import org.apache.sis.storage.IllegalNameException;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
+import org.apache.sis.storage.event.WarningEvent;
 import org.apache.sis.internal.sql.feature.Database;
 import org.apache.sis.internal.sql.feature.Resources;
 import org.apache.sis.internal.storage.MetadataBuilder;
@@ -256,25 +257,16 @@ public class SQLStore extends DataStore implements Aggregate {
     }
 
     /**
-     * Ignored in current implementation, since this resource produces no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
+     * Registers a listener to notify when the specified kind of event occurs in this data store.
+     * The current implementation of this data store can emit only {@link WarningEvent}s;
+     * any listener specified for another kind of events will be ignored.
      */
     @Override
     public <T extends StoreEvent> void addListener(StoreListener<? super T> listener, Class<T> eventType) {
-    }
-
-    /**
-     * Ignored in current implementation, since this resource produces no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
-     */
-    @Override
-    public <T extends StoreEvent> void removeListener(StoreListener<? super T> listener, Class<T> eventType) {
+        // If an argument is null, we let the parent class throws (indirectly) NullArgumentException.
+        if (listener == null || eventType == null || eventType.isAssignableFrom(WarningEvent.class)) {
+            super.addListener(listener, eventType);
+        }
     }
 
     /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
index d3da090..e07f19b 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
@@ -18,11 +18,11 @@ package org.apache.sis.internal.storage;
 
 import java.util.Optional;
 import java.util.OptionalLong;
+import org.opengis.util.GenericName;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.util.logging.WarningListeners;
-import org.opengis.util.GenericName;
+import org.apache.sis.storage.event.StoreListeners;
 
 // Branch-dependent imports
 import org.opengis.feature.FeatureType;
@@ -40,7 +40,7 @@ import org.opengis.feature.FeatureType;
  *   <li>{@link #features(boolean parallel)} (mandatory)</li>
  * </ul>
  *
- * {@section Thread safety}
+ * <div class="section">Thread safety</div>
  * Default methods of this abstract class are thread-safe.
  * Synchronization, when needed, uses {@code this} lock.
  *
@@ -54,20 +54,10 @@ public abstract class AbstractFeatureSet extends AbstractResource implements Fea
     /**
      * Creates a new resource.
      *
-     * @param listeners  the set of registered warning listeners for the data store, or {@code null} if none.
-     */
-    protected AbstractFeatureSet(final WarningListeners<DataStore> listeners) {
-        super(listeners);
-    }
-
-    /**
-     * Creates a new feature set with the same warning listeners than the given resource,
-     * or with {@code null} listeners if they are unknown.
-     *
-     * @param resource  the resources from which to get the listeners, or {@code null} if none.
+     * @param  parent  listeners of the parent resource, or {@code null} if none.
      */
-    protected AbstractFeatureSet(final FeatureSet resource) {
-        super(resource);
+    protected AbstractFeatureSet(final StoreListeners parent) {
+        super(parent);
     }
 
     /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
index 944bc6f..0cd9832 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
@@ -19,18 +19,16 @@ package org.apache.sis.internal.storage;
 import java.util.Arrays;
 import java.util.Optional;
 import org.opengis.geometry.Envelope;
-import org.apache.sis.storage.DataStore;
+import org.opengis.metadata.spatial.DimensionNameType;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.math.MathFunctions;
-import org.apache.sis.storage.Resource;
-import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
-import org.opengis.metadata.spatial.DimensionNameType;
 
 
 /**
@@ -45,20 +43,10 @@ public abstract class AbstractGridResource extends AbstractResource implements G
     /**
      * Creates a new resource.
      *
-     * @param listeners  the set of registered warning listeners for the data store, or {@code null} if none.
-     */
-    protected AbstractGridResource(final WarningListeners<DataStore> listeners) {
-        super(listeners);
-    }
-
-    /**
-     * Creates a new resource with the same warning listeners than the given resource,
-     * or {@code null} if the listeners are unknown.
-     *
-     * @param resource  the resources from which to get the listeners, or {@code null} if none.
+     * @param  parent  listeners of the parent resource, or {@code null} if none.
      */
-    protected AbstractGridResource(final Resource resource) {
-        super(resource);
+    protected AbstractGridResource(final StoreListeners parent) {
+        super(parent);
     }
 
     /**
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 d3ca538..e4fbc2d 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
@@ -16,18 +16,17 @@
  */
 package org.apache.sis.internal.storage;
 
-import java.util.Locale;
 import java.util.Optional;
+import org.opengis.util.GenericName;
 import org.opengis.metadata.Metadata;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.util.Localized;
-import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.storage.Resource;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
+import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.storage.event.WarningEvent;
 
 
 /**
@@ -40,7 +39,10 @@ import org.apache.sis.storage.event.StoreListener;
  *   <li>{@link #createMetadata(MetadataBuilder)} (optional)</li>
  * </ul>
  *
- * {@section Thread safety}
+ * This class extends {@link StoreListeners} for convenience reasons.
+ * This implementation details may change in any future SIS version.
+ *
+ * <div class="section">Thread safety</div>
  * Default methods of this abstract class are thread-safe.
  * Synchronization, when needed, uses {@code this} lock.
  *
@@ -49,7 +51,7 @@ import org.apache.sis.storage.event.StoreListener;
  * @since   0.8
  * @module
  */
-public abstract class AbstractResource implements Resource, Localized {
+public class AbstractResource extends StoreListeners implements Resource {
     /**
      * A description of this resource as an unmodifiable metadata, or {@code null} if not yet computed.
      * If non-null, this metadata shall contain at least the resource {@linkplain #getIdentifier() identifier}.
@@ -58,50 +60,22 @@ public abstract class AbstractResource implements Resource, Localized {
     private Metadata metadata;
 
     /**
-     * The set of registered warning listeners for the data store, or {@code null} if none.
-     */
-    private final WarningListeners<DataStore> listeners;
-
-    /**
      * Creates a new resource.
      *
-     * @param listeners  the set of registered warning listeners for the data store, or {@code null} if none.
+     * @param  parent  listeners of the parent resource, or {@code null} if none.
      */
-    protected AbstractResource(final WarningListeners<DataStore> listeners) {
-        this.listeners = listeners;
+    public AbstractResource(final StoreListeners parent) {
+        super(parent, null);
     }
 
     /**
-     * Creates a new resource with the same warning listeners than the given resource,
-     * or with {@code null} listeners if they are unknown.
-     *
-     * @param resource  the resources from which to get the listeners, or {@code null} if none.
-     */
-    protected AbstractResource(final Resource resource) {
-        listeners = (resource instanceof AbstractResource) ? ((AbstractResource) resource).listeners : null;
-    }
-
-    /**
-     * Returns the locale for error messages or warnings.
-     * Returns {@code null} if no locale is explicitly defined.
-     *
-     * @return the locale, or {@code null} if not explicitly defined.
+     * Returns the resource persistent identifier if available.
+     * The default implementation returns an empty value.
+     * Subclasses are strongly encouraged to override if they can provide a value.
      */
     @Override
-    public final Locale getLocale() {
-        return (listeners != null) ? listeners.getLocale() : null;
-    }
-
-    /**
-     * Returns the display name of the data store, or {@code null} if none.
-     * This is a convenience method for formatting error messages in subclasses.
-     *
-     * @return the data store display name, or {@code null}.
-     *
-     * @see DataStore#getDisplayName()
-     */
-    protected final String getStoreName() {
-        return (listeners != null) ? listeners.getSource().getDisplayName() : null;
+    public Optional<GenericName> getIdentifier() throws DataStoreException {
+        return Optional.empty();
     }
 
     /**
@@ -153,15 +127,6 @@ public abstract class AbstractResource implements Resource, Localized {
     }
 
     /**
-     * Invoked when a non-fatal exception occurred.
-     *
-     * @param  e  the non-fatal exception to report.
-     */
-    protected final void warning(final Exception e) {
-        listeners.warning(null, e);
-    }
-
-    /**
      * Clears any cache in this resource, forcing the data to be recomputed when needed again.
      * This method should be invoked if the data in underlying data store changed.
      */
@@ -170,24 +135,14 @@ public abstract class AbstractResource implements Resource, Localized {
     }
 
     /**
-     * Ignored in current implementation, on the assumption that most resources produce no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
+     * Registers only listeners for {@link WarningEvent}s on the assumption that most resources
+     * (at least the read-only ones) produce no change events.
      */
     @Override
     public <T extends StoreEvent> void addListener(StoreListener<? super T> listener, Class<T> eventType) {
-    }
-
-    /**
-     * Ignored in current implementation, on the assumption that most resources produce no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
-     */
-    @Override
-    public <T extends StoreEvent> void removeListener(StoreListener<? super T> listener, Class<T> eventType) {
+        // If an argument is null, we let the parent class throws (indirectly) NullArgumentException.
+        if (listener == null || eventType == null || eventType.isAssignableFrom(WarningEvent.class)) {
+            super.addListener(listener, eventType);
+        }
     }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java
index 291bf24..5b48f15 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AggregatedFeatureSet.java
@@ -20,16 +20,14 @@ import java.util.List;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Optional;
-import org.opengis.util.GenericName;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.maintenance.ScopeCode;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.storage.event.StoreListeners;
 
 // Branch-dependent imports
 import org.opengis.feature.FeatureType;
@@ -64,24 +62,10 @@ abstract class AggregatedFeatureSet extends AbstractFeatureSet {
     /**
      * Creates a new aggregated feature set.
      *
-     * @param  listeners  the set of registered warning listeners for the data store, or {@code null} if none.
+     * @param  parent  listeners of the parent resource, or {@code null} if none.
      */
-    protected AggregatedFeatureSet(final WarningListeners<DataStore> listeners) {
-        super(listeners);
-        /*
-         * TODO: we should add listeners on source feature sets. By doing this,
-         *       we could be notified of changes and invoke clearCache().
-         */
-    }
-
-    /**
-     * Creates a new feature set with the same warning listeners than the given resource,
-     * or with {@code null} listeners if they are unknown.
-     *
-     * @param resource  the resources from which to get the listeners, or {@code null} if none.
-     */
-    protected AggregatedFeatureSet(final FeatureSet resource) {
-        super(resource);
+    protected AggregatedFeatureSet(final StoreListeners parent) {
+        super(parent);
     }
 
     /**
@@ -93,14 +77,6 @@ abstract class AggregatedFeatureSet extends AbstractFeatureSet {
     abstract Collection<FeatureSet> dependencies();
 
     /**
-     * Returns an empty value since this resource is a computation result.
-     */
-    @Override
-    public Optional<GenericName> getIdentifier() {
-        return Optional.empty();
-    }
-
-    /**
      * Adds the envelopes of the aggregated feature sets in the given list. If some of the feature sets
      * are themselves aggregated feature sets, then this method traverses them recursively. We compute
      * the union of all envelopes at once after we got all envelopes.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ConcatenatedFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ConcatenatedFeatureSet.java
index e0de321..3e0b5a7 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ConcatenatedFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ConcatenatedFeatureSet.java
@@ -23,15 +23,14 @@ import java.util.OptionalLong;
 import java.util.stream.Stream;
 import org.apache.sis.feature.Features;
 import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.BackingStoreException;
-import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.storage.Query;
 
 // Branch-dependent imports
@@ -68,10 +67,10 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet {
     private final FeatureType commonType;
 
     /**
-     * Creates a new concatenated feature set with the same listeners and types than the given feature set,
+     * Creates a new concatenated feature set with the same types than the given feature set,
      * but different sources.
      */
-    private ConcatenatedFeatureSet(final ConcatenatedFeatureSet original, final FeatureSet[] sources) {
+    private ConcatenatedFeatureSet(final FeatureSet[] sources, final ConcatenatedFeatureSet original) {
         super(original);
         this.sources = UnmodifiableArrayList.wrap(sources);
         commonType = original.commonType;
@@ -83,13 +82,13 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet {
      * this verification must be done by the caller. This constructor retains the given {@code sources} array
      * by direct reference; clone, if desired, shall be done by the caller.
      *
-     * @param  listeners  the set of registered warning listeners for the data store, or {@code null} if none.
-     * @param  sources    the sequence of feature sets to expose in a single set.
-     *                    Must neither be null, empty nor contain a single element only.
+     * @param  parent   listeners of the parent resource, or {@code null} if none.
+     * @param  sources  the sequence of feature sets to expose in a single set.
+     *                  Must neither be null, empty nor contain a single element only.
      * @throws DataStoreException if given feature sets does not share any common type.
      */
-    protected ConcatenatedFeatureSet(final WarningListeners<DataStore> listeners, final FeatureSet[] sources) throws DataStoreException {
-        super(listeners);
+    protected ConcatenatedFeatureSet(final StoreListeners parent, final FeatureSet[] sources) throws DataStoreException {
+        super(parent);
         for (int i=0; i<sources.length; i++) {
             ArgumentChecks.ensureNonNullElement("sources", i, sources[i]);
         }
@@ -120,7 +119,7 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet {
             ArgumentChecks.ensureNonNullElement("sources", 0, fs);
             return fs;
         } else {
-            return new ConcatenatedFeatureSet((WarningListeners<DataStore>) null, sources.clone());
+            return new ConcatenatedFeatureSet(null, sources.clone());
         }
     }
 
@@ -143,7 +142,7 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet {
                 return fs;
             }
             default: {
-                return new ConcatenatedFeatureSet((WarningListeners<DataStore>) null, sources.toArray(new FeatureSet[size]));
+                return new ConcatenatedFeatureSet(null, sources.toArray(new FeatureSet[size]));
             }
         }
     }
@@ -236,6 +235,6 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet {
             subsets[i] = source.subset(query);
             modified |= subsets[i] != source;
         }
-        return modified ? new ConcatenatedFeatureSet(this, subsets) : this;
+        return modified ? new ConcatenatedFeatureSet(subsets, this) : this;
     }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/DocumentedStoreProvider.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/DocumentedStoreProvider.java
index ab8b8b2..6170f40 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/DocumentedStoreProvider.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/DocumentedStoreProvider.java
@@ -21,9 +21,8 @@ import java.util.logging.LogRecord;
 import org.opengis.metadata.distribution.Format;
 import org.apache.sis.metadata.sql.MetadataSource;
 import org.apache.sis.metadata.sql.MetadataStoreException;
-import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.logging.Logging;
-import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.internal.system.Modules;
 
 
@@ -89,7 +88,7 @@ public abstract class DocumentedStoreProvider extends URIDataStore.Provider {
      * @param  listeners  where to report the warning in case of error, or {@code null} if none.
      * @return a description of the data format.
      */
-    public final Format getFormat(final WarningListeners<DataStore> listeners) {
+    public final Format getFormat(final StoreListeners listeners) {
         /*
          * Note: this method does not cache the format because such caching is already done by MetadataSource.
          */
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java
index bc501b5..30ffcec 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java
@@ -30,13 +30,12 @@ import org.apache.sis.feature.DefaultFeatureType;
 import org.apache.sis.feature.DefaultAssociationRole;
 import org.apache.sis.internal.feature.AttributeConvention;
 import org.apache.sis.internal.storage.query.SimpleQuery;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.collection.Containers;
-import org.apache.sis.util.logging.WarningListeners;
 
 // Branch-dependent imports
 import org.opengis.feature.Feature;
@@ -198,7 +197,7 @@ public class JoinFeatureSet extends AggregatedFeatureSet {
      *   <li>{@code "identifierSuffix"} — string to insert at the end of join identifiers (optional).</li>
      * </ul>
      *
-     * @param  listeners    the set of registered warning listeners for the data store, or {@code null} if none.
+     * @param  parent       listeners of the parent resource, or {@code null} if none.
      * @param  left         the first source of features. This is often (but not necessarily) the largest set.
      * @param  leftAlias    name of the associations to the {@code left} features, or {@code null} for a default name.
      * @param  right        the second source of features. Should be the set in which iterations are cheapest.
@@ -208,14 +207,14 @@ public class JoinFeatureSet extends AggregatedFeatureSet {
      * @param  featureInfo  information about the {@link FeatureType} of this
      * @throws DataStoreException if an error occurred while creating the feature set.
      */
-    public JoinFeatureSet(final WarningListeners<DataStore> listeners,
+    public JoinFeatureSet(final StoreListeners parent,
                           final FeatureSet left,  String leftAlias,
                           final FeatureSet right, String rightAlias,
                           final Type joinType, final PropertyIsEqualTo condition,
                           Map<String,?> featureInfo)
             throws DataStoreException
     {
-        super(listeners);
+        super(parent);
         final FeatureType leftType  = left.getType();
         final FeatureType rightType = right.getType();
         final GenericName leftName  = leftType.getName();
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
index 065d2ff..061bbdf 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MemoryFeatureSet.java
@@ -19,9 +19,8 @@ package org.apache.sis.internal.storage;
 import java.util.Collection;
 import java.util.OptionalLong;
 import java.util.stream.Stream;
-import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.logging.WarningListeners;
 
 // Branch-dependent imports
 import org.opengis.feature.Feature;
@@ -55,14 +54,12 @@ public class MemoryFeatureSet extends AbstractFeatureSet {
      * <code>{@linkplain Feature#getType()} == type</code> for all elements in the given collection
      * (this is not verified).
      *
-     * @param listeners  the set of registered warning listeners for the data store, or {@code null} if none.
+     * @param parent     listeners of the parent resource, or {@code null} if none.
      * @param type       the type of all features in the given collection.
      * @param features   collection of stored features. This collection will not be copied.
      */
-    public MemoryFeatureSet(final WarningListeners<DataStore> listeners,
-                            final FeatureType type, final Collection<Feature> features)
-    {
-        super(listeners);
+    public MemoryFeatureSet(final StoreListeners parent, final FeatureType type, final Collection<Feature> features) {
+        super(parent);
         ArgumentChecks.ensureNonNull("type",     type);
         ArgumentChecks.ensureNonNull("features", features);
         this.type     = type;
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/URIDataStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/URIDataStore.java
index 25f179e..7db0fbb 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/URIDataStore.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/URIDataStore.java
@@ -33,6 +33,7 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.IllegalOpenParameterException;
 import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
+import org.apache.sis.storage.event.WarningEvent;
 import org.apache.sis.internal.storage.io.IOUtilities;
 
 
@@ -268,26 +269,14 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R
     }
 
     /**
-     * Ignored in current implementation, on the assumption that most data stores
-     * (at least the read-only ones) produce no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
+     * Registers only listeners for {@link WarningEvent}s on the assumption that most data stores
+     * (at least the read-only ones) produce no change events.
      */
     @Override
     public <T extends StoreEvent> void addListener(StoreListener<? super T> listener, Class<T> eventType) {
-    }
-
-    /**
-     * Ignored in current implementation, on the assumption that most data stores
-     * (at least the read-only ones) produce no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
-     */
-    @Override
-    public <T extends StoreEvent> void removeListener(StoreListener<? super T> listener, Class<T> eventType) {
+        // If an argument is null, we let the parent class throws (indirectly) NullArgumentException.
+        if (listener == null || eventType == null || eventType.isAssignableFrom(WarningEvent.class)) {
+            super.addListener(listener, eventType);
+        }
     }
 }
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 6b46c89..6b42068 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
@@ -57,6 +57,7 @@ import org.apache.sis.internal.storage.StoreResource;
 import org.apache.sis.internal.storage.Resources;
 import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
+import org.apache.sis.storage.event.WarningEvent;
 
 
 /**
@@ -403,25 +404,16 @@ class Store extends DataStore implements StoreResource, Aggregate, DirectoryStre
     }
 
     /**
-     * Ignored in current implementation, since this resource produces no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
+     * Registers a listener to notify when the specified kind of event occurs in this data store.
+     * The current implementation of this data store can emit only {@link WarningEvent}s;
+     * any listener specified for another kind of events will be ignored.
      */
     @Override
     public <T extends StoreEvent> void addListener(StoreListener<? super T> listener, Class<T> eventType) {
-    }
-
-    /**
-     * Ignored in current implementation, since this resource produces no events.
-     *
-     * @param  <T>        {@inheritDoc}
-     * @param  listener   {@inheritDoc}
-     * @param  eventType  {@inheritDoc}
-     */
-    @Override
-    public <T extends StoreEvent> void removeListener(StoreListener<? super T> listener, Class<T> eventType) {
+        // If an argument is null, we let the parent class throws (indirectly) NullArgumentException.
+        if (listener == null || eventType == null || eventType.isAssignableFrom(WarningEvent.class)) {
+            super.addListener(listener, eventType);
+        }
     }
 
     /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
index 41c05e4..0f4dc55 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
@@ -45,12 +45,11 @@ import java.nio.channels.ReadableByteChannel;
 import java.nio.channels.WritableByteChannel;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.storage.Resources;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.ForwardOnlyStorageException;
+import org.apache.sis.storage.event.StoreListeners;
 
 
 /**
@@ -231,10 +230,10 @@ public abstract class ChannelFactory {
         if (storage instanceof URL) {
             final URL file = (URL) storage;
             return new ChannelFactory() {
-                @Override public ReadableByteChannel readable(String filename, WarningListeners<DataStore> listeners) throws IOException {
+                @Override public ReadableByteChannel readable(String filename, StoreListeners listeners) throws IOException {
                     return Channels.newChannel(file.openStream());
                 }
-                @Override public WritableByteChannel writable(String filename, WarningListeners<DataStore> listeners) throws IOException {
+                @Override public WritableByteChannel writable(String filename, StoreListeners listeners) throws IOException {
                     return Channels.newChannel(file.openConnection().getOutputStream());
                 }
             };
@@ -252,10 +251,10 @@ public abstract class ChannelFactory {
             final Path path = (Path) storage;
             if (!Files.isDirectory(path)) {
                 return new ChannelFactory() {
-                    @Override public ReadableByteChannel readable(String filename, WarningListeners<DataStore> listeners) throws IOException {
+                    @Override public ReadableByteChannel readable(String filename, StoreListeners listeners) throws IOException {
                         return Files.newByteChannel(path, optionSet);
                     }
-                    @Override public WritableByteChannel writable(String filename, WarningListeners<DataStore> listeners) throws IOException {
+                    @Override public WritableByteChannel writable(String filename, StoreListeners listeners) throws IOException {
                         return Files.newByteChannel(path, optionSet);
                     }
                 };
@@ -297,7 +296,7 @@ public abstract class ChannelFactory {
      * @throws DataStoreException if the channel is read-once.
      * @throws IOException if the input stream or its underlying byte channel can not be created.
      */
-    public InputStream inputStream(String filename, WarningListeners<DataStore> listeners)
+    public InputStream inputStream(String filename, StoreListeners listeners)
             throws DataStoreException, IOException
     {
         return Channels.newInputStream(readable(filename, listeners));
@@ -313,7 +312,7 @@ public abstract class ChannelFactory {
      * @throws DataStoreException if the channel is write-once.
      * @throws IOException if the output stream or its underlying byte channel can not be created.
      */
-    public OutputStream outputStream(String filename, WarningListeners<DataStore> listeners)
+    public OutputStream outputStream(String filename, StoreListeners listeners)
             throws DataStoreException, IOException
     {
         return Channels.newOutputStream(writable(filename, listeners));
@@ -330,7 +329,7 @@ public abstract class ChannelFactory {
      * @throws DataStoreException if the channel is read-once.
      * @throws IOException if an error occurred while opening the channel.
      */
-    public abstract ReadableByteChannel readable(String filename, WarningListeners<DataStore> listeners)
+    public abstract ReadableByteChannel readable(String filename, StoreListeners listeners)
             throws DataStoreException, IOException;
 
     /**
@@ -344,7 +343,7 @@ public abstract class ChannelFactory {
      * @throws DataStoreException if the channel is write-once.
      * @throws IOException if an error occurred while opening the channel.
      */
-    public abstract WritableByteChannel writable(String filename, WarningListeners<DataStore> listeners)
+    public abstract WritableByteChannel writable(String filename, StoreListeners listeners)
             throws DataStoreException, IOException;
 
     /**
@@ -385,7 +384,7 @@ public abstract class ChannelFactory {
          * throws an exception on all subsequent invocations.
          */
         @Override
-        public ReadableByteChannel readable(final String filename, final WarningListeners<DataStore> listeners)
+        public ReadableByteChannel readable(final String filename, final StoreListeners listeners)
                 throws DataStoreException, IOException
         {
             final Channel in = channel;
@@ -407,7 +406,7 @@ public abstract class ChannelFactory {
          * throws an exception on all subsequent invocations.
          */
         @Override
-        public WritableByteChannel writable(final String filename, final WarningListeners<DataStore> listeners)
+        public WritableByteChannel writable(final String filename, final StoreListeners listeners)
                 throws DataStoreException, IOException
         {
             final Channel out = channel;
@@ -459,7 +458,7 @@ public abstract class ChannelFactory {
          * {@link File} to a {@link Path}. On all subsequent invocations, the file is opened silently.</p>
          */
         @Override
-        public FileInputStream inputStream(final String filename, final WarningListeners<DataStore> listeners)
+        public FileInputStream inputStream(final String filename, final StoreListeners listeners)
                 throws IOException
         {
             final FileInputStream in;
@@ -489,7 +488,7 @@ public abstract class ChannelFactory {
          * {@link File} to a {@link Path}. On all subsequent invocations, the file is opened silently.</p>
          */
         @Override
-        public FileOutputStream outputStream(final String filename, final WarningListeners<DataStore> listeners)
+        public FileOutputStream outputStream(final String filename, final StoreListeners listeners)
                 throws IOException
         {
             final FileOutputStream out;
@@ -511,7 +510,7 @@ public abstract class ChannelFactory {
          * Since the exception was nevertheless unexpected, log its stack trace in order to allow the developer
          * to check if there is something wrong.
          */
-        private void warning(final String method, final WarningListeners<DataStore> listeners) {
+        private void warning(final String method, final StoreListeners listeners) {
             if (cause != null) {
                 final LogRecord record = new LogRecord(Level.WARNING, cause.toString());
                 record.setLoggerName(Modules.STORAGE);
@@ -531,7 +530,7 @@ public abstract class ChannelFactory {
          * Opens a new channel for the file given at construction time.
          */
         @Override
-        public ReadableByteChannel readable(String filename, WarningListeners<DataStore> listeners) throws IOException {
+        public ReadableByteChannel readable(String filename, StoreListeners listeners) throws IOException {
             return inputStream(filename, listeners).getChannel();
         }
 
@@ -539,7 +538,7 @@ public abstract class ChannelFactory {
          * Opens a new channel for the file given at construction time.
          */
         @Override
-        public WritableByteChannel writable(String filename, WarningListeners<DataStore> listeners) throws IOException {
+        public WritableByteChannel writable(String filename, StoreListeners listeners) throws IOException {
             return outputStream(filename, listeners).getChannel();
         }
     }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/FeatureSubset.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/FeatureSubset.java
index 828feb5..2a2b887 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/FeatureSubset.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/FeatureSubset.java
@@ -17,9 +17,7 @@
 package org.apache.sis.internal.storage.query;
 
 import java.util.List;
-import java.util.Optional;
 import java.util.stream.Stream;
-import org.opengis.util.GenericName;
 import org.apache.sis.internal.feature.FeatureUtilities;
 import org.apache.sis.internal.storage.AbstractFeatureSet;
 import org.apache.sis.internal.storage.Resources;
@@ -27,6 +25,7 @@ import org.apache.sis.filter.InvalidExpressionException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.event.StoreListeners;
 
 // Branch-dependent imports
 import org.opengis.feature.Feature;
@@ -69,20 +68,12 @@ final class FeatureSubset extends AbstractFeatureSet {
      * Creates a new set of features by filtering the given set using the given query.
      */
     FeatureSubset(final FeatureSet source, final SimpleQuery query) {
-        super(source);
+        super(source instanceof StoreListeners ? (StoreListeners) source : null);
         this.source = source;
         this.query = query;
     }
 
     /**
-     * Returns an empty value since this resource is a computation result.
-     */
-    @Override
-    public Optional<GenericName> getIdentifier() {
-        return Optional.empty();
-    }
-
-    /**
      * Returns a description of properties that are common to all features in this dataset.
      */
     @Override
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/StoreFormat.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/StoreFormat.java
index 8d042c2..d7342f0 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/StoreFormat.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/StoreFormat.java
@@ -25,7 +25,6 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.io.wkt.WKTFormat;
 import org.apache.sis.io.wkt.Warnings;
 import org.apache.sis.referencing.CRS;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.internal.feature.Geometries;
@@ -33,7 +32,7 @@ import org.apache.sis.internal.feature.GeometryWrapper;
 import org.apache.sis.internal.referencing.DefinitionVerifier;
 import org.apache.sis.internal.storage.Resources;
 import org.apache.sis.internal.system.Loggers;
-import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.ArraysExt;
 
 
@@ -57,7 +56,7 @@ public final class StoreFormat extends WKTFormat {
     /**
      * Where to send warnings.
      */
-    private final WarningListeners<DataStore> listeners;
+    private final StoreListeners listeners;
 
     /**
      * Creates a new WKT parser and encoder.
@@ -65,7 +64,7 @@ public final class StoreFormat extends WKTFormat {
      * @param  library    the geometry library, or {@code null} for the default.
      * @param  listeners  where to send warnings.
      */
-    public StoreFormat(final GeometryLibrary library, final WarningListeners<DataStore> listeners) {
+    public StoreFormat(final GeometryLibrary library, final StoreListeners listeners) {
         super(null, null);
         this.library   = library;
         this.listeners = listeners;
@@ -152,10 +151,9 @@ public final class StoreFormat extends WKTFormat {
      * Reports a warning for a WKT that can not be read. This method should be invoked only when the CRS
      * can not be created at all; it should not be invoked if the CRS has been created with some warnings.
      */
-    final void log(final Exception e) {
-        final DataStore store = listeners.getSource();
-        listeners.warning(Resources.forLocale(store.getLocale())
-                .getString(Resources.Keys.CanNotReadCRS_WKT_1, store.getDisplayName()), e);
+    private void log(final Exception e) {
+        listeners.warning(Resources.forLocale(listeners.getLocale())
+                .getString(Resources.Keys.CanNotReadCRS_WKT_1, listeners.getSourceName()), e);
     }
 
     /**
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 e6a1509..4000afa 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
@@ -33,6 +33,7 @@ import org.apache.sis.xml.XML;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.UnsupportedStorageException;
+import org.apache.sis.storage.event.WarningEvent;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.util.logging.WarningListener;
 import org.apache.sis.util.resources.Errors;
@@ -121,7 +122,7 @@ final class Store extends URIDataStore {
      * Returns the properties to give to the (un)marshaller.
      */
     private Map<String,?> properties() {
-        if (listeners.hasListeners()) {
+        if (listeners.hasListeners(WarningEvent.class)) {
             return Collections.singletonMap(XML.WARNING_LISTENER, new WarningListener<Object>() {
                 /** Returns the type of objects that emit warnings of interest for this listener. */
                 @Override public Class<Object> getSourceClass() {
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
index 3b4577e..194a435 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
@@ -31,11 +31,13 @@ import org.opengis.parameter.ParameterValueGroup;
 import org.apache.sis.util.Localized;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.WarningListener;
-import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.internal.storage.StoreUtilities;
 import org.apache.sis.internal.storage.Resources;
 import org.apache.sis.internal.util.Strings;
 import org.apache.sis.referencing.NamedIdentifier;
+import org.apache.sis.storage.event.StoreEvent;
+import org.apache.sis.storage.event.StoreListener;
+import org.apache.sis.storage.event.StoreListeners;
 
 
 /**
@@ -100,9 +102,9 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
     private Locale locale;
 
     /**
-     * The set of registered {@link WarningListener}s for this data store.
+     * The set of registered {@link StoreListener}s for this data store.
      */
-    protected final WarningListeners<DataStore> listeners;
+    protected final StoreListeners listeners;
 
     /**
      * Creates a new instance with no provider and initially no listener.
@@ -111,7 +113,7 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
         provider  = null;
         name      = null;
         locale    = Locale.getDefault(Locale.Category.DISPLAY);
-        listeners = new WarningListeners<>(this);
+        listeners = new StoreListeners(null, this);
     }
 
     /**
@@ -130,7 +132,7 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
         this.provider  = provider;
         this.name      = connector.getStorageName();
         this.locale    = Locale.getDefault(Locale.Category.DISPLAY);
-        this.listeners = new WarningListeners<>(this);
+        this.listeners = new StoreListeners(null, this);
         /*
          * Above locale is NOT OptionKey.LOCALE because we are not talking about the same locale.
          * The one in this DataStore is for warning and exception messages, not for parsing data.
@@ -140,8 +142,8 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
     /**
      * Creates a new instance as a child of another data store instance.
      * The new instance inherits the parent {@linkplain #getProvider() provider}.
-     * The parent and the child share the same listeners: adding or removing a listener to a parent
-     * adds or removes the same listeners to all children, and conversely.
+     * Events created by this {@code DataStore} are forwarded to listeners registered
+     * into the parent data store too.
      *
      * @param  parent     the parent data store, or {@code null} if none.
      * @param  connector  information about the storage (URL, stream, reader instance, <i>etc</i>).
@@ -151,15 +153,17 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
      */
     protected DataStore(final DataStore parent, final StorageConnector connector) throws DataStoreException {
         ArgumentChecks.ensureNonNull("connector", connector);
+        final StoreListeners forwardTo;
         if (parent != null) {
+            forwardTo = parent.listeners;
             provider  = parent.provider;
             locale    = parent.locale;
-            listeners = parent.listeners;
         } else {
+            forwardTo = null;
             provider  = null;
             locale    = Locale.getDefault(Locale.Category.DISPLAY);
-            listeners = new WarningListeners<>(this);
         }
+        listeners = new StoreListeners(forwardTo, this);
         name = connector.getStorageName();
     }
 
@@ -422,6 +426,66 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
     }
 
     /**
+     * Registers a listener to notify when the specified kind of event occurs in this data store or in a resource.
+     * The data store will call the {@link StoreListener#eventOccured(StoreEvent)} method when new events matching
+     * the {@code eventType} occur. An event may be a change in data store content or structure, or a warning that
+     * occurred during a read or write operation.
+     *
+     * <p>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)}
+     * method will be invoked only once per event. This filtering applies even if the listener is registered
+     * on individual resources of this data store.</p>
+     *
+     * <p>If this data store may produce events of the given type, then the given listener is kept by strong reference;
+     * it will not be garbage collected unless {@linkplain #removeListener(StoreListener, Class) explicitly removed}
+     * or unless this {@code DataStore} is itself garbage collected. However if the given type of events can never
+     * happen with this data store, then this method is not required to keep a reference to the given listener.</p>
+     *
+     * <div class="section">Warning events</div>
+     * If {@code eventType} is assignable from <code>{@linkplain org.apache.sis.storage.event.WarningEvent}.class</code>,
+     * then registering that listener turns off logging of warning messages for this data store.
+     * 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  listener   listener to notify about events.
+     * @param  eventType  type of {@link StoreEvent} to listen (can not be {@code null}).
+     *
+     * @since 1.0
+     */
+    @Override
+    public <T extends StoreEvent> void addListener(StoreListener<? super T> listener, Class<T> eventType) {
+        listeners.addListener(listener, eventType);
+    }
+
+    /**
+     * Unregisters a listener previously added to this data store for the given type of events.
+     * The {@code eventType} must be the exact same class than the one given to the {@code addListener(…)} method;
+     * this method does not remove listeners registered for subclasses and does not remove listeners registered in
+     * children resources.
+     *
+     * <p>If the same listener has been registered many times for the same even type, then this method removes only
+     * the most recent registration. In other words if {@code addListener(ls, type)} has been invoked twice, then
+     * {@code removeListener(ls, type)} needs to be invoked twice in order to remove all instances of that listener.
+     * If the given listener is not found, then this method does nothing (no exception is thrown).</p>
+     *
+     * <div class="section">Warning events</div>
+     * If {@code eventType} is <code>{@linkplain org.apache.sis.storage.event.WarningEvent}.class</code>
+     * and if, after this method invocation, there is no remaining listener for warning events,
+     * then this {@code DataStore} will send future warnings to the loggers.
+     *
+     * @param  <T>        compile-time value of the {@code eventType} argument.
+     * @param  listener   listener to stop notifying about events.
+     * @param  eventType  type of {@link StoreEvent} which were listened (can not be {@code null}).
+     *
+     * @since 1.0
+     */
+    @Override
+    public <T extends StoreEvent> void removeListener(StoreListener<? super T> listener, Class<T> eventType) {
+        listeners.removeListener(listener, eventType);
+    }
+
+    /**
      * Adds a listener to be notified when a warning occurred while reading from or writing to the storage.
      * When a warning occurs, there is a choice:
      *
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 3360ef9..7b6603f 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
@@ -76,15 +76,9 @@ public abstract class StoreEvent extends EventObject implements Localized {
      */
     @Override
     public Locale getLocale() {
-        return getLocale(source);
-    }
-
-    /**
-     * {@link #getLocale()} implementation shared with {@link StoreListeners#getLocale()}.
-     */
-    static Locale getLocale(final Object source) {
         if (source instanceof Localized) {
-            return ((Localized) source).getLocale();
+            final Locale locale = ((Localized) source).getLocale();
+            if (locale != null) return locale;
         }
         if (source instanceof StoreResource) {
             final DataStore ds = ((StoreResource) source).getOriginator();
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 65e274b..01521cb 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
@@ -23,6 +23,7 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.logging.LogRecord;
 import java.lang.reflect.Method;
+import org.apache.sis.util.Classes;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Localized;
 import org.apache.sis.util.Exceptions;
@@ -75,7 +76,7 @@ import org.apache.sis.storage.Resource;
  * @since   1.0
  * @module
  */
-public class StoreListeners extends org.apache.sis.util.logging.WarningListeners<Resource> implements Localized {
+public class StoreListeners extends org.apache.sis.util.logging.WarningListeners implements Localized {
     /**
      * Parent manager to notify in addition to this manager.
      */
@@ -208,9 +209,18 @@ public class StoreListeners extends org.apache.sis.util.logging.WarningListeners
      * @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.
      */
-    public StoreListeners(final StoreListeners parent, final Resource source) {
+    public StoreListeners(final StoreListeners parent, Resource source) {
         super(source);
-        ArgumentChecks.ensureNonNull("source", source);
+        /*
+         * Undocumented feature for allowing subclass to specify `this` as the source resource.
+         * This is used as a convenience by AbstractResource internal class. We need this hack
+         * because subclasses can not reference `this` before super-class constructor completed.
+         */
+        if (source == null && this instanceof Resource) {
+            source = (Resource) this;
+        } else {
+            ArgumentChecks.ensureNonNull("source", source);
+        }
         this.source = source;
         this.parent = parent;
     }
@@ -244,6 +254,35 @@ public class StoreListeners extends org.apache.sis.util.logging.WarningListeners
     }
 
     /**
+     * Returns a short name or label for the source. It may be the name of the file opened by a data store.
+     * The returned name can be useful in warning messages for identifying the problematic source.
+     *
+     * <p>The default implementation {@linkplain DataStore#getDisplayName() fetches that name from the data store},
+     * or returns an arbitrary name if it can get it otherwise.</p>
+     *
+     * @return a short name of label for the source (never {@code null}).
+     *
+     * @see DataStore#getDisplayName()
+     */
+    public String getSourceName() {
+        final DataStore ds = getDataStore(this);
+        if (ds != null) {
+            String name = ds.getDisplayName();
+            if (name != null) {
+                return name;
+            }
+            final DataStoreProvider provider = ds.getProvider();
+            if (provider != null) {
+                name = provider.getShortName();
+                if (name != null) {
+                    return name;
+                }
+            }
+        }
+        return Classes.getShortClassName(source);
+    }
+
+    /**
      * Returns the locale used by this manager, or {@code null} if unspecified.
      * That locale is typically inherited from the {@link DataStore} locale
      * and can be used for formatting messages.
@@ -255,7 +294,16 @@ public class StoreListeners extends org.apache.sis.util.logging.WarningListeners
      */
     @Override
     public Locale getLocale() {
-        return StoreEvent.getLocale(source);
+        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;
     }
 
     /**
@@ -507,6 +555,7 @@ public class StoreListeners extends org.apache.sis.util.logging.WarningListeners
         }
         if (ce == null) {
             ce = new ForType<>(eventType, listeners);
+            listeners = ce;
         }
         ce.add(listener);
     }
@@ -546,16 +595,22 @@ public class StoreListeners extends org.apache.sis.util.logging.WarningListeners
     }
 
     /**
-     * Returns {@code true} if this object contains at least one listener.
+     * Returns {@code true} if this object or its parent contains at least one listener for the given type of event.
      *
-     * @return {@code true} if this object contains at least one listener, {@code false} otherwise.
+     * @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.
      */
-    @Override
-    public boolean hasListeners() {
+    public boolean hasListeners(final Class<? extends StoreEvent> eventType) {
+        ArgumentChecks.ensureNonNull("eventType", eventType);
         StoreListeners m = this;
         do {
-            if (listeners != null && listeners.hasListeners()) {
-                return true;
+            for (ForType<?> e = m.listeners; e != null; e = e.next) {
+                if (eventType.isAssignableFrom(e.type)) {
+                    if (e.hasListeners()) {
+                        return true;
+                    }
+                    break;
+                }
             }
             m = m.parent;
         } while (m != null);
@@ -563,11 +618,22 @@ public class StoreListeners extends org.apache.sis.util.logging.WarningListeners
     }
 
     /**
+     * Returns {@code true} if this object contains at least one listener.
+     *
+     * @return {@code true} if this object contains at least one listener, {@code false} otherwise.
+     */
+    @Override
+    @Deprecated
+    public boolean hasListeners() {
+        return hasListeners(StoreEvent.class);
+    }
+
+    /**
      * @deprecated Replaced by {@code addListener(listener, WarningEvent.class)}.
      */
     @Override
     @Deprecated
-    public void addWarningListener(final WarningListener<? super Resource> listener) {
+    public void addWarningListener(final WarningListener listener) {
         addListener(new Legacy(listener), WarningEvent.class);
     }
 
@@ -576,7 +642,7 @@ public class StoreListeners extends org.apache.sis.util.logging.WarningListeners
      */
     @Override
     @Deprecated
-    public void removeWarningListener(final WarningListener<? super Resource> listener) {
+    public void removeWarningListener(final WarningListener listener) {
         for (ForType<?> e = listeners; e != null; e = e.next) {
             if (e.type.equals(WarningEvent.class)) {
                 StoreListener<?>[] list = e.listeners;
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/AbstractGridResourceTest.java b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/AbstractGridResourceTest.java
index f4b9f1b..37bfeea 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/AbstractGridResourceTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/AbstractGridResourceTest.java
@@ -17,8 +17,6 @@
 package org.apache.sis.internal.storage;
 
 import java.util.List;
-import java.util.Optional;
-import org.opengis.util.GenericName;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
@@ -42,7 +40,6 @@ public final strictfp class AbstractGridResourceTest  extends TestCase {
      * A resource performing no operation.
      */
     private final AbstractGridResource resource = new AbstractGridResource((AbstractGridResource) null) {
-        @Override public Optional<GenericName> getIdentifier()       {return Optional.empty();}
         @Override public GridGeometry          getGridGeometry()     {throw new UnsupportedOperationException();}
         @Override public List<SampleDimension> getSampleDimensions() {throw new UnsupportedOperationException();}
         @Override public GridCoverage read(GridGeometry d, int... r) {throw new UnsupportedOperationException();}
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreMock.java b/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreMock.java
index 16b1f60..cbbce51 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreMock.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/DataStoreMock.java
@@ -18,8 +18,7 @@ package org.apache.sis.storage;
 
 import org.opengis.metadata.Metadata;
 import org.opengis.parameter.ParameterValueGroup;
-import org.apache.sis.storage.event.StoreEvent;
-import org.apache.sis.storage.event.StoreListener;
+import org.apache.sis.storage.event.StoreListeners;
 
 
 /**
@@ -30,7 +29,7 @@ import org.apache.sis.storage.event.StoreListener;
  * @since   0.8
  * @module
  */
-final strictfp class DataStoreMock extends DataStore {
+public final strictfp class DataStoreMock extends DataStore {
     /**
      * The display name.
      */
@@ -38,8 +37,10 @@ final strictfp class DataStoreMock extends DataStore {
 
     /**
      * Creates a new data store mock with the given display name.
+     *
+     * @param  name  data store display name.
      */
-    DataStoreMock(final String name) {
+    public DataStoreMock(final String name) {
         this.name = name;
     }
 
@@ -51,25 +52,46 @@ final strictfp class DataStoreMock extends DataStore {
         return name;
     }
 
+    /**
+     * Returns {@code null} since there is no open parameters.
+     */
     @Override
     public ParameterValueGroup getOpenParameters() {
         return null;
     }
 
+    /**
+     * Returns {@code null} since there is no metadata.
+     */
     @Override
     public Metadata getMetadata() {
         return null;
     }
 
+    /**
+     * Do nothing.
+     */
     @Override
-    public <T extends StoreEvent> void addListener(StoreListener<? super T> listener, Class<T> eventType) {
+    public void close() {
     }
 
-    @Override
-    public <T extends StoreEvent> void removeListener(StoreListener<? super T> listener, Class<T> eventType) {
+    /**
+     * Opens access to the data store listeners.
+     *
+     * @return the data store listeners.
+     */
+    public StoreListeners listeners() {
+        return listeners;
     }
 
-    @Override
-    public void close() {
+    /**
+     * Sends a pseudo-warning message for testing purpose. This method is defined in this class
+     * for allowing {@link StoreListeners} to detect that the warning come from a data store by
+     * inspecting the stack frame.
+     *
+     * @param  message  the message to send.
+     */
+    public void simulateWarning(String message) {
+        listeners.warning(message);
     }
 }
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
new file mode 100644
index 0000000..9185879
--- /dev/null
+++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.util.logging.Level;
+import java.util.logging.LogRecord;
+import org.apache.sis.storage.DataStoreMock;
+import org.apache.sis.test.DependsOnMethod;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests the {@link StoreListeners} class.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final strictfp class StoreListenersTest extends TestCase implements StoreListener<WarningEvent> {
+    /**
+     * Dummy data store used for firing events.
+     */
+    private final DataStoreMock store;
+
+    /**
+     * The warning received by {@link #eventOccured(WarningEvent)}.
+     * This is stored for allowing test methods to verify the properties.
+     */
+    private LogRecord warning;
+
+    /**
+     * Creates a new test case.
+     */
+    public StoreListenersTest() {
+        store = new DataStoreMock("Event emitter");
+    }
+
+    /**
+     * Invoked when a warning occurred. The implementation in this test verifies that the {@code source} property has
+     * the expected values, then stores the log record in the {@link #warning} field for inspection by the test method.
+     *
+     * @param  warning  the warning event emitted by the data store.
+     */
+    @Override
+    public void eventOccured(final WarningEvent warning) {
+        assertSame("source", store, warning.getSource());
+        this.warning = warning.getDescription();
+    }
+
+    /**
+     * Tests {@link StoreListeners#addListener(StoreListener, Class)} followed by
+     * {@link StoreListeners#removeListener(StoreListener, Class)}.
+     */
+    @Test
+    public void testAddAndRemoveStoreListener() {
+        final StoreListeners listeners = store.listeners();
+        assertFalse("hasListeners()", listeners.hasListeners(WarningEvent.class));
+        listeners.addListener(this, WarningEvent.class);
+        assertTrue("hasListeners()", listeners.hasListeners(WarningEvent.class));
+        listeners.removeListener(this, WarningEvent.class);
+        assertFalse("hasListeners()", listeners.hasListeners(WarningEvent.class));
+        listeners.removeListener(this, WarningEvent.class);         // Should be no-op.
+    }
+
+    /**
+     * Tests {@link StoreListeners#warning(String, Exception)} with a registered listener.
+     */
+    @Test
+    @DependsOnMethod("testAddAndRemoveStoreListener")
+    public void testWarning() {
+        final LogRecord record = new LogRecord(Level.WARNING, "The message");
+        store.addListener(this, WarningEvent.class);
+        store.listeners().warning(record);
+        assertSame(record, warning);
+    }
+
+    /**
+     * Tests {@link StoreListeners#warning(String, Exception)} with a registered listener.
+     * This method shall infer the source class name and source method name automatically.
+     */
+    @Test
+    @DependsOnMethod("testWarning")
+    public void testWarningWithAutoSource() {
+        store.addListener(this, WarningEvent.class);
+        store.simulateWarning("The message");
+        assertNotNull("Listener has not been notified.", warning);
+        assertEquals(DataStoreMock.class.getName(), warning.getSourceClassName());
+        assertEquals("simulateWarning", warning.getSourceMethodName());
+        assertEquals("The message", warning.getMessage());
+    }
+}
diff --git a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
index 084d68c..afa697e 100644
--- a/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/test/suite/StorageTestSuite.java
@@ -43,6 +43,7 @@ import org.junit.BeforeClass;
     org.apache.sis.storage.FeatureNamingTest.class,
     org.apache.sis.storage.ProbeResultTest.class,
     org.apache.sis.storage.StorageConnectorTest.class,
+    org.apache.sis.storage.event.StoreListenersTest.class,
     org.apache.sis.internal.storage.query.SimpleQueryTest.class,
     org.apache.sis.internal.storage.xml.MimeTypeDetectorTest.class,
     org.apache.sis.internal.storage.xml.StoreProviderTest.class,
diff --git a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/StaxDataStore.java b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/StaxDataStore.java
index 24f613c..e7d59e5 100644
--- a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/StaxDataStore.java
+++ b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/xml/stream/StaxDataStore.java
@@ -125,7 +125,7 @@ public abstract class StaxDataStore extends URIDataStore {
      * instead than creating new streams for re-reading the data.  If we can not reset the stream but can
      * create a new one, then this field will become a reference to the new stream. This change should be
      * done only in last resort, when there is no way to reuse the existing stream. This is because the
-     * streams created by {@link ChannelFactory#inputStream(String, WarningListeners)} are not of the same
+     * streams created by {@link ChannelFactory#inputStream(String, StoreListeners)} are not of the same
      * kind than the streams created by {@link StorageConnector}.</p>
      *
      * @see #close()


Mime
View raw message