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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 0bf48cdcae Add an event to be fired when a resource is closed. This 
event is handled in a special way, in that it automatically register another 
listener on the parent data store for propagating `CloseEvent` to child 
resources.
0bf48cdcae is described below

commit 0bf48cdcae1a0c2a6c593e342d9a1ce39ff64501
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Jun 7 19:45:30 2022 +0200

    Add an event to be fired when a resource is closed. This event is handled 
in a special way, in that it automatically register another listener on the 
parent data store for propagating `CloseEvent` to child resources.
    
    https://issues.apache.org/jira/browse/SIS-549
---
 .../apache/sis/storage/landsat/LandsatStore.java   |   5 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |   3 +-
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |   5 +-
 .../apache/sis/storage/netcdf/package-info.java    |   2 +-
 .../java/org/apache/sis/storage/sql/SQLStore.java  |   5 +-
 .../org/apache/sis/storage/sql/package-info.java   |   2 +-
 .../org/apache/sis/internal/storage/Resources.java |   5 +
 .../sis/internal/storage/Resources.properties      |   1 +
 .../sis/internal/storage/Resources_fr.properties   |   1 +
 .../org/apache/sis/internal/storage/csv/Store.java |   5 +-
 .../sis/internal/storage/csv/package-info.java     |   2 +-
 .../sis/internal/storage/esri/AsciiGridStore.java  |   3 +-
 .../sis/internal/storage/esri/RasterStore.java     |   3 +-
 .../sis/internal/storage/esri/RawRasterStore.java  |   3 +-
 .../sis/internal/storage/esri/WritableStore.java   |   3 +-
 .../sis/internal/storage/esri/package-info.java    |   2 +-
 .../apache/sis/internal/storage/folder/Store.java  |   5 +-
 .../sis/internal/storage/folder/package-info.java  |   2 +-
 .../sis/internal/storage/image/WorldFileStore.java |   5 +-
 .../sis/internal/storage/image/WritableStore.java  |   3 +-
 .../sis/internal/storage/image/package-info.java   |   2 +-
 .../org/apache/sis/internal/storage/wkt/Store.java |   5 +-
 .../sis/internal/storage/wkt/package-info.java     |   2 +-
 .../org/apache/sis/internal/storage/xml/Store.java |   5 +-
 .../sis/internal/storage/xml/package-info.java     |   2 +-
 .../java/org/apache/sis/storage/DataStore.java     |  18 +-
 .../org/apache/sis/storage/event/CloseEvent.java   |  87 +++++++++
 .../org/apache/sis/storage/event/StoreEvent.java   |  47 ++++-
 .../apache/sis/storage/event/StoreListeners.java   | 203 ++++++++++++++++++---
 .../java/org/apache/sis/storage/DataStoreMock.java |  15 +-
 .../sis/storage/event/StoreListenersTest.java      |  35 +++-
 .../org/apache/sis/internal/storage/gpx/Store.java |   5 +-
 .../sis/internal/storage/gpx/package-info.java     |   2 +-
 .../internal/storage/xml/stream/StaxDataStore.java |   4 +
 34 files changed, 426 insertions(+), 71 deletions(-)

diff --git 
a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/LandsatStore.java
 
b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/LandsatStore.java
index 30fa5eb49a..1786e17eda 100644
--- 
a/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/LandsatStore.java
+++ 
b/storage/sis-earth-observation/src/main/java/org/apache/sis/storage/landsat/LandsatStore.java
@@ -79,7 +79,7 @@ import org.apache.sis.setup.OptionKey;
  *
  * @author  Thi Phuong Hao Nguyen (VNSC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   1.1
  * @module
  */
@@ -153,7 +153,7 @@ public class LandsatStore extends DataStore implements 
Aggregate {
                     connector.getStorage(), 
connector.getOption(OptionKey.OPEN_OPTIONS));
         }
         if (getClass() == LandsatStore.class) {
-            listeners.useWarningEventsOnly();
+            listeners.useReadOnlyEvents();
         }
     }
 
@@ -294,6 +294,7 @@ public class LandsatStore extends DataStore implements 
Aggregate {
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();                  // Should never fail.
         metadata = null;
         DataStoreException error = null;
         for (final Band band : BandGroup.bands(components)) {
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 859cb69e3d..90c53e20c6 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
@@ -209,7 +209,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
             throw new DataStoreException(e);
         }
         if (getClass() == GeoTiffStore.class) {
-            listeners.useWarningEventsOnly();
+            listeners.useReadOnlyEvents();
         }
     }
 
@@ -511,6 +511,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();                  // Should never fail.
         final Reader r = reader;
         reader = null;
         components = null;
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 407aa5569f..3d1a5151c2 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
@@ -58,7 +58,7 @@ import ucar.nc2.constants.CDM;
  * Instances of this data store are created by {@link 
NetcdfStoreProvider#open(StorageConnector)}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see NetcdfStoreProvider
  *
@@ -129,7 +129,7 @@ public class NetcdfStore extends DataStore implements 
Aggregate {
             decoder.namespace = f.createNameSpace(f.createLocalName(null, id), 
null);
         }
         if (getClass() == NetcdfStore.class) {
-            listeners.useWarningEventsOnly();
+            listeners.useReadOnlyEvents();
         }
     }
 
@@ -267,6 +267,7 @@ public class NetcdfStore extends DataStore implements 
Aggregate {
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();                  // Should never fail.
         final Decoder reader = decoder;
         decoder    = null;
         metadata   = null;
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/package-info.java
 
b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/package-info.java
index 2955c7220d..547a5dc248 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/package-info.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/package-info.java
@@ -40,7 +40,7 @@
  * Care must be taken for avoiding confusion when using SIS and UCAR libraries 
together.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.3
  * @module
  */
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 06566785c9..c2fc534734 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
@@ -56,7 +56,7 @@ import org.apache.sis.util.Exceptions;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.0
  * @module
  */
@@ -163,7 +163,7 @@ public class SQLStore extends DataStore implements 
Aggregate {
         this.tableNames = ArraysExt.resize(tableNames, tableCount);
         this.queries    = ArraysExt.resize(queries,    queryCount);
         if (getClass() == SQLStore.class) {
-            listeners.useWarningEventsOnly();
+            listeners.useReadOnlyEvents();
         }
     }
 
@@ -332,6 +332,7 @@ public class SQLStore extends DataStore implements 
Aggregate {
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();      // Should never fail.
         // There is no JDBC connection to close here.
         model = null;
     }
diff --git 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/package-info.java
 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/package-info.java
index 5fab0666d4..89cdda1084 100644
--- 
a/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/package-info.java
+++ 
b/storage/sis-sqlstore/src/main/java/org/apache/sis/storage/sql/package-info.java
@@ -56,7 +56,7 @@
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.0
  * @module
  */
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
index d27cddc3e1..39b136756a 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
@@ -214,6 +214,11 @@ public final class Resources extends IndexedResourceBundle 
{
          */
         public static final short DuplicatedSampleDimensionIndex_1 = 53;
 
+        /**
+         * Exception occurred in a listener for events of type ‘{0}’.
+         */
+        public static final short ExceptionInListener_1 = 74;
+
         /**
          * Header in the “{0}” file is too large.
          */
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
index cd6a783721..0f863cd896 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
@@ -65,6 +65,7 @@ IncompatibleGridGeometry          = All coverages must have 
the same grid geomet
 InconsistentNameComponents_2      = Components of the \u201c{1}\u201d name are 
inconsistent with those of the name previously binded in \u201c{0}\u201d data 
store.
 InvalidExpression_2               = Invalid or unsupported \u201c{1}\u201d 
expression at index {0}.
 InvalidSampleDimensionIndex_2     = Sample dimension index {1} is invalid. 
Expected an index from 0 to {0} inclusive.
+ExceptionInListener_1             = Exception occurred in a listener for 
events of type \u2018{0}\u2019.
 LoadedGridCoverage_6              = Loaded grid coverage between {1} \u2013 
{2} and {3} \u2013 {4} from file \u201c{0}\u201d in {5} seconds.
 MarkNotSupported_1                = Marks are not supported on \u201c{0}\u201d 
stream.
 MissingResourceIdentifier_1       = Resource \u201c{0}\u201d does not have an 
identifier.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
index 6f87021ddc..3a5d43e79b 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
@@ -70,6 +70,7 @@ IncompatibleGridGeometry          = Toutes les couvertures de 
donn\u00e9es doive
 InvalidExpression_2               = Expression \u00ab\u202f{1}\u202f\u00bb 
invalide ou non-support\u00e9e \u00e0 l\u2019index {0}.
 InvalidSampleDimensionIndex_2     = L\u2019index de dimension 
d\u2019\u00e9chantillonnage {1} est invalide. On attendait un index de 0 \u00e0 
{0} inclusif.
 InconsistentNameComponents_2      = Les \u00e9l\u00e9ments qui composent le 
nom \u00ab\u202f{1}\u202f\u00bb ne sont pas coh\u00e9rents avec ceux du nom qui 
avait \u00e9t\u00e9 pr\u00e9c\u00e9demment li\u00e9 dans les donn\u00e9es de 
\u00ab\u202f{0}\u202f\u00bb.
+ExceptionInListener_1             = Une exception est survenue dans un capteur 
d'\u00e9v\u00e9nements de type \u2018{0}\u2019.
 LoadedGridCoverage_6              = Lecture d\u2019une couverture de 
donn\u00e9es entre {1} \u2013 {2} et {3} \u2013 {4} \u00e0 partir du fichier 
\u00ab\u202f{0}\u202f\u00bb en {5} secondes.
 MarkNotSupported_1                = Les marques ne sont pas support\u00e9es 
sur le flux \u00ab\u202f{0}\u202f\u00bb.
 MissingResourceIdentifier_1       = La ressource \u00ab\u202f{0}\u202f\u00bb 
n\u2019a pas d\u2019identifiant.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
index 50fd6de7b7..084d67ffa7 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
@@ -305,7 +305,7 @@ final class Store extends URIDataStore implements 
FeatureSet {
         this.featureType = featureType;
         this.foliation   = foliation;
         this.dissociate |= (timeEncoding == null);
-        listeners.useWarningEventsOnly();
+        listeners.useReadOnlyEvents();
     }
 
     /**
@@ -846,8 +846,9 @@ final class Store extends URIDataStore implements 
FeatureSet {
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();                  // Should never fail.
         final BufferedReader s = source;
-        source = null;                  // Cleared first in case of failure.
+        source = null;                      // Cleared first in case of 
failure.
         if (s != null) try {
             s.close();
         } catch (IOException e) {
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/package-info.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/package-info.java
index 3e75069761..a9fb532ee4 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/package-info.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/package-info.java
@@ -53,7 +53,7 @@
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.7
  * @module
  */
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStore.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStore.java
index 26266fd599..0980ae4a0f 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStore.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/AsciiGridStore.java
@@ -140,7 +140,7 @@ import org.apache.sis.util.resources.Errors;
  * which is usually the case given how inefficient this format is.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
@@ -544,6 +544,7 @@ cellsize:       if (value != null) {
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();                  // Should never fail.
         final CharactersView view = input;
         input        = null;                // Cleared first in case of 
failure.
         coverage     = null;
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
index 0e588da647..5dcda27a12 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RasterStore.java
@@ -133,7 +133,7 @@ abstract class RasterStore extends PRJDataStore implements 
GridCoverageResource
     RasterStore(final DataStoreProvider provider, final StorageConnector 
connector) throws DataStoreException {
         super(provider, connector);
         nodataValue = Double.NaN;
-        listeners.useWarningEventsOnly();
+        listeners.useReadOnlyEvents();
     }
 
     /**
@@ -515,6 +515,7 @@ abstract class RasterStore extends PRJDataStore implements 
GridCoverageResource
      */
     @Override
     public void close() throws DataStoreException {
+        // `listeners.close()` should be invoked by sub-classes.
         metadata = null;
     }
 }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStore.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStore.java
index adef8032fa..41006b2a3a 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStore.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/RawRasterStore.java
@@ -60,7 +60,7 @@ import static org.apache.sis.internal.util.Numerics.wholeDiv;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
@@ -531,6 +531,7 @@ final class RawRasterStore extends RasterStore {
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();                  // Should never fail.
         final ChannelDataInput in = input;
         input  = null;                      // Cleared first in case of 
failure.
         reader = null;
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/WritableStore.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/WritableStore.java
index 1b51c553c2..6f10d6c2ea 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/WritableStore.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/WritableStore.java
@@ -49,7 +49,7 @@ import org.opengis.coverage.grid.SequenceType;
  * An ASCII Grid store with writing capabilities.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
@@ -295,6 +295,7 @@ final class WritableStore extends AsciiGridStore implements 
WritableGridCoverage
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();                      // Should never fail.
         final ChannelDataOutput out = output;
         output = null;
         if (out != null) try {
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/package-info.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/package-info.java
index 1746e7c191..31e37c1273 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/package-info.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/esri/package-info.java
@@ -46,7 +46,7 @@
  * Sub-setting parameters are ignored.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see <a 
href="https://desktop.arcgis.com/en/arcmap/latest/manage-data/raster-and-images/esri-ascii-raster-format.htm";>Esri
 ASCII raster format</a>
  * @see <a 
href="https://desktop.arcgis.com/en/arcmap/latest/manage-data/raster-and-images/bil-bip-and-bsq-raster-files.htm";>BIL,
 BIP, and BSQ raster files</a>
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 d7b5ae5817..22c788df69 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
@@ -78,7 +78,7 @@ import org.apache.sis.storage.event.WarningEvent;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -178,7 +178,7 @@ class Store extends DataStore implements StoreResource, 
Aggregate, DirectoryStre
         children   = new ConcurrentHashMap<>();
         children.put(path.toRealPath(), this);
         componentProvider = format;
-        listeners.useWarningEventsOnly();
+        listeners.useReadOnlyEvents();
     }
 
     /**
@@ -431,6 +431,7 @@ class Store extends DataStore implements StoreResource, 
Aggregate, DirectoryStre
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();                                          // Should 
never fail.
         final Collection<Resource> resources = components;
         if (resources != null) {
             components = null;                                      // Clear 
first in case of failure.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/package-info.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/package-info.java
index 906c57fc3c..c93d40ac46 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/package-info.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/package-info.java
@@ -19,7 +19,7 @@
  * {@link org.apache.sis.storage.DataStore} implementation for a folder 
containing an arbitrary amount of data files.
  *
  * @author  Johann Sorel (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.8
  * @module
  */
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java
index 2099f5e115..70c73afc46 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java
@@ -110,7 +110,7 @@ import org.apache.sis.setup.OptionKey;
  * is known to support only one image per file.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
@@ -239,7 +239,7 @@ public class WorldFileStore extends PRJDataStore {
      */
     WorldFileStore(final FormatFinder format, final boolean readOnly) throws 
DataStoreException, IOException {
         super(format.provider, format.connector);
-        listeners.useWarningEventsOnly();
+        listeners.useReadOnlyEvents();
         identifiers = new HashMap<>();
         suffix = format.suffix;
         if (readOnly || !format.openAsWriter) {
@@ -782,6 +782,7 @@ loop:   for (int convention=0;; convention++) {
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();                  // Should never fail.
         final ImageReader codec = reader;
         reader       = null;
         metadata     = null;
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java
index 18136c3de7..cb7956c0eb 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java
@@ -74,7 +74,7 @@ import org.apache.sis.setup.OptionKey;
  * known to support only one image per file.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.2
  * @module
  */
@@ -494,6 +494,7 @@ writeCoeffs:    for (int i=0;; i++) {
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();                  // Should never fail.
         try {
             final ImageWriter codec = writer;
             writer = null;
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/package-info.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/package-info.java
index d5768d82dd..b98b6eb255 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/package-info.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/package-info.java
@@ -40,7 +40,7 @@
  * then move in a public package with {@code imageio} package name.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see <a href="https://en.wikipedia.org/wiki/World_file";>World File format 
description on Wikipedia</a>
  *
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/Store.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/Store.java
index dc15d878b0..947698ccf8 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/Store.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/Store.java
@@ -47,7 +47,7 @@ import org.apache.sis.util.CharSequences;
  * the file containing WKT definition is the main file, not an auxiliary 
file.</div>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.7
  * @module
  */
@@ -106,7 +106,7 @@ final class Store extends URIDataStore {
         timezone = connector.getOption(OptionKey.TIMEZONE);
         library  = connector.getOption(OptionKey.GEOMETRY_LIBRARY);
         source   = connector.commit(Reader.class, StoreProvider.NAME);
-        listeners.useWarningEventsOnly();
+        listeners.useReadOnlyEvents();
     }
 
     /**
@@ -199,6 +199,7 @@ final class Store extends URIDataStore {
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();              // Should never fail.
         final Reader s = source;
         source = null;                  // Cleared first in case of failure.
         objects.clear();
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/package-info.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/package-info.java
index 21ef05a1cd..db340993be 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/package-info.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/wkt/package-info.java
@@ -19,7 +19,7 @@
  * {@link org.apache.sis.storage.DataStore} implementation for Well Known Text.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   0.7
  * @module
  */
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 d4ec9e17da..d37630bb1e 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
@@ -57,7 +57,7 @@ import org.apache.sis.setup.OptionKey;
  * The above list may be extended in any future SIS version.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   0.4
  * @module
  */
@@ -102,7 +102,7 @@ final class Store extends URIDataStore implements Filter {
             throw new UnsupportedStorageException(super.getLocale(), 
StoreProvider.NAME,
                     connector.getStorage(), 
connector.getOption(OptionKey.OPEN_OPTIONS));
         }
-        listeners.useWarningEventsOnly();
+        listeners.useReadOnlyEvents();
     }
 
     /**
@@ -224,6 +224,7 @@ final class Store extends URIDataStore implements Filter {
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();                      // Should never fail.
         object = null;
         final Closeable in = input(source);
         source = null;                          // Cleared first in case of 
failure.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/xml/package-info.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/xml/package-info.java
index dbd4be634c..2f1691aa47 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/xml/package-info.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/xml/package-info.java
@@ -26,7 +26,7 @@
  * the {@code sis-xmlstore} module extends this package with classes designed 
for use with StAX cursor API.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   0.4
  * @module
  */
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 5731e5f275..5b8f2e8801 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
@@ -35,6 +35,7 @@ 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.CloseEvent;
 import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
 import org.apache.sis.storage.event.StoreListeners;
@@ -526,19 +527,26 @@ public abstract class DataStore implements Resource, 
Localized, AutoCloseable {
 
     /**
      * Closes this data store and releases any underlying resources.
+     * A {@link CloseEvent} is sent to listeners before the data store is 
closed.
      *
      * <h4>Note for implementers</h4>
-     * Data stores having resources to release should <em>not</em> override 
the {@link Object#equals(Object)}
-     * and {@link #hashCode()} methods, since comparisons other than identity 
comparisons may confuse some
-     * cache mechanisms (e.g. they may think that a data store has already 
been closed).
-     * Conversely data stores for which {@code addListener(…)}, {@code 
removeListener(…)} and {@code close()}
-     * methods perform no operation can override {@code equals(…)} and {@code 
hashCode()} if desired.
+     * Implementations should invoke {@code listeners.close()} on their first 
line
+     * for sending notification to all listeners before the data store is 
actually
+     * closed.
      *
      * @throws DataStoreException if an error occurred while closing this data 
store.
+     *
+     * @see StoreListeners#close()
      */
     @Override
     public abstract void close() throws DataStoreException;
 
+    /*
+     * Data stores should not override `Object.equals(Object)` and 
`hashCode()` methods,
+     * because comparisons other than identity comparisons may confuse cache 
mechanisms
+     * (e.g. caches may think that a data store has already been closed).
+     */
+
     /**
      * Returns a string representation of this data store for debugging 
purpose.
      * The content of the string returned by this method may change in any 
future SIS version.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CloseEvent.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CloseEvent.java
new file mode 100644
index 0000000000..4ca37e07a2
--- /dev/null
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CloseEvent.java
@@ -0,0 +1,87 @@
+/*
+ * 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 org.apache.sis.storage.Resource;
+
+
+/**
+ * Notifies listeners that a resource or a data store is being closed and 
should no longer be used.
+ * Resources are automatically considered closed when a parent resource or 
data store is closed.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   1.3
+ * @version 1.3
+ * @module
+ */
+public class CloseEvent extends StoreEvent {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 9121559491613566295L;
+
+    /**
+     * Constructs an event for a resource that has been closed.
+     *
+     * @param  source  the resource which has been closed.
+     * @throws IllegalArgumentException if the given source is null.
+     */
+    public CloseEvent(final Resource source) {
+        super(source);
+    }
+
+
+
+
+    /**
+     * A listener to register on the parent of a resource for closing the 
resource
+     * automatically if the parent is closed.
+     *
+     * @see StoreListeners#closeListener
+     */
+    static final class ParentListener implements StoreListener<CloseEvent> {
+        /**
+         * The parent resource to listen to.
+         */
+        private final Resource parent;
+
+        /**
+         * The listeners to notify.
+         */
+        private final StoreListeners listeners;
+
+        /**
+         * Creates a new listener to be registered on the parent of the given 
set of listeners.
+         *
+         * @param  parent     the parent resource to listen to.
+         * @param  listeners  the child set of listeners.
+         */
+        ParentListener(final Resource parent, final StoreListeners listeners) {
+            this.parent    = parent;
+            this.listeners = listeners;
+        }
+
+        /**
+         * Invoked when a parent resource or data store is closed.
+         */
+        @Override public void eventOccured(final CloseEvent event) {
+            if (event.getSource() == parent) {      // Necessary check for 
avoiding never-ending loop.
+                listeners.close();
+            }
+        }
+    }
+}
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 7b6603f5bb..a7f50abfd1 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
@@ -30,7 +30,7 @@ import org.apache.sis.internal.storage.StoreResource;
  * Those events are created by {@link Resource} implementations and sent to 
all registered listeners.
  *
  * @author  Johann Sorel (Geomatys)
- * @version 1.0
+ * @version 1.3
  *
  * @see StoreListener
  *
@@ -43,6 +43,18 @@ public abstract class StoreEvent extends EventObject 
implements Localized {
      */
     private static final long serialVersionUID = -1725093072445990248L;
 
+    /**
+     * Whether this event has been consumed.
+     * A consumed event is not propagated to other listeners.
+     */
+    private boolean consumed;
+
+    /**
+     * Whether to consume this event after all listeners registered on the 
{@linkplain #getSource() source}
+     * resource but before listeners registered on the parent resource or data 
store.
+     */
+    private boolean consumeLater;
+
     /**
      * Constructs an event that occurred in the given resource.
      *
@@ -88,4 +100,37 @@ public abstract class StoreEvent extends EventObject 
implements Localized {
         }
         return null;
     }
+
+    /**
+     * Indicates whether this event has been consumed by any listener.
+     * A consumed event is not propagated further to other listeners.
+     *
+     * @return {@code true} if this event has been consumed, {@code false} 
otherwise.
+     *
+     * @since 1.3
+     */
+    public final boolean isConsumed() {
+        return consumed;
+    }
+
+    /**
+     * Returns {@code true} if the event propagation can continue with parent 
listeners.
+     */
+    final boolean isConsumedForParent() {
+        return consumed |= consumeLater;
+    }
+
+    /**
+     * Marks this event as consumed. This stops its further propagation to 
other listeners.
+     *
+     * @param  later  {@code true} for consuming now, or {@code false} for 
consuming after all listeners
+     *         registered on the {@linkplain #getSource() source} resource but 
before listeners registered
+     *         on the parent resource or data store.
+     *
+     * @since 1.3
+     */
+    public void consume(final boolean later) {
+        if (later) consumed = true;
+        else consumeLater = true;
+    }
 }
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 1ec498eaf7..924d1f66cd 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
@@ -21,12 +21,12 @@ import java.util.Set;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.Optional;
-import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.logging.LogRecord;
 import java.util.logging.Filter;
+import java.util.concurrent.ExecutionException;
 import java.lang.reflect.Method;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Localized;
@@ -35,6 +35,8 @@ import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.collection.Containers;
+import org.apache.sis.internal.jdk9.JDK9;
+import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.storage.Resources;
 import org.apache.sis.internal.storage.StoreResource;
 import org.apache.sis.internal.storage.StoreUtilities;
@@ -114,8 +116,19 @@ public class StoreListeners implements Localized {
 
     /**
      * Frequently used value for {@link #permittedEventTypes}.
+     *
+     * @see #useReadOnlyEvents()
      */
-    private static final Set<Class<? extends StoreEvent>> WARNING_EVENT_TYPE = 
Collections.singleton(WarningEvent.class);
+    private static final Set<Class<? extends StoreEvent>> READ_EVENT_TYPES =
+                         JDK9.setOf(WarningEvent.class, CloseEvent.class);
+
+    /**
+     * The {@link CloseEvent.ParentListener} registered on {@link #parent}, or 
{@code null} if not yet created.
+     * This is created the first time that a {@link CloseEvent} listener is 
registered on a resource which is
+     * not the root resource. Those listeners are handled in a special way, 
because a close event on the root
+     * should propagate to all children.
+     */
+    private StoreListener<CloseEvent> closeListener;
 
     /**
      * All listeners for a given even type.
@@ -177,8 +190,11 @@ public class StoreListeners implements Localized {
          * It the listener has been registered twice, only the most recent 
registration is removed.
          *
          * <p>It is caller responsibility to perform synchronization.</p>
+         *
+         * @param  listener  the listener to remove.
+         * @return {@code true} if the list of listeners is empty after this 
method call.
          */
-        final void remove(final StoreListener<? super T> listener) {
+        final boolean remove(final StoreListener<? super T> listener) {
             StoreListener<? super T>[] list = listeners;
             if (list != null) {
                 for (int i=list.length; --i >= 0;) {
@@ -193,6 +209,7 @@ public class StoreListeners implements Localized {
                     }
                 }
             }
+            return list == null;
         }
 
         /**
@@ -224,19 +241,30 @@ public class StoreListeners implements Localized {
          * @param  event  the event to send to listeners.
          * @param  done   listeners who were already notified, for avoiding to 
notify them twice.
          * @return the {@code done} map, created when first needed.
+         * @throws ExecutionException if at least one listener failed to 
execute.
          */
-        final Map<StoreListener<?>,Boolean> eventOccured(final T event, 
Map<StoreListener<?>,Boolean> done) {
+        final Map<StoreListener<?>,Boolean> eventOccured(final T event, 
Map<StoreListener<?>,Boolean> done)
+                throws ExecutionException
+        {
+            RuntimeException error = null;
             final StoreListener<? super T>[] list = listeners;
             if (list != null) {
                 if (done == null) {
                     done = new IdentityHashMap<>(list.length);
                 }
                 for (final StoreListener<? super T> listener : list) {
-                    if (done.put(listener, Boolean.TRUE) == null) {
+                    if (event.isConsumed()) break;
+                    if (done.put(listener, Boolean.TRUE) == null) try {
                         listener.eventOccured(event);
+                    } catch (RuntimeException ex) {
+                        if (error == null) error = ex;
+                        else error.addSuppressed(ex);
                     }
                 }
             }
+            if (error != null) {
+                throw new 
ExecutionException(Resources.format(Resources.Keys.ExceptionInListener_1, 
type), error);
+            }
             return done;
         }
     }
@@ -293,13 +321,13 @@ public class StoreListeners implements Localized {
     private static DataStore getDataStore(StoreListeners m) {
         do {
             final Resource source = m.source;
-            if (source instanceof DataStore) {
-                return (DataStore) source;
-            }
             if (source instanceof StoreResource) {
                 final DataStore ds = ((StoreResource) source).getOriginator();
                 if (ds != null) return ds;
             }
+            if (source instanceof DataStore) {      // Fallback if not 
explicitly specified.
+                return (DataStore) source;
+            }
             m = m.parent;
         } while (m != null);
         return null;
@@ -384,23 +412,13 @@ public class StoreListeners implements Localized {
     }
 
     /**
-     * Notifies this {@code StoreListeners} that it will fire only {@link 
WarningEvent}s. This method is a
-     * shortcut for <code>{@linkplain setUsableEventTypes 
setUsableEventTypes}(WarningEvent.class)}</code>,
-     * provided because frequently used by read-only data store 
implementations.
-     *
-     * @see #setUsableEventTypes(Class...)
-     * @see WarningEvent
+     * @deprecated Renamed {@link #useReadOnlyEvents()}.
      *
      * @since 1.2
      */
-    public synchronized void useWarningEventsOnly() {
-        final Set<Class<? extends StoreEvent>> current = permittedEventTypes;
-        if (current == null) {
-            permittedEventTypes = WARNING_EVENT_TYPE;
-        } else if (!WARNING_EVENT_TYPE.equals(current)) {
-            throw illegalEventType(WarningEvent.class);
-        }
-        ForType.removeUnreachables(listeners, WARNING_EVENT_TYPE);
+    @Deprecated
+    public void useWarningEventsOnly() {
+        useReadOnlyEvents();
     }
 
     /**
@@ -573,20 +591,43 @@ public class StoreListeners implements Localized {
         }
     }
 
+    /**
+     * Invoked if an error occurred in a least one listener during the 
propagation of an event.
+     * The {@linkplain ExecutionException#getCause() cause} of the exception 
is a {@link RuntimeException}.
+     * If exceptions occurred in more than one listener, all exceptions after 
the first one are specified
+     * as {@linkplain ExecutionException#getSuppressed() suppressed 
exceptions} of the cause.
+     *
+     * <p>This method should not delegate to {@link #warning(Exception)} 
because the error is not with the
+     * data store itself. Furthermore the exception may have occurred during 
{@code warning(…)} execution,
+     * in which case the exception is a kind of "warning about warning 
report".</p>
+     *
+     * @param  method  name of the method invoking this method.
+     * @param  error   the exception that occurred.
+     */
+    private static void canNotNotify(final String method, final 
ExecutionException error) {
+        Logging.unexpectedException(Logger.getLogger(Modules.STORAGE), 
StoreListeners.class, method, error);
+    }
+
     /**
      * Sends the given event to all listeners registered for the given type or 
for a super-type.
      * This method first notifies the listeners registered in this {@code 
StoreListeners}, then
      * notifies listeners registered in parent {@code StoreListeners}s. Each 
listener will be
      * notified only once even if it has been registered many times.
      *
+     * <p>If one or many {@link StoreListener#eventOccured(StoreEvent)} 
implemetations throw a
+     * {@link RuntimeException}, those exceptions will be collected and 
reported in a single
+     * {@linkplain Logging#unexpectedException(Logger, Class, String, 
Throwable) log record}.
+     * Runtime exceptions in listeners do not cause this method to fail.</p>
+     *
      * @param  <T>        compile-time value of the {@code eventType} argument.
      * @param  event      the event to fire.
-     * @param  eventType  the type of events to be fired.
+     * @param  eventType  the type of the event to be fired.
      * @return {@code true} if the event has been sent to at least one 
listener.
      * @throws IllegalArgumentException if the given event type is not one of 
the types of events
      *         that this {@code StoreListeners} can fire.
+     *
+     * @see #close()
      */
-    @SuppressWarnings("unchecked")
     public <T extends StoreEvent> boolean fire(final T event, final Class<T> 
eventType) {
         ArgumentChecks.ensureNonNull("event", event);
         ArgumentChecks.ensureNonNull("eventType", eventType);
@@ -594,16 +635,48 @@ public class StoreListeners implements Localized {
         if (permittedEventTypes != null && 
!permittedEventTypes.contains(eventType)) {
             throw illegalEventType(eventType);
         }
+        try {
+            return fire(this, event, eventType);
+        } catch (ExecutionException ex) {
+            canNotNotify("fire", ex);
+            return true;
+        }
+    }
+
+    /**
+     * Sends the given event to all listeners registered in the given set of 
listeners and its parent.
+     * This method does not perform any argument validation; they must be done 
by the caller.
+     *
+     * <p>This method does not need (and should not) be synchronized.</p>
+     *
+     * @param  <T>        compile-time value of the {@code eventType} argument.
+     * @param  m          the set of listeners that may be interested in the 
event.
+     * @param  event      the event to fire.
+     * @param  eventType  the type of the event to be fired.
+     * @return {@code true} if the event has been sent to at least one 
listener.
+     * @throws ExecutionException
+     */
+    @SuppressWarnings("unchecked")
+    private static <T extends StoreEvent> boolean fire(StoreListeners m, final 
T event, final Class<T> eventType)
+            throws ExecutionException
+    {
         Map<StoreListener<?>,Boolean> done = null;
-        StoreListeners m = this;
+        ExecutionException error = null;
         do {
             for (ForType<?> e = m.listeners; e != null; e = e.next) {
-                if (e.type.isAssignableFrom(eventType)) {
+                if (e.type.isAssignableFrom(eventType)) try {
                     done = ((ForType<? super T>) e).eventOccured(event, done);
+                } catch (ExecutionException ex) {
+                    if (error == null) error = ex;
+                    else error.getCause().addSuppressed(ex.getCause());
                 }
+                if (event.isConsumedForParent()) break;
             }
             m = m.parent;
         } while (m != null);
+        if (error != null) {
+            throw error;
+        }
         return (done != null) && !done.isEmpty();
     }
 
@@ -672,6 +745,16 @@ public class StoreListeners implements Localized {
                 listeners = ce;
             }
             ce.add(listener);
+            /*
+             * If we are adding a listener for `CloseEvent`, we may need (as a 
special case)
+             * to register a listener to the parent for propagating the close 
events.
+             */
+            if (parent != null) {
+                if (closeListener == null && 
CloseEvent.class.isAssignableFrom(eventType)) {
+                    closeListener = new 
CloseEvent.ParentListener(parent.source, this);
+                    parent.addListener(CloseEvent.class, closeListener);
+                }
+            }
         }
     }
 
@@ -703,7 +786,17 @@ public class StoreListeners implements Localized {
         ArgumentChecks.ensureNonNull("eventType", eventType);
         for (ForType<?> e = listeners; e != null; e = e.next) {
             if (e.type.equals(eventType)) {
-                ((ForType<T>) e).remove(listener);
+                if (((ForType<T>) e).remove(listener) && parent != null) {
+                    /*
+                     * If the list of listeners become empty and if the event 
type was `CloseEvent`,
+                     * cleanup the parent list of listeners too. We do a 
special case for close events
+                     * because closing a parent data store implicitly closes 
the child resources.
+                     */
+                    if (closeListener != null && 
CloseEvent.class.isAssignableFrom(eventType)) {
+                        parent.removeListener(CloseEvent.class, closeListener);
+                        closeListener = null;
+                    }
+                }
                 break;
             }
         }
@@ -760,7 +853,7 @@ public class StoreListeners implements Localized {
      * @param  permitted  type of events that are permitted. Permitted 
sub-types shall be explicitly enumerated as well.
      * @throws IllegalArgumentException if one of the given types was not 
permitted before invocation of this method.
      *
-     * @see #useWarningEventsOnly()
+     * @see #useReadOnlyEvents()
      *
      * @since 1.2
      */
@@ -776,7 +869,59 @@ public class StoreListeners implements Localized {
                 throw illegalEventType(type);
             }
         }
-        permittedEventTypes = WARNING_EVENT_TYPE.equals(types) ? 
WARNING_EVENT_TYPE : CollectionsExt.compact(types);
+        permittedEventTypes = READ_EVENT_TYPES.equals(types) ? 
READ_EVENT_TYPES : CollectionsExt.compact(types);
         ForType.removeUnreachables(listeners, types);
     }
+
+    /**
+     * Notifies this {@code StoreListeners} that it will fire only {@link 
WarningEvent}s and {@link CloseEvent}.
+     * This method is a shortcut for <code>{@linkplain setUsableEventTypes 
setUsableEventTypes}(WarningEvent.class,
+     * CloseEvent.class)}</code>, provided because frequently used by 
read-only data store implementations.
+     *
+     * <p>Declaring a root resource (typically a {@link DataStore}) as 
read-only implies that all children
+     * (e.g. {@linkplain org.apache.sis.storage.Aggregate#components() 
components of an aggregate})
+     * are also read-only.</p>
+     *
+     * @see #setUsableEventTypes(Class...)
+     * @see WarningEvent
+     * @see CloseEvent
+     *
+     * @since 1.3
+     */
+    public synchronized void useReadOnlyEvents() {
+        final Set<Class<? extends StoreEvent>> current = permittedEventTypes;
+        if (current == null) {
+            permittedEventTypes = READ_EVENT_TYPES;
+        } else if (!READ_EVENT_TYPES.equals(current)) {
+            throw illegalEventType(WarningEvent.class);
+        }
+        ForType.removeUnreachables(listeners, READ_EVENT_TYPES);
+    }
+
+    /**
+     * Sends a {@link CloseEvent} to all listeners registered for that kind of 
event,
+     * then discards listeners in this instance (but not in parents).
+     * Because listeners are discarded, invoking this method many times
+     * on the same instance have no effect after the first invocation.
+     *
+     * <p>If one or many {@link StoreListener#eventOccured(StoreEvent)} 
implementations throw
+     * a {@link RuntimeException}, those exceptions will be collected and 
reported in a single
+     * {@linkplain Logging#unexpectedException(Logger, Class, String, 
Throwable) log record}.
+     * Runtime exceptions in listeners do not cause this method to fail.</p>
+     *
+     * @see #fire(StoreEvent, Class)
+     * @see DataStore#close()
+     *
+     * @since 1.3
+     */
+    public void close() {
+        try {
+            fire(this, new CloseEvent(source), CloseEvent.class);
+        } catch (ExecutionException ex) {
+            canNotNotify("close", ex);
+        }
+        closeListener = null;
+        listeners     = null;       // Volatile field should be last.
+        // Do not remove parent listeners; maybe parent will be closed next.
+    }
 }
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 a51a921ced..ea41037b92 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
@@ -26,7 +26,7 @@ import org.apache.sis.storage.event.StoreListeners;
  * A dummy data store
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -70,10 +70,11 @@ public final strictfp class DataStoreMock extends DataStore 
{
     }
 
     /**
-     * Do nothing.
+     * Notifies listeners if any. Otherwise does nothing.
      */
     @Override
     public void close() {
+        listeners.close();
     }
 
     /**
@@ -95,4 +96,14 @@ public final strictfp class DataStoreMock extends DataStore {
     public void simulateWarning(String message) {
         listeners.warning(message);
     }
+
+    /**
+     * Returns a new dummy child having this data store as a parent.
+     *
+     * @return a dummy child.
+     */
+    public Resource newChild() {
+        return new AbstractResource(listeners, false) {
+        };
+    }
 }
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
index f4af5537be..f353231094 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java
@@ -18,6 +18,7 @@ package org.apache.sis.storage.event;
 
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
+import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.DataStoreMock;
 import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.test.TestCase;
@@ -30,7 +31,7 @@ import static org.junit.Assert.*;
  * Tests the {@link StoreListeners} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  * @since   1.0
  * @module
  */
@@ -128,4 +129,36 @@ public final strictfp class StoreListenersTest extends 
TestCase implements Store
         assertEquals("simulateWarning", warning.getSourceMethodName());
         assertEquals("The message", warning.getMessage());
     }
+
+    /**
+     * Tests {@link StoreListeners#close()}. This event is handled in a 
special way:
+     * close event on the parent resource causes the same event to be fired 
for all
+     * children.
+     */
+    @Test
+    public void testClose() {
+        final Resource resource = store.newChild();
+        class Listener implements StoreListener<CloseEvent> {
+            /** Whether the resource has been closed. */boolean isClosed;
+
+            @Override public void eventOccured(CloseEvent event) {
+                assertSame(resource, event.getSource());
+                isClosed = true;
+            }
+        }
+        final Listener listener = new Listener();
+        /*
+         * First, register and unregister. No event should be received.
+         */
+        resource.addListener(CloseEvent.class, listener);
+        resource.removeListener(CloseEvent.class, listener);
+        store.close();
+        assertFalse(listener.isClosed);
+        /*
+         * Register and close. Now the event should be received.
+         */
+        resource.addListener(CloseEvent.class, listener);
+        store.close();
+        assertTrue(listener.isClosed);
+    }
 }
diff --git 
a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java
 
b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java
index 451059e7b3..2e353f687e 100644
--- 
a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java
+++ 
b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/Store.java
@@ -55,7 +55,7 @@ import org.opengis.feature.FeatureType;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -102,7 +102,7 @@ public final class Store extends StaxDataStore implements 
FeatureSet {
         } catch (FactoryException e) {
             throw new DataStoreException(e);
         }
-        listeners.useWarningEventsOnly();
+        listeners.useReadOnlyEvents();
     }
 
     /**
@@ -284,6 +284,7 @@ public final class Store extends StaxDataStore implements 
FeatureSet {
      */
     @Override
     public synchronized void close() throws DataStoreException {
+        listeners.close();                  // Should never fail.
         final Reader r = reader;
         reader = null;
         if (r != null) try {
diff --git 
a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/package-info.java
 
b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/package-info.java
index 7bd8f4e1f4..0ef4f26ff1 100644
--- 
a/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/package-info.java
+++ 
b/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/storage/gpx/package-info.java
@@ -56,7 +56,7 @@
  * </ul>
  *
  * @author  Johann Sorel (Geomatys)
- * @version 0.8
+ * @version 1.3
  *
  * @see <a href="https://en.wikipedia.org/wiki/GPS_Exchange_Format";>GPS 
Exchange Format on Wikipedia</a>
  * @see <a href="http://www.topografix.com/GPX/1/1/";>GPX 1.1 Schema 
Documentation</a>
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 22f7a27b1c..ce4fe09e2f 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
@@ -587,6 +587,10 @@ public abstract class StaxDataStore extends URIDataStore {
      * Closes the input or output stream and releases any resources used by 
this XML data store.
      * This data store can not be used anymore after this method has been 
invoked.
      *
+     * <h4>Note for implementers</h4>
+     * Implementations should invoke {@code listeners.close()} on their first 
line
+     * before to clear their resources and to invoke {@code super.close()}.
+     *
      * @throws DataStoreException if an error occurred while closing the input 
or output stream.
      */
     @Override

Reply via email to