sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Review of the addition of Resource.getIdentifier() method: - Restore import statements in their original order for easier merges between branches. - Add "throws DataStoreException" in method signature. - Closer relationship with DataStore.findResource(String). - Review implementations computing the value from available informations. - Provide scope information when applicable.
Date Sat, 13 Oct 2018 13:27:44 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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new e003c32  Review of the addition of Resource.getIdentifier() method: - Restore import statements in their original order for easier merges between branches. - Add "throws DataStoreException" in method signature. - Closer relationship with DataStore.findResource(String). - Review implementations computing the value from available informations. - Provide scope information when applicable.
e003c32 is described below

commit e003c327ff8082467a456ac167965b7a0d60d07e
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sat Oct 13 15:21:28 2018 +0200

    Review of the addition of Resource.getIdentifier() method:
    - Restore import statements in their original order for easier merges between branches.
    - Add "throws DataStoreException" in method signature.
    - Closer relationship with DataStore.findResource(String).
    - Review implementations computing the value from available informations.
    - Provide scope information when applicable.
---
 .../main/java/org/apache/sis/util/iso/Names.java   |   2 +-
 .../org/apache/sis/internal/util/Citations.java    |   2 +-
 .../org/apache/sis/internal/util/Numerics.java     |  21 +++
 .../org/apache/sis/internal/util/NumericsTest.java |  11 ++
 .../sis/storage/earthobservation/LandsatStore.java |  85 ++++-----
 .../apache/sis/storage/geotiff/GeoTiffStore.java   | 135 ++++++++++----
 .../sis/storage/geotiff/ImageFileDirectory.java    |  39 ++--
 .../org/apache/sis/storage/geotiff/Reader.java     |  20 ++-
 .../org/apache/sis/internal/netcdf/Decoder.java    |  24 ++-
 .../sis/internal/netcdf/impl/ChannelDecoder.java   |   4 +-
 .../sis/internal/netcdf/impl/FeaturesInfo.java     |  56 +++---
 .../sis/internal/netcdf/ucar/DecoderWrapper.java   |   4 +-
 .../sis/internal/netcdf/ucar/FeaturesWrapper.java  |  12 +-
 .../apache/sis/storage/netcdf/MetadataReader.java  |  14 +-
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |  56 ++++--
 .../apache/sis/internal/sql/feature/Database.java  |   6 +-
 .../org/apache/sis/internal/sql/feature/Table.java |  64 +++----
 .../java/org/apache/sis/storage/sql/SQLStore.java  |  60 +++++--
 .../sis/internal/storage/AbstractResource.java     |  81 +++++++--
 .../sis/internal/storage/JoinFeatureSet.java       |  14 +-
 .../sis/internal/storage/MemoryFeatureSet.java     |   6 +-
 .../sis/internal/storage/StoreUtilities.java       |  46 +++--
 .../org/apache/sis/internal/storage/csv/Store.java |  93 +++++-----
 .../apache/sis/internal/storage/folder/Store.java  | 115 +++++++-----
 .../sis/internal/storage/folder/WritableStore.java |   8 +-
 .../sis/internal/storage/query/FeatureSubset.java  |  14 +-
 .../org/apache/sis/internal/storage/wkt/Store.java |  35 ++--
 .../org/apache/sis/internal/storage/xml/Store.java |  65 ++-----
 .../java/org/apache/sis/storage/DataStore.java     | 200 +++++++++++++--------
 .../apache/sis/storage/IllegalNameException.java   |   3 +
 .../main/java/org/apache/sis/storage/Resource.java |  20 ++-
 .../java/org/apache/sis/storage/DataStoreMock.java |  14 +-
 .../org/apache/sis/internal/storage/gpx/Store.java |  51 +++---
 33 files changed, 831 insertions(+), 549 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java
index 78f8acb..7d3f083 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/util/iso/Names.java
@@ -97,7 +97,7 @@ public final class Names extends Static {
      * @param  factory    the factory to use for creating the namespace.
      * @param  namespace  the namespace string, taken as a whole (not parsed).
      * @param  separator  the separator between the namespace and the local part, or {@code null} for the default.
-     * @return the namespace object.
+     * @return the namespace object, or {@code null} if the given {@code namespace} was null or empty.
      */
     private static NameSpace createNameSpace(final NameFactory factory, final CharSequence namespace, final String separator) {
         if (namespace == null || namespace.length() == 0) {
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Citations.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Citations.java
index 4ca958c..b756bd9 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Citations.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Citations.java
@@ -501,7 +501,7 @@ public final class Citations extends Static {
              * we will avoid the StringBuilder creation in the vast majority of times.
              *
              * Note that 'µ' and its friends are not ignorable, so we do not remove them.
-             * This method is "getUnicodeIdentifier", not "getXmlIdentifier".
+             * This method is aimed for "getUnicodeIdentifier", not "getXmlIdentifier".
              */
             final int length = identifier.length();
             for (int i=0; i<length;) {
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
index 876a9ba..5e50833 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
@@ -308,6 +308,27 @@ public final class Numerics extends Static {
     }
 
     /**
+     * Returns {@code true} if the following text is non-null, non-empty
+     * and contains only digits from {@code '0'} to {@code '9'} inclusive.
+     *
+     * @param  text  the text to verify, or {@code null}.
+     * @return {@code true} if the given text is an unsigned integer.
+     */
+    public static boolean isUnsignedInteger(final String text) {
+        if (text != null) {
+            final int length = text.length();
+            if (length != 0) {
+                char c;
+                int i = 0;
+                while ((c = text.charAt(i)) >= '0' && c <= '9') {
+                    if (++i >= length) return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
      * Converts an unsigned {@code long} to a {@code float} value.
      *
      * @param  value  the unsigned {@code long} value.
diff --git a/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java b/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java
index 017a753..bf2889f 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java
@@ -78,6 +78,17 @@ public final strictfp class NumericsTest extends TestCase {
     }
 
     /**
+     * Tests {@link Numerics#isUnsignedInteger(String)}.
+     */
+    @Test
+    public void testIsUnsignedInteger() {
+        assertFalse(Numerics.isUnsignedInteger(null));
+        assertFalse(Numerics.isUnsignedInteger(""));
+        assertTrue (Numerics.isUnsignedInteger("12345"));
+        assertFalse(Numerics.isUnsignedInteger("123A5"));
+    }
+
+    /**
      * Tests the {@link Numerics#epsilonEqual(double, double, ComparisonMode)} method.
      */
     @Test
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 f59594a..46b94ce 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
@@ -16,24 +16,24 @@
  */
 package org.apache.sis.storage.earthobservation;
 
+import java.io.Reader;
 import java.io.BufferedReader;
-import java.io.IOException;
 import java.io.LineNumberReader;
-import java.io.Reader;
+import java.io.IOException;
 import java.net.URI;
-import org.apache.sis.internal.storage.URIDataStore;
-import org.apache.sis.setup.OptionKey;
+import org.opengis.metadata.Metadata;
+import org.opengis.util.GenericName;
+import org.opengis.util.FactoryException;
+import org.opengis.parameter.ParameterValueGroup;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreReferencingException;
-import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.UnsupportedStorageException;
+import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.event.ChangeEvent;
 import org.apache.sis.storage.event.ChangeListener;
-import org.opengis.metadata.Metadata;
-import org.opengis.parameter.ParameterValueGroup;
-import org.opengis.util.FactoryException;
-import org.opengis.util.GenericName;
+import org.apache.sis.internal.storage.URIDataStore;
+import org.apache.sis.setup.OptionKey;
 
 
 /**
@@ -86,6 +86,11 @@ public class LandsatStore extends DataStore {
     private Metadata metadata;
 
     /**
+     * The identifier, cached when first requested.
+     */
+    private GenericName identifier;
+
+    /**
      * Creates a new Landsat store from the given file, URL, stream or character reader.
      * This constructor invokes {@link StorageConnector#closeAllExcept(Object)},
      * keeping open only the needed resource.
@@ -106,14 +111,37 @@ public class LandsatStore extends DataStore {
     }
 
     /**
-     * Returns a null value.
-     * TODO : implement this method when Coverage API will be finished.
+     * Returns the parameters used to open this Landsat data store.
+     * If non-null, the parameters are described by {@link LandsatStoreProvider#getOpenParameters()} and contains at
+     * least a parameter named {@value org.apache.sis.storage.DataStoreProvider#LOCATION} with a {@link URI} value.
+     * This method may return {@code null} if the storage input can not be described by a URI
+     * (for example a Landsat file reading directly from a {@link java.nio.channels.ReadableByteChannel}).
+     *
+     * @return parameters used for opening this data store, or {@code null} if not available.
      *
-     * @return null.
+     * @since 0.8
      */
     @Override
-    public GenericName getIdentifier() {
-        return null;
+    public ParameterValueGroup getOpenParameters() {
+        return URIDataStore.parameters(provider, location);
+    }
+
+    /**
+     * Returns the value associated to {@code LANDSAT_SCENE_ID} in the Landsat metadata file.
+     * This value is fetched from
+     * <code>{@linkplain #getMetadata()}/​identificationInfo/​citation/​identifier</code>.
+     *
+     * @return the identifier fetched from metadata, or {@code null} if none.
+     * @throws DataStoreException if an error occurred while reading the metadata.
+     *
+     * @since 1.0
+     */
+    @Override
+    public synchronized GenericName getIdentifier() throws DataStoreException {
+        if (identifier == null) {
+            identifier = super.getIdentifier();
+        }
+        return identifier;
     }
 
     /**
@@ -122,7 +150,7 @@ public class LandsatStore extends DataStore {
      * data quality, usage constraints and more.
      *
      * @return information about the dataset.
-     * @throws DataStoreException if an error occurred while reading the data.
+     * @throws DataStoreException if an error occurred while reading the metadata.
      */
     @Override
     public synchronized Metadata getMetadata() throws DataStoreException {
@@ -142,22 +170,6 @@ public class LandsatStore extends DataStore {
     }
 
     /**
-     * Returns the parameters used to open this Landsat data store.
-     * If non-null, the parameters are described by {@link LandsatStoreProvider#getOpenParameters()} and contains at
-     * least a parameter named {@value org.apache.sis.storage.DataStoreProvider#LOCATION} with a {@link URI} value.
-     * This method may return {@code null} if the storage input can not be described by a URI
-     * (for example a Landsat file reading directly from a {@link java.nio.channels.ReadableByteChannel}).
-     *
-     * @return parameters used for opening this data store, or {@code null} if not available.
-     *
-     * @since 0.8
-     */
-    @Override
-    public ParameterValueGroup getOpenParameters() {
-        return URIDataStore.parameters(provider, location);
-    }
-
-    /**
      * Ignored in current implementation, since this resource produces no events.
      *
      * @param  <T>        {@inheritDoc}
@@ -188,15 +200,4 @@ public class LandsatStore extends DataStore {
     public synchronized void close() throws DataStoreException {
         metadata = null;
     }
-
-    /**
-     * Returns a string representation of this Landsat store for debugging purpose.
-     * The content of the string returned by this method may change in any future SIS version.
-     *
-     * @return a string representation of this datastore for debugging purpose.
-     */
-    @Override
-    public String toString() {
-        return getClass().getSimpleName() + '[' + getDisplayName() + ']';
-    }
 }
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 ade5c9f..cd8f677 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
@@ -16,34 +16,40 @@
  */
 package org.apache.sis.storage.geotiff;
 
+import java.util.Locale;
 import java.io.IOException;
 import java.net.URI;
 import java.nio.charset.Charset;
+import java.util.logging.LogRecord;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.StandardOpenOption;
-import java.util.Locale;
-import java.util.logging.LogRecord;
-import org.apache.sis.internal.storage.MetadataBuilder;
-import org.apache.sis.internal.storage.URIDataStore;
-import org.apache.sis.internal.storage.io.ChannelDataInput;
-import org.apache.sis.internal.storage.io.IOUtilities;
-import org.apache.sis.internal.util.Constants;
-import org.apache.sis.metadata.sql.MetadataStoreException;
+import org.opengis.util.NameSpace;
+import org.opengis.util.NameFactory;
+import org.opengis.util.GenericName;
+import org.opengis.util.FactoryException;
+import org.opengis.metadata.Metadata;
+import org.opengis.metadata.maintenance.ScopeCode;
+import org.opengis.parameter.ParameterValueGroup;
 import org.apache.sis.setup.OptionKey;
+import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.DataStore;
-import org.apache.sis.storage.DataStoreClosedException;
-import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.UnsupportedStorageException;
+import org.apache.sis.storage.DataStoreClosedException;
+import org.apache.sis.storage.IllegalNameException;
 import org.apache.sis.storage.event.ChangeEvent;
 import org.apache.sis.storage.event.ChangeListener;
+import org.apache.sis.internal.storage.io.ChannelDataInput;
+import org.apache.sis.internal.storage.io.IOUtilities;
+import org.apache.sis.internal.storage.MetadataBuilder;
+import org.apache.sis.internal.storage.StoreUtilities;
+import org.apache.sis.internal.storage.URIDataStore;
+import org.apache.sis.internal.util.Constants;
+import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.util.resources.Errors;
-import org.opengis.metadata.Metadata;
-import org.opengis.metadata.maintenance.ScopeCode;
-import org.opengis.parameter.ParameterValueGroup;
-import org.opengis.util.FactoryException;
-import org.opengis.util.GenericName;
 
 
 /**
@@ -70,10 +76,19 @@ public class GeoTiffStore extends DataStore {
 
     /**
      * The {@link GeoTiffStoreProvider#LOCATION} parameter value, or {@code null} if none.
+     * This is used for information purpose only, not for actual reading operations.
+     *
+     * @see #getOpenParameters()
      */
     private final URI location;
 
     /**
+     * The data store identifier created from the filename, or {@code null} if none.
+     * Defined as a namespace for use as the scope of children resources (the images).
+     */
+    final NameSpace identifier;
+
+    /**
      * The metadata, or {@code null} if not yet created.
      *
      * @see #getMetadata()
@@ -105,17 +120,46 @@ public class GeoTiffStore extends DataStore {
         } catch (IOException e) {
             throw new DataStoreException(e);
         }
+        if (location != null) {
+            final NameFactory f = reader.nameFactory;
+            String filename = IOUtilities.filenameWithoutExtension(input.filename);
+            if (Numerics.isUnsignedInteger(filename)) filename += ".tiff";
+            identifier = f.createNameSpace(f.createLocalName(null, filename), null);
+        } else {
+            // Location not convertible to URI. The string representation is probably a class name, which is not useful.
+            identifier = null;
+        }
+    }
+
+    /**
+     * 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.
+     * This method may return {@code null} if the storage input can not be described by a URI
+     * (for example a GeoTIFF file reading directly from a {@link java.nio.channels.ReadableByteChannel}).
+     *
+     * @return parameters used for opening this data store, or {@code null} if not available.
+     *
+     * @since 0.8
+     */
+    @Override
+    public ParameterValueGroup getOpenParameters() {
+        return URIDataStore.parameters(provider, location);
     }
 
     /**
-     * Returns a null value.
-     * TODO : implement this method when Coverage API will be finished.
+     * Returns an identifier constructed from the name of the TIFF file.
+     * An identifier is available only if the storage input specified at construction time was something convertible to
+     * {@link java.net.URI}, for example an {@link java.net.URL}, {@link java.io.File} or {@link java.nio.file.Path}.
      *
-     * @return null.
+     * @return the identifier derived from the filename, or {@code null} if none.
+     * @throws DataStoreException if an error occurred while fetching the identifier.
+     *
+     * @since 1.0
      */
     @Override
-    public GenericName getIdentifier() {
-        return null;
+    public GenericName getIdentifier() throws DataStoreException {
+        return (identifier != null) ? identifier.name() : null;
     }
 
     /**
@@ -147,7 +191,7 @@ public class GeoTiffStore extends DataStore {
                     dir.completeMetadata(builder, locale);
                 }
             } catch (IOException e) {
-                throw new DataStoreException(errors().getString(Errors.Keys.CanNotRead_1, reader.input.filename), e);
+                throw errorIO(e);
             } catch (FactoryException | ArithmeticException e) {
                 throw new DataStoreContentException(getLocale(), Constants.GEOTIFF, reader.input.filename, null).initCause(e);
             }
@@ -157,8 +201,9 @@ public class GeoTiffStore extends DataStore {
              * file did not specified any ImageDescription tag, then we will had the filename as a title instead than an
              * identifier because the title is mandatory in ISO 19115 metadata.
              */
-            if (location != null) {
-                builder.addTitleOrIdentifier(IOUtilities.filenameWithoutExtension(reader.input.filename), MetadataBuilder.Scope.ALL);
+            final GenericName id = getIdentifier();
+            if (id != null) {
+                builder.addTitleOrIdentifier(id.toString(), MetadataBuilder.Scope.ALL);
             }
             metadata = builder.build(true);
         }
@@ -166,19 +211,10 @@ public class GeoTiffStore extends DataStore {
     }
 
     /**
-     * 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.
-     * This method may return {@code null} if the storage input can not be described by a URI
-     * (for example a GeoTIFF file reading directly from a {@link java.nio.channels.ReadableByteChannel}).
-     *
-     * @return parameters used for opening this data store, or {@code null} if not available.
-     *
-     * @since 0.8
+     * Returns the exception to throw when an I/O error occurred.
      */
-    @Override
-    public ParameterValueGroup getOpenParameters() {
-        return URIDataStore.parameters(provider, location);
+    private DataStoreException errorIO(final IOException e) {
+        return new DataStoreException(errors().getString(Errors.Keys.CanNotRead_1, reader.input.filename), e);
     }
 
     /**
@@ -193,6 +229,33 @@ public class GeoTiffStore extends DataStore {
     }
 
     /**
+     * Returns the image at the given index. Images numbering starts at 1.
+     *
+     * @param  sequence  string representation of the image index, starting at 1.
+     * @return image at the given index.
+     * @throws DataStoreException if the requested image can not be obtained.
+     */
+    @Override
+    public Resource findResource(final String sequence) throws DataStoreException {
+        Exception cause;
+        int index;
+        try {
+            index = Integer.parseInt(sequence);
+            cause = null;
+        } catch (NumberFormatException e) {
+            index = 0;
+            cause = e;
+        }
+        if (index > 0) try {
+            ImageFileDirectory image = reader().getImageFileDirectory(index - 1);
+            if (image != null) return image;
+        } catch (IOException e) {
+            throw errorIO(e);
+        }
+        throw new IllegalNameException(StoreUtilities.resourceNotFound(this, sequence), cause);
+    }
+
+    /**
      * Ignored in current implementation, since this resource produces no events.
      *
      * @param  <T>        {@inheritDoc}
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 2e991c3..16e57b2 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
@@ -17,29 +17,29 @@
 package org.apache.sis.storage.geotiff;
 
 import java.io.IOException;
-import java.nio.charset.Charset;
 import java.text.ParseException;
 import java.util.Arrays;
 import java.util.Locale;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
+import java.nio.charset.Charset;
 import javax.measure.Unit;
 import javax.measure.quantity.Length;
-import org.apache.sis.coverage.grid.GridExtent;
-import org.apache.sis.coverage.grid.GridGeometry;
+import org.opengis.metadata.Metadata;
+import org.opengis.metadata.citation.DateType;
+import org.opengis.util.FactoryException;
+import org.opengis.util.GenericName;
 import org.apache.sis.internal.geotiff.Resources;
 import org.apache.sis.internal.storage.AbstractResource;
 import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
-import org.apache.sis.math.Vector;
-import org.apache.sis.measure.Units;
-import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.GridCoverageResource;
-import org.opengis.metadata.Metadata;
-import org.opengis.metadata.citation.DateType;
-import org.opengis.util.FactoryException;
-import org.opengis.util.GenericName;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.math.Vector;
+import org.apache.sis.measure.Units;
 
 
 /**
@@ -78,6 +78,14 @@ final class ImageFileDirectory extends AbstractResource implements GridCoverageR
     private final Reader reader;
 
     /**
+     * The identifier as a sequence number in the namespace of the {@link GeoTiffStore}.
+     * The first image has the sequence number "1".
+     *
+     * @see #getIdentifier()
+     */
+    private final GenericName identifier;
+
+    /**
      * {@code true} if this {@code ImageFileDirectory} has not yet read all deferred entries.
      * When this flag is {@code true}, the {@code ImageFileDirectory} is not yet ready for use.
      */
@@ -339,10 +347,12 @@ final class ImageFileDirectory extends AbstractResource implements GridCoverageR
      * Creates a new image file directory.
      *
      * @param reader  information about the input stream to read, the metadata and the character encoding.
+     * @param index   the image index as a sequence number starting with 0 for the first image.
      */
-    ImageFileDirectory(final Reader reader) {
+    ImageFileDirectory(final Reader reader, final int index) {
         super(reader.owner);
         this.reader = reader;
+        identifier = reader.nameFactory.createLocalName(reader.owner.identifier, String.valueOf(index + 1));
     }
 
     /**
@@ -367,13 +377,14 @@ final class ImageFileDirectory extends AbstractResource implements GridCoverageR
     }
 
     /**
-     * Datastore root resource has no identifier.
+     * Returns the identifier as a sequence number in the namespace of the {@link GeoTiffStore}.
+     * The first image has the sequence number "1".
      *
-     * @return null
+     * @see #getMetadata()
      */
     @Override
     public GenericName getIdentifier() {
-        return null;
+        return identifier;
     }
 
     /**
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
index 1385a75..8302217 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
@@ -25,11 +25,13 @@ import java.util.Iterator;
 import java.io.IOException;
 import java.nio.ByteOrder;
 import java.text.ParseException;
+import org.opengis.util.NameFactory;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.geotiff.Resources;
+import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.util.resources.Errors;
 
 
@@ -48,7 +50,7 @@ import org.apache.sis.util.resources.Errors;
  * @author  Alexis Manin (Geomatys)
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -124,6 +126,11 @@ final class Reader extends GeoTIFF {
     final MetadataBuilder metadata;
 
     /**
+     * The factory to use for creating image identifiers.
+     */
+    final NameFactory nameFactory;
+
+    /**
      * Creates a new GeoTIFF reader which will read data from the given input.
      * The input must be at the beginning of the GeoTIFF file.
      *
@@ -132,10 +139,11 @@ final class Reader extends GeoTIFF {
      */
     Reader(final GeoTiffStore owner, final ChannelDataInput input) throws IOException, DataStoreException {
         super(owner);
-        this.input    = input;
-        this.origin   = input.getStreamPosition();
-        this.metadata = new MetadataBuilder();
-        this.doneIFD  = new HashSet<>();
+        this.input       = input;
+        this.origin      = input.getStreamPosition();
+        this.metadata    = new MetadataBuilder();
+        this.doneIFD     = new HashSet<>();
+        this.nameFactory = DefaultFactories.forBuildin(NameFactory.class);
         /*
          * A TIFF file begins with either "II" (0x4949) or "MM" (0x4D4D) characters.
          * Those characters identify the byte order. Note that we do not need to care
@@ -252,7 +260,7 @@ final class Reader extends GeoTIFF {
              * to get the pointer to the next IFD.
              */
             final int offsetSize = Integer.BYTES << intSizeExpansion;
-            final ImageFileDirectory dir = new ImageFileDirectory(this);
+            final ImageFileDirectory dir = new ImageFileDirectory(this, index);
             for (long remaining = readUnsignedShort(); --remaining >= 0;) {
                 /*
                  * Each entry in the Image File Directory has the following format:
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 45dd2ae..7256bc3 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
@@ -21,10 +21,13 @@ import java.util.Objects;
 import java.util.Collection;
 import java.io.Closeable;
 import java.io.IOException;
+import org.opengis.util.NameSpace;
+import org.opengis.util.NameFactory;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.internal.system.DefaultFactories;
 
 
 /**
@@ -40,6 +43,18 @@ import org.apache.sis.util.logging.WarningListeners;
  */
 public abstract class Decoder implements Closeable {
     /**
+     * The data store identifier created from the global attributes, or {@code null} if none.
+     * Defined as a namespace for use as the scope of children resources (the variables).
+     * This is set by netCDF store constructor and shall not be modified afterward.
+     */
+    public NameSpace namespace;
+
+    /**
+     * The factory to use for creating variable identifiers.
+     */
+    public final NameFactory nameFactory;
+
+    /**
      * The library for geometric objects, or {@code null} for the default.
      * This will be used only if there is geometric objects to create.
      * If the netCDF file contains only raster data, this value is ignored.
@@ -65,15 +80,16 @@ public abstract class Decoder implements Closeable {
      */
     protected Decoder(final GeometryLibrary geomlib, final WarningListeners<DataStore> listeners) {
         Objects.requireNonNull(listeners);
-        this.geomlib   = geomlib;
-        this.listeners = listeners;
+        this.geomlib     = geomlib;
+        this.listeners   = listeners;
+        this.nameFactory = DefaultFactories.forBuildin(NameFactory.class);
     }
 
     /**
      * Returns a filename for formatting error message and for information purpose.
-     * The filename should not contain path.
+     * The filename should not contain path, but may contain file extension.
      *
-     * @return a filename to report in warning or error messages.
+     * @return a filename to include in warnings or error messages.
      */
     public abstract String getFilename();
 
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 b393bd3..e63d5fa 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
@@ -578,9 +578,9 @@ public final class ChannelDecoder extends Decoder {
 
     /**
      * Returns a filename for formatting error message and for information purpose.
-     * The filename does not contain path.
+     * The filename does not contain path, but may contain file extension.
      *
-     * @return a filename to report in warning or error messages.
+     * @return a filename to include in warnings or error messages.
      */
     @Override
     public final String getFilename() {
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
index 232f3c0..d3b9fa2 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
@@ -16,36 +16,35 @@
  */
 package org.apache.sis.internal.netcdf.impl;
 
-import java.io.IOException;
+import java.util.Map;
+import java.util.List;
+import java.util.Collection;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.Spliterator;
-import java.util.function.Consumer;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
-import org.apache.sis.feature.DefaultAttributeType;
-import org.apache.sis.feature.DefaultFeatureType;
-import org.apache.sis.internal.feature.MovingFeature;
+import java.util.function.Consumer;
+import java.io.IOException;
+import org.opengis.util.GenericName;
+import org.apache.sis.math.Vector;
 import org.apache.sis.internal.netcdf.DataType;
 import org.apache.sis.internal.netcdf.DiscreteSampling;
 import org.apache.sis.internal.netcdf.Resources;
-import org.apache.sis.math.Vector;
-import org.apache.sis.setup.GeometryLibrary;
-import org.apache.sis.storage.DataStore;
+import org.apache.sis.internal.feature.MovingFeature;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.feature.DefaultAttributeType;
 import org.apache.sis.util.collection.BackingStoreException;
-import org.apache.sis.util.logging.WarningListeners;
-import org.opengis.feature.AttributeType;
+import ucar.nc2.constants.CF;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.PropertyType;
-import org.opengis.util.GenericName;
-import ucar.nc2.constants.CF;
+import org.opengis.feature.AttributeType;
 
 
 /**
@@ -53,7 +52,7 @@ import ucar.nc2.constants.CF;
  * netCDF files encoded as specified in the OGC 16-114 (OGC Moving Features Encoding Extension: netCDF) specification.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -75,7 +74,7 @@ final class FeaturesInfo extends DiscreteSampling {
     private final VariableInfo time;
 
     /**
-     * The variable that contains <var>x</var> and <var>y</var> ordinate values (typically longitudes and latitudes).
+     * The variable that contains <var>x</var> and <var>y</var> coordinate values (typically longitudes and latitudes).
      * All variables in this array shall have the same length, and that length shall be the same than {@link #time}.
      */
     private final VariableInfo[] coordinates;
@@ -93,18 +92,20 @@ final class FeaturesInfo extends DiscreteSampling {
     /**
      * Creates a new discrete sampling parser for features identified by the given variable.
      *
+     * @param  decoder      the source of the features to create.
      * @param  counts       the count of instances per feature.
      * @param  identifiers  the feature identifiers.
-     * @param  library      the library for geometric objects, or {@code null} for the default.
-     * @param  listeners    the set of registered warning listeners for the data store.
+     * @param  time         the variable that contains time.
+     * @param  coordinates  the variable that contains <var>x</var> and <var>y</var> coordinate values.
+     * @param  properties   the variables that contain custom properties.
      * @throws IllegalArgumentException if the given library is non-null but not available.
      */
     @SuppressWarnings("rawtypes")                               // Because of generic array creation.
-    private FeaturesInfo(final Vector counts, final VariableInfo identifiers, final VariableInfo time,
-            final Collection<VariableInfo> coordinates, final Collection<VariableInfo> properties,
-            final GeometryLibrary library, final WarningListeners<DataStore> listeners)
+    private FeaturesInfo(final ChannelDecoder decoder,
+            final Vector counts, final VariableInfo identifiers, final VariableInfo time,
+            final Collection<VariableInfo> coordinates, final Collection<VariableInfo> properties)
     {
-        super(library, listeners);
+        super(decoder.geomlib, decoder.listeners);
         this.counts      = counts;
         this.identifiers = identifiers;
         this.coordinates = coordinates.toArray(new VariableInfo[coordinates.size()]);
@@ -146,13 +147,13 @@ final class FeaturesInfo extends DiscreteSampling {
             // TODO: add description.
             pt[i] = new DefaultAttributeType<>(info, valueClass, minOccurs, maxOccurs, null, characteristics);
         }
-        info.put(DefaultAttributeType.NAME_KEY, "Feature");     // TODO: find a better name.
+        String name = "Features";       // TODO: find a better name.
+        info.put(DefaultAttributeType.NAME_KEY, decoder.nameFactory.createLocalName(decoder.namespace, name));
         type = new DefaultFeatureType(info, false, null, pt);
     }
 
     /**
-     *
-     * @return type name.
+     * Returns an identifier for the collection of features in the netCDF file.
      */
     @Override
     public GenericName getIdentifier() {
@@ -283,8 +284,7 @@ search: for (final VariableInfo counts : decoder.variables) {
                     }
                     final VariableInfo time = coordinates.remove("T");
                     if (time != null) {
-                        features.add(new FeaturesInfo(counts.read(), identifiers, time, coordinates.values(),
-                                properties, decoder.geomlib, decoder.listeners));
+                        features.add(new FeaturesInfo(decoder, counts.read(), identifiers, time, coordinates.values(), properties));
                     }
                 }
             }
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 62486b5..1f4c7e0 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
@@ -128,9 +128,9 @@ public final class DecoderWrapper extends Decoder implements CancelTask {
 
     /**
      * Returns a filename for formatting error message and for information purpose.
-     * The filename should not contain path.
+     * The filename should not contain path, but may contain file extension.
      *
-     * @return a filename to report in warning or error messages.
+     * @return a filename to include in warnings or error messages.
      */
     @Override
     public String getFilename() {
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 a8db9f2..226c977 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,21 +17,23 @@
 package org.apache.sis.internal.netcdf.ucar;
 
 import java.util.stream.Stream;
-import org.apache.sis.internal.netcdf.DiscreteSampling;
-import org.apache.sis.setup.GeometryLibrary;
+import org.opengis.util.GenericName;
 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 ucar.nc2.ft.FeatureCollection;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
-import org.opengis.util.GenericName;
-import ucar.nc2.ft.FeatureCollection;
 
 
 /**
  * A wrapper around the UCAR {@code ucar.nc2.ft} package.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
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 0493dcc..8ddd1e1 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
@@ -63,7 +63,6 @@ import org.apache.sis.internal.netcdf.GridGeometry;
 import org.apache.sis.internal.storage.io.IOUtilities;
 import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.storage.wkt.StoreFormat;
-import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.CharSequences;
@@ -169,11 +168,6 @@ final class MetadataReader extends MetadataBuilder {
     private final String[] searchPath;
 
     /**
-     * The name factory, created when first needed.
-     */
-    private transient NameFactory nameFactory;
-
-    /**
      * The contact, used at metadata creation time for avoiding to construct identical objects
      * more than once.
      *
@@ -932,12 +926,8 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
         newSampleDimension();
         final String name = trim(variable.getName());
         if (name != null) {
-            if (nameFactory == null) {
-                nameFactory = DefaultFactories.forBuildin(NameFactory.class);
-                // Real dependency injection to be used in a future version.
-            }
-            setBandIdentifier(nameFactory.createMemberName(null, name,
-                    nameFactory.createTypeName(null, variable.getDataTypeName())));
+            final NameFactory f = decoder.nameFactory;
+            setBandIdentifier(f.createMemberName(null, name, f.createTypeName(null, variable.getDataTypeName())));
         }
         Object[] v = variable.getAttributeValues(CF.STANDARD_NAME, false);
         final String id = (v.length == 1) ? trim((String) v[0]) : 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 35bf5f7..3629377 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
@@ -18,26 +18,29 @@ package org.apache.sis.storage.netcdf;
 
 import java.io.IOException;
 import java.net.URI;
-import java.util.Collection;
 import java.util.List;
+import java.util.Collection;
+import org.opengis.util.NameSpace;
+import org.opengis.util.NameFactory;
+import org.opengis.util.GenericName;
+import org.opengis.metadata.Metadata;
+import org.opengis.parameter.ParameterValueGroup;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.UnsupportedStorageException;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.Aggregate;
 import org.apache.sis.internal.netcdf.Decoder;
 import org.apache.sis.internal.storage.URIDataStore;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.metadata.ModifiableMetadata;
 import org.apache.sis.setup.OptionKey;
-import org.apache.sis.storage.Aggregate;
-import org.apache.sis.storage.DataStore;
-import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.Resource;
-import org.apache.sis.storage.StorageConnector;
-import org.apache.sis.storage.UnsupportedStorageException;
 import org.apache.sis.storage.event.ChangeEvent;
 import org.apache.sis.storage.event.ChangeListener;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.Version;
-import org.opengis.metadata.Metadata;
-import org.opengis.parameter.ParameterValueGroup;
-import org.opengis.util.GenericName;
+import ucar.nc2.constants.ACDD;
 import ucar.nc2.constants.CDM;
 
 
@@ -62,6 +65,9 @@ public class NetcdfStore extends DataStore implements Aggregate {
 
     /**
      * The {@link NetcdfStoreProvider#LOCATION} parameter value, or {@code null} if none.
+     * This is used for information purpose only, not for actual reading operations.
+     *
+     * @see #getOpenParameters()
      */
     private final URI location;
 
@@ -100,6 +106,14 @@ public class NetcdfStore extends DataStore implements Aggregate {
             throw new UnsupportedStorageException(super.getLocale(), NetcdfStoreProvider.NAME,
                     connector.getStorage(), connector.getOption(OptionKey.OPEN_OPTIONS));
         }
+        String id = decoder.stringValue(ACDD.id);
+        if (id == null || (id = id.trim()).isEmpty()) {
+            id = decoder.getFilename();
+        }
+        if (id != null) {
+            final NameFactory f = decoder.nameFactory;
+            decoder.namespace = f.createNameSpace(f.createLocalName(null, id), null);
+        }
     }
 
     /**
@@ -119,16 +133,6 @@ public class NetcdfStore extends DataStore implements Aggregate {
     }
 
     /**
-     * SQL Datastore root resource has no identifier.
-     *
-     * @return null
-     */
-    @Override
-    public GenericName getIdentifier() {
-        return null;
-    }
-
-    /**
      * Returns the version number of the Climate and Forecast (CF) conventions used in the netCDF file.
      * The use of CF convention is mandated by the OGC 11-165r2 standard
      * (<cite>CF-netCDF3 Data Model Extension standard</cite>).
@@ -148,6 +152,20 @@ public class NetcdfStore extends DataStore implements Aggregate {
     }
 
     /**
+     * Returns an identifier constructed from global attributes or the filename of the netCDF file.
+     *
+     * @return the identifier fetched from global attributes or the filename, or {@code null} if none.
+     * @throws DataStoreException if an error occurred while fetching the identifier.
+     *
+     * @since 1.0
+     */
+    @Override
+    public GenericName getIdentifier() throws DataStoreException {
+        final NameSpace namespace = decoder.namespace;
+        return (namespace != null) ? namespace.name() : null;
+    }
+
+    /**
      * Returns information about the dataset as a whole. The returned metadata object can contain information
      * such as the spatiotemporal extent of the dataset, contact information about the creator or distributor,
      * data quality, usage constraints and more.
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 7996149..67f5ec3 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
@@ -152,8 +152,8 @@ public final class Database {
             tablesByNames.add(store, table.featureType.getName(), table);
             hasGeometry |= table.hasGeometry;
         }
-        this.tables = tableList.toArray(new Table[tableList.size()]);
-        this.functions = analyzer.functions;
+        this.tables      = tableList.toArray(new Table[tableList.size()]);
+        this.functions   = analyzer.functions;
         this.hasGeometry = hasGeometry;
     }
 
@@ -175,7 +175,7 @@ public final class Database {
 
     /**
      * Stores information about tables in the given metadata.
-     * Only tables explicitely requested by the user are listed.
+     * Only tables explicitly requested by the user are listed.
      *
      * @param  metadata  information about the database.
      * @param  builder   where to add information about the tables.
diff --git a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
index fd0be5f..e41af4e 100644
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/Table.java
@@ -16,45 +16,46 @@
  */
 package org.apache.sis.internal.sql.feature;
 
-import java.sql.Connection;
-import java.sql.DatabaseMetaData;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Collection;
+import java.util.Map;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
+import java.sql.DatabaseMetaData;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
 import javax.sql.DataSource;
-import org.apache.sis.feature.builder.AssociationRoleBuilder;
+import org.opengis.util.GenericName;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.feature.builder.AttributeRole;
 import org.apache.sis.feature.builder.AttributeTypeBuilder;
+import org.apache.sis.feature.builder.AssociationRoleBuilder;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
 import org.apache.sis.internal.feature.Geometries;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.InternalDataStoreException;
 import org.apache.sis.internal.metadata.sql.Reflection;
 import org.apache.sis.internal.metadata.sql.SQLUtilities;
 import org.apache.sis.internal.storage.AbstractFeatureSet;
 import org.apache.sis.internal.util.CollectionsExt;
-import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.InternalDataStoreException;
+import org.apache.sis.util.collection.WeakValueHashMap;
+import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.CharSequences;
-import org.apache.sis.util.Classes;
-import org.apache.sis.util.Debug;
 import org.apache.sis.util.Exceptions;
+import org.apache.sis.util.Classes;
 import org.apache.sis.util.Numbers;
-import org.apache.sis.util.collection.TreeTable;
-import org.apache.sis.util.collection.WeakValueHashMap;
-import org.apache.sis.util.iso.Names;
-import org.opengis.feature.AttributeType;
+import org.apache.sis.util.Debug;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureAssociationRole;
 import org.opengis.feature.FeatureType;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.util.GenericName;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.FeatureAssociationRole;
 
 
 /**
@@ -416,21 +417,6 @@ final class Table extends AbstractFeatureSet {
     }
 
     /**
-     *
-     * @return table identifier composed of catalog, schema and table name.
-     */
-    @Override
-    public GenericName getIdentifier() {
-        if (name.catalog != null && name.schema != null) {
-            return Names.createGenericName(null, null, name.catalog, name.schema, name.table);
-        } else if (name.schema != null) {
-            return Names.createGenericName(null, null, name.schema, name.table);
-        } else {
-            return Names.createLocalName(null, null, name.table);
-        }
-    }
-
-    /**
      * Returns the given relations as an array, or {@code null} if none.
      */
     private static Relation[] toArray(final Collection<Relation> relations) {
@@ -530,6 +516,14 @@ final class Table extends AbstractFeatureSet {
 
 
     /**
+     * Returns the table identifier composed of catalog, schema and table name.
+     */
+    @Override
+    public final GenericName getIdentifier() {
+        return featureType.getName().toFullyQualifiedName();
+    }
+
+    /**
      * Returns the feature type inferred from the database structure analysis.
      */
     @Override
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 72d5073..0de338b 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
@@ -16,27 +16,28 @@
  */
 package org.apache.sis.storage.sql;
 
-import java.sql.Connection;
-import java.sql.SQLException;
 import java.util.Collection;
 import javax.sql.DataSource;
-import org.apache.sis.internal.sql.feature.Database;
-import org.apache.sis.internal.sql.feature.Resources;
-import org.apache.sis.internal.storage.MetadataBuilder;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.lang.reflect.Method;
+import org.opengis.util.GenericName;
+import org.opengis.metadata.Metadata;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.metadata.spatial.SpatialRepresentationType;
+import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.Aggregate;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.IllegalNameException;
-import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.event.ChangeEvent;
 import org.apache.sis.storage.event.ChangeListener;
+import org.apache.sis.internal.sql.feature.Database;
+import org.apache.sis.internal.sql.feature.Resources;
+import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Exceptions;
-import org.opengis.metadata.Metadata;
-import org.opengis.metadata.spatial.SpatialRepresentationType;
-import org.opengis.parameter.ParameterValueGroup;
-import org.opengis.util.GenericName;
 
 
 /**
@@ -53,6 +54,17 @@ import org.opengis.util.GenericName;
  */
 public class SQLStore extends DataStore implements Aggregate {
     /**
+     * Names of possible public getter methods for data source title, in preference order.
+     */
+    private static final String[] NAME_GETTERS = {
+            "getDescription",           // PostgreSQL, SQL Server
+            "getDataSourceName",        // Derby
+            "getDatabaseName",          // Derby, PostgreSQL, SQL Server
+            "getUrl",                   // PostgreSQL
+            "getURL"                    // SQL Server
+    };
+
+    /**
      * The data source to use for obtaining connections to the database.
      */
     private final DataSource source;
@@ -132,12 +144,12 @@ public class SQLStore extends DataStore implements Aggregate {
     }
 
     /**
-     * SQL Datastore root resource has no identifier.
+     * SQL data store root resource has no identifier.
      *
-     * @return null
+     * @return {@code null}.
      */
     @Override
-    public GenericName getIdentifier() {
+    public GenericName getIdentifier() throws DataStoreException {
         return null;
     }
 
@@ -158,10 +170,11 @@ public class SQLStore extends DataStore implements Aggregate {
     /**
      * Returns the database model, analyzing the database schema when first needed.
      * This method performs the same work than {@link #model()}, but using an existing connection.
+     * Callers must own a synchronization lock on {@code this}.
      *
      * @param c  connection to the database.
      */
-    private synchronized Database model(final Connection c) throws DataStoreException, SQLException {
+    private Database model(final Connection c) throws DataStoreException, SQLException {
         if (model == null) {
             model = new Database(this, c, source, tableNames, listeners);
         }
@@ -189,6 +202,25 @@ public class SQLStore extends DataStore implements Aggregate {
             } catch (SQLException e) {
                 throw new DataStoreException(Exceptions.unwrap(e));
             }
+            /*
+             * Try to find a title from the data source description.
+             */
+            for (final String c : NAME_GETTERS) {
+                try {
+                    final Method method = source.getClass().getMethod(c);
+                    if (method.getReturnType() == String.class) {
+                        String name = (String) method.invoke(source);
+                        if (name != null && !(name = name.trim()).isEmpty()) {
+                            builder.addTitle(name);
+                            break;
+                        }
+                    }
+                } catch (NoSuchMethodException | SecurityException e) {
+                    // Ignore - try the next method.
+                } catch (ReflectiveOperationException e) {
+                    throw new DataStoreException(Exceptions.unwrap(e));
+                }
+            }
             metadata = builder.build(true);
         }
         return metadata;
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 9b74787..511d86a 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
@@ -17,12 +17,16 @@
 package org.apache.sis.internal.storage;
 
 import java.util.Locale;
+import org.opengis.util.GenericName;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.Metadata;
+import org.opengis.metadata.Identifier;
+import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.extent.Extent;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.metadata.extent.GeographicExtent;
 import org.opengis.metadata.identification.Identification;
+import org.apache.sis.referencing.NamedIdentifier;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.util.Localized;
 import org.apache.sis.util.logging.WarningListeners;
@@ -35,6 +39,9 @@ import org.apache.sis.storage.event.ChangeListener;
 
 /**
  * Base implementation of resources contained in data stores.
+ * This class provides default implementation of {@link #getIdentifier()} and {@link #getEnvelope()}
+ * methods which extract their information from the value returned by {@link #getMetadata()}.
+ * Subclasses should override those methods if they can provide those information more efficiently.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
@@ -90,6 +97,58 @@ public abstract class AbstractResource implements Resource, Localized {
     }
 
     /**
+     * Returns an identifier for this resource. The default implementation returns the first identifier
+     * of {@code Metadata/​identificationInfo/​citation}, provided that exactly one such citation is found.
+     * If more than one citation is found, then this method returns {@code null} since the identification
+     * is considered ambiguous. This is the same default implementation than {@link DataStore#getIdentifier()}.
+     *
+     * @return the resource identifier inferred from metadata, or {@code null} if none or ambiguous.
+     * @throws DataStoreException if an error occurred while fetching the identifier.
+     *
+     * @see DataStore#getIdentifier()
+     */
+    @Override
+    public GenericName getIdentifier() throws DataStoreException {
+        return identifier(getMetadata());
+    }
+
+    /**
+     * Implementation of {@link #getIdentifier()}, provided as a separated method for implementations
+     * that do not extend {@code AbstractResource}.
+     *
+     * @param  metadata  the metadata from which to infer the identifier, or {@code null}.
+     * @return the resource identifier inferred from metadata, or {@code null} if none or ambiguous.
+     *
+     * @see StoreUtilities#getAnyIdentifier(Metadata, boolean)
+     */
+    public static GenericName identifier(final Metadata metadata) {
+        if (metadata != null) {
+            Citation citation = null;
+            for (final Identification id : metadata.getIdentificationInfo()) {
+                final Citation c = id.getCitation();
+                if (c != null) {
+                    if (citation != null && citation != c) return null;                 // Ambiguity.
+                    citation = c;
+                }
+            }
+            if (citation != null) {
+                Identifier first = null;
+                for (final Identifier c : citation.getIdentifiers()) {
+                    if (c instanceof GenericName) {
+                        return (GenericName) c;
+                    } else if (first == null) {
+                        first = c;
+                    }
+                }
+                if (first != null) {
+                    return new NamedIdentifier(first);
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
      * Returns the spatio-temporal envelope of this resource.
      * The default implementation computes the union of all {@link GeographicBoundingBox} in the resource metadata,
      * assuming the {@linkplain org.apache.sis.referencing.CommonCRS#defaultGeographic() default geographic CRS}
@@ -97,6 +156,8 @@ public abstract class AbstractResource implements Resource, Localized {
      *
      * @return the spatio-temporal resource extent, or {@code null} if none.
      * @throws DataStoreException if an error occurred while reading or computing the envelope.
+     *
+     * @see org.apache.sis.storage.DataSet#getEnvelope()
      */
     public Envelope getEnvelope() throws DataStoreException {
         return envelope(getMetadata());
@@ -113,18 +174,14 @@ public abstract class AbstractResource implements Resource, Localized {
         GeneralEnvelope bounds = null;
         if (metadata != null) {
             for (final Identification identification : metadata.getIdentificationInfo()) {
-                if (identification != null) {                                               // Paranoiac check.
-                    for (final Extent extent : identification.getExtents()) {
-                        if (extent != null) {                                               // Paranoiac check.
-                            for (final GeographicExtent ge : extent.getGeographicElements()) {
-                                if (ge instanceof GeographicBoundingBox) {
-                                    final GeneralEnvelope env = new GeneralEnvelope((GeographicBoundingBox) ge);
-                                    if (bounds == null) {
-                                        bounds = env;
-                                    } else {
-                                        bounds.add(env);
-                                    }
-                                }
+                for (final Extent extent : identification.getExtents()) {
+                    for (final GeographicExtent ge : extent.getGeographicElements()) {
+                        if (ge instanceof GeographicBoundingBox) {
+                            final GeneralEnvelope env = new GeneralEnvelope((GeographicBoundingBox) ge);
+                            if (bounds == null) {
+                                bounds = env;
+                            } else {
+                                bounds.add(env);
                             }
                         }
                     }
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 e7e9434..f7e6e7f 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
@@ -16,16 +16,17 @@
  */
 package org.apache.sis.internal.storage;
 
-import java.util.Collections;
 import java.util.Map;
+import java.util.Collections;
 import java.util.Spliterator;
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
-import org.apache.sis.feature.DefaultAssociationRole;
-import org.apache.sis.feature.DefaultFeatureType;
+import org.opengis.util.GenericName;
+import org.opengis.geometry.Envelope;
 import org.apache.sis.feature.FeatureOperations;
-import org.apache.sis.filter.DefaultFilterFactory;
+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;
@@ -35,6 +36,8 @@ 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;
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.Operation;
@@ -43,8 +46,7 @@ import org.opengis.filter.Filter;
 import org.opengis.filter.FilterFactory;
 import org.opengis.filter.PropertyIsEqualTo;
 import org.opengis.filter.expression.Expression;
-import org.opengis.geometry.Envelope;
-import org.opengis.util.GenericName;
+import org.apache.sis.filter.DefaultFilterFactory;
 
 
 /**
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 e62217a..6757d43 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
@@ -18,13 +18,15 @@ package org.apache.sis.internal.storage;
 
 import java.util.Collection;
 import java.util.stream.Stream;
+import org.opengis.util.GenericName;
+import org.opengis.metadata.Metadata;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.WarningListeners;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
-import org.opengis.metadata.Metadata;
-import org.opengis.util.GenericName;
 
 
 /**
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/StoreUtilities.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/StoreUtilities.java
index b52d702..2b08ef4 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/StoreUtilities.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/StoreUtilities.java
@@ -20,6 +20,7 @@ import java.util.EnumSet;
 import java.util.stream.Stream;
 import java.nio.file.OpenOption;
 import java.nio.file.StandardOpenOption;
+import org.opengis.util.GenericName;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.identification.Identification;
 import org.opengis.metadata.identification.DataIdentification;
@@ -85,22 +86,16 @@ public final class StoreUtilities extends Static {
      * Returns an identifier for a resource having the given metadata, or {@code null} if none.
      * This method checks the information returned by {@link Metadata#getIdentificationInfo()},
      * with precedence to {@link DataIdentification} over other kinds of {@link Identification}.
-     *
-     * @param  metadata  the metadata from which to get a data identifier, or {@code null}.
-     * @return a data identifier, or {@code null} if none.
-     */
-    public static String getIdentifier(final Metadata metadata) {
-        return Citations.removeIgnorableCharacters(getIdentifier(metadata, true));
-    }
-
-    /**
-     * Implementation of {@link #getIdentifier(Metadata)} to be shared with {@link #getLabel(Resource)}.
+     * This method does not check for ambiguity (if there is more than one identification info).
      *
      * @param  metadata  the metadata from which to get a data identifier, or {@code null}.
      * @param  unicode   whether to restrict to valid Unicode identifiers.
      * @return a data identifier, or {@code null} if none.
+     *
+     * @see AbstractResource#identifier(Metadata)
+     * @see Citations#removeIgnorableCharacters(String)
      */
-    private static String getIdentifier(final Metadata metadata, final boolean unicode) {
+    private static String getAnyIdentifier(final Metadata metadata, final boolean unicode) {
         String fallback = null;
         if (metadata != null) {
             for (final Identification md : metadata.getIdentificationInfo()) {
@@ -118,9 +113,9 @@ public final class StoreUtilities extends Static {
     }
 
     /**
-     * Returns a short label for the given resource. This method returns an identifier if possible,
-     * or the title otherwise. If neither an identifier or title can be found, then this method returns
-     * the kind of resource implemented by the given object.
+     * Returns a short label for the given resource. This method returns the display name if possible,
+     * or the identifier otherwise. If neither a display name, identifier or title can be found, then
+     * this method returns the kind of resource implemented by the given object.
      *
      * @param  resource  the resource for which to get a label.
      * @return a human-readable label for the given resource (not to be used as an identifier).
@@ -132,9 +127,14 @@ public final class StoreUtilities extends Static {
             title = ((DataStore) resource).getDisplayName();
         }
         if (title == null) {
-            title = getIdentifier(resource.getMetadata(), false);
-            if (title == null) {
-                title = Classes.getShortName(getInterface(resource.getClass()));
+            final GenericName identifier = resource.getIdentifier();
+            if (identifier != null) {
+                title = identifier.toString();
+            } else {
+                title = getAnyIdentifier(resource.getMetadata(), false);
+                if (title == null) {
+                    title = Classes.getShortName(getInterface(resource.getClass()));
+                }
             }
         }
         return title;
@@ -296,4 +296,16 @@ public final class StoreUtilities extends Static {
             target.add(stream.iterator());
         }
     }
+
+    /**
+     * Returns an error message for a resource not found. This is used for exception to be thrown
+     * as {@link org.apache.sis.storage.IllegalNameException}.
+     *
+     * @param  store       the store for which a resource has not been found.
+     * @param  identifier  the requested identifier.
+     * @return error message for the exception to be thrown.
+     */
+    public static String resourceNotFound(final DataStore store, final String identifier) {
+        return Resources.forLocale(store.getLocale()).getString(Resources.Keys.ResourceNotFound_2, store.getDisplayName(), identifier);
+    }
 }
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 1f940b0..7604a22 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
@@ -16,68 +16,70 @@
  */
 package org.apache.sis.internal.storage.csv;
 
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.LineNumberReader;
-import java.io.Reader;
-import java.net.URI;
-import java.nio.charset.Charset;
-import java.time.DateTimeException;
-import java.time.Instant;
+import java.util.List;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
-import java.util.List;
 import java.util.Locale;
-import java.util.logging.Level;
-import java.util.logging.LogRecord;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.time.Instant;
+import java.time.DateTimeException;
+import java.io.Reader;
+import java.io.BufferedReader;
+import java.io.LineNumberReader;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.Charset;
 import javax.measure.Unit;
 import javax.measure.quantity.Time;
+import org.opengis.util.GenericName;
+import org.opengis.util.FactoryException;
+import org.opengis.geometry.Envelope;
+import org.opengis.metadata.Metadata;
+import org.opengis.metadata.maintenance.ScopeCode;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.crs.TemporalCRS;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.feature.DefaultAttributeType;
 import org.apache.sis.feature.DefaultFeatureType;
 import org.apache.sis.feature.FoliationRepresentation;
-import org.apache.sis.geometry.GeneralEnvelope;
-import org.apache.sis.geometry.ImmutableEnvelope;
-import org.apache.sis.internal.feature.Geometries;
-import org.apache.sis.internal.feature.MovingFeature;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.internal.referencing.GeodeticObjectBuilder;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.internal.storage.MetadataBuilder;
-import org.apache.sis.internal.storage.Resources;
-import org.apache.sis.internal.storage.URIDataStore;
 import org.apache.sis.internal.storage.io.IOUtilities;
 import org.apache.sis.internal.storage.io.RewindableLineReader;
-import org.apache.sis.internal.util.UnmodifiableArrayList;
-import org.apache.sis.io.InvalidSeekException;
-import org.apache.sis.measure.Units;
+import org.apache.sis.internal.feature.Geometries;
+import org.apache.sis.internal.feature.MovingFeature;
+import org.apache.sis.internal.storage.Resources;
+import org.apache.sis.internal.storage.URIDataStore;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.metadata.iso.DefaultMetadata;
 import org.apache.sis.metadata.sql.MetadataStoreException;
-import org.apache.sis.referencing.CRS;
-import org.apache.sis.referencing.CommonCRS;
-import org.apache.sis.setup.OptionKey;
 import org.apache.sis.storage.DataOptionKey;
-import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreReferencingException;
-import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.UnsupportedStorageException;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.setup.OptionKey;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.resources.Errors;
-import org.opengis.feature.AttributeType;
+import org.apache.sis.io.InvalidSeekException;
+import org.apache.sis.measure.Units;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.PropertyType;
-import org.opengis.geometry.Envelope;
-import org.opengis.metadata.Metadata;
-import org.opengis.metadata.maintenance.ScopeCode;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.referencing.crs.TemporalCRS;
-import org.opengis.referencing.operation.TransformException;
-import org.opengis.util.FactoryException;
-import org.opengis.util.GenericName;
+import org.opengis.feature.AttributeType;
 
 
 /**
@@ -310,16 +312,6 @@ final class Store extends URIDataStore implements FeatureSet {
     }
 
     /**
-     * CSV Identifier is identical to it's feature type name.
-     *
-     * @return CSV feature type name.
-     */
-    @Override
-    public GenericName getIdentifier() {
-        return featureType.getName();
-    }
-
-    /**
      * Moves the reader position to beginning of file, if possible. We try to use the mark defined by the constructor,
      * which is set after the last header line. If the mark is no longer valid, then we have to create a new line reader.
      * In this later case, we have to skip the header lines (i.e. we reproduce the constructor loop, but without parsing
@@ -627,6 +619,17 @@ final class Store extends URIDataStore implements FeatureSet {
     }
 
     /**
+     * Returns an identifier for this CSV data store.
+     * This method returns the {@link #getType() type} name, which is itself derived from the file name.
+     *
+     * @return identifier for this CSV data store.
+     */
+    @Override
+    public GenericName getIdentifier() throws DataStoreException {
+        return featureType.getName();
+    }
+
+    /**
      * Returns the metadata associated to the CSV file, or {@code null} if none.
      *
      * @return the metadata associated to the CSV file, or {@code null} if none.
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 dbf864e..61e1842 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
@@ -16,43 +16,46 @@
  */
 package org.apache.sis.internal.storage.folder;
 
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.charset.Charset;
-import java.nio.file.DirectoryIteratorException;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
+import java.util.Map;
+import java.util.List;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.List;
+import java.util.Collections;
 import java.util.Locale;
-import java.util.Map;
 import java.util.TimeZone;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.logging.Level;
-import org.apache.sis.internal.storage.MetadataBuilder;
-import org.apache.sis.internal.storage.Resources;
-import org.apache.sis.internal.storage.StoreResource;
-import org.apache.sis.internal.storage.StoreUtilities;
-import org.apache.sis.internal.util.UnmodifiableArrayList;
+import java.util.concurrent.ConcurrentHashMap;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import java.nio.file.DirectoryStream;
+import java.nio.file.DirectoryIteratorException;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import org.opengis.util.GenericName;
+import org.opengis.util.NameFactory;
+import org.opengis.util.NameSpace;
+import org.opengis.metadata.Metadata;
+import org.opengis.metadata.maintenance.ScopeCode;
+import org.opengis.parameter.ParameterValueGroup;
 import org.apache.sis.setup.OptionKey;
+import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.Aggregate;
 import org.apache.sis.storage.DataStore;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.DataStores;
-import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.UnsupportedStorageException;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
+import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.internal.storage.MetadataBuilder;
+import org.apache.sis.internal.storage.StoreUtilities;
+import org.apache.sis.internal.storage.StoreResource;
+import org.apache.sis.internal.storage.Resources;
 import org.apache.sis.storage.event.ChangeEvent;
 import org.apache.sis.storage.event.ChangeListener;
-import org.apache.sis.util.collection.BackingStoreException;
-import org.apache.sis.util.iso.Names;
-import org.opengis.metadata.Metadata;
-import org.opengis.metadata.maintenance.ScopeCode;
-import org.opengis.parameter.ParameterValueGroup;
-import org.opengis.util.GenericName;
 
 
 /**
@@ -80,15 +83,24 @@ import org.opengis.util.GenericName;
 class Store extends DataStore implements StoreResource, Aggregate, DirectoryStream.Filter<Path> {
     /**
      * The data store for the root directory specified by the user.
+     * May be {@code this} if this store instance is for the root directory.
      */
-    private final DataStore originator;
+    private final Store originator;
 
     /**
-     * The {@link FolderStoreProvider#LOCATION} parameter value, or {@code null} if none.
+     * The {@link FolderStoreProvider#LOCATION} parameter value.
      */
     protected final Path location;
 
     /**
+     * An identifier for this folder, or {@code null} if this folder is the root specified by the user.
+     * Shall never be null for sub-folders found in the root folder.
+     *
+     * @see #identifier(NameFactory)
+     */
+    private GenericName identifier;
+
+    /**
      * Formatting conventions of dates and numbers, or {@code null} if unspecified.
      */
     protected final Locale locale;
@@ -172,7 +184,7 @@ class Store extends DataStore implements StoreResource, Aggregate, DirectoryStre
      * @param  connector  information about the storage (URL, stream, <i>etc</i>).
      * @throws DataStoreException if an error occurred while opening the stream.
      */
-    private Store(final Store parent, final StorageConnector connector) throws DataStoreException {
+    private Store(final Store parent, final StorageConnector connector, final NameFactory nameFactory) throws DataStoreException {
         super(parent, connector);
         originator        = parent;
         location          = connector.getStorageAs(Path.class);
@@ -181,13 +193,7 @@ class Store extends DataStore implements StoreResource, Aggregate, DirectoryStre
         encoding          = connector.getOption(OptionKey.ENCODING);
         children          = parent.children;
         componentProvider = parent.componentProvider;
-    }
-
-    @Override
-    public GenericName getIdentifier() {
-        final String displayName = getDisplayName();
-        if (displayName != null) return Names.createLocalName(null, null, displayName);
-        return null;
+        identifier        = nameFactory.createLocalName(parent.identifier(nameFactory).scope(), super.getDisplayName());
     }
 
     /**
@@ -222,6 +228,32 @@ class Store extends DataStore implements StoreResource, Aggregate, DirectoryStre
     }
 
     /**
+     * Returns the name of this folder.
+     */
+    @Override
+    public GenericName getIdentifier() {
+        return identifier(null);
+    }
+
+    /**
+     * Returns the name of this folder, creating it if needed.
+     * Only the root folder may have its creation delayed.
+     *
+     * @param  nameFactory  the factory to use for creating the root folder, or {@code null} for the default.
+     */
+    private synchronized GenericName identifier(NameFactory nameFactory) {
+        if (identifier == null) {
+            if (nameFactory == null) {
+                nameFactory = DefaultFactories.forBuildin(NameFactory.class);
+            }
+            GenericName name = nameFactory.createLocalName(null, super.getDisplayName());
+            NameSpace   ns   = nameFactory.createNameSpace(name, Collections.singletonMap("separator", "/"));
+            identifier       = nameFactory.createLocalName(ns, ".");
+        }
+        return identifier;
+    }
+
+    /**
      * Returns information about the data store as a whole.
      * Those metadata contains the directory name in the resource title.
      *
@@ -231,11 +263,10 @@ class Store extends DataStore implements StoreResource, Aggregate, DirectoryStre
     public synchronized Metadata getMetadata() {
         if (metadata == null) {
             final MetadataBuilder mb = new MetadataBuilder();
-            final String name = getDisplayName();
-            mb.addResourceScope(ScopeCode.COLLECTION, Resources.formatInternational(Resources.Keys.DirectoryContent_1, name));
+            mb.addResourceScope(ScopeCode.COLLECTION, Resources.formatInternational(Resources.Keys.DirectoryContent_1, getDisplayName()));
             mb.addLanguage(locale,   MetadataBuilder.Scope.RESOURCE);
             mb.addEncoding(encoding, MetadataBuilder.Scope.RESOURCE);
-            mb.addTitleOrIdentifier(name, MetadataBuilder.Scope.ALL);
+            mb.addTitleOrIdentifier(identifier.toString(), MetadataBuilder.Scope.RESOURCE);
             metadata = mb.build(true);
         }
         return metadata;
@@ -244,13 +275,17 @@ class Store extends DataStore implements StoreResource, Aggregate, DirectoryStre
     /**
      * Returns all resources found in the folder given at construction time.
      * Only the resources recognized by a {@link DataStore} will be included.
-     * This includes sub-folders. Resources are in no particular order.
+     * Sub-folders are represented by other folder {@code Store} instances;
+     * their resources are available by invoking {@link Aggregate#components()}
+     * on them (this method does not traverse sub-folders recursively by itself).
+     * Resources are in no particular order.
      */
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public synchronized Collection<Resource> components() throws DataStoreException {
         if (components == null) {
             final List<DataStore> resources = new ArrayList<>();
+            final NameFactory nameFactory = DefaultFactories.forBuildin(NameFactory.class);
             try (DirectoryStream<Path> stream = Files.newDirectoryStream(location, this)) {
                 for (final Path candidate : stream) {
                     /*
@@ -281,7 +316,7 @@ class Store extends DataStore implements StoreResource, Aggregate, DirectoryStre
                             } else if (componentProvider.probeContent(connector).isSupported()) {
                                 next = componentProvider.open(connector);   // Open a file of specified format.
                             } else if (Files.isDirectory(candidate)) {
-                                next = new Store(this, connector);          // Open a sub-directory.
+                                next = new Store(this, connector, nameFactory);        // Open a sub-directory.
                             } else {
                                 connector.closeAllExcept(null);             // Not the format specified at construction time.
                                 continue;
@@ -292,7 +327,7 @@ class Store extends DataStore implements StoreResource, Aggregate, DirectoryStre
                                 listeners.warning(Level.FINE, null, ex);
                                 continue;
                             }
-                            next = new Store(this, connector);
+                            next = new Store(this, connector, nameFactory);
                         } catch (DataStoreException ex) {
                             try {
                                 connector.closeAllExcept(null);
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/WritableStore.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/WritableStore.java
index 54fd103..24808e9 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/WritableStore.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/folder/WritableStore.java
@@ -23,6 +23,7 @@ import java.nio.file.SimpleFileVisitor;
 import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.io.IOException;
+import org.opengis.util.GenericName;
 import org.apache.sis.setup.OptionKey;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.DataStore;
@@ -91,11 +92,14 @@ final class WritableStore extends Store implements WritableAggregate {
         /*
          * Infer a filename from the resource identifier, if one can be found.
          * A suffix is added to the filename if available (some formats may have no suffix at all).
+         *
+         * TODO: find a more specific metadata property for this informtion.
          */
-        String filename = StoreUtilities.getIdentifier(resource.getMetadata());
-        if (filename == null) {
+        final GenericName identifier = resource.getIdentifier();
+        if (identifier == null) {
             throw new DataStoreException(message(Resources.Keys.MissingResourceIdentifier_1, StoreUtilities.getLabel(resource)));
         }
+        String filename = identifier.toString();
         final String[] suffixes = StoreUtilities.getFileSuffixes(componentProvider.getClass());
         if (suffixes.length != 0) {
             filename += '.' + suffixes[0];
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 14a2798..cc65820 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
@@ -18,17 +18,19 @@ package org.apache.sis.internal.storage.query;
 
 import java.util.List;
 import java.util.stream.Stream;
+import org.opengis.util.GenericName;
+import org.opengis.geometry.Envelope;
 import org.apache.sis.internal.feature.FeatureUtilities;
 import org.apache.sis.internal.storage.AbstractFeatureSet;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.FeatureSet;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
 import org.opengis.filter.Filter;
-import org.opengis.filter.expression.Expression;
 import org.opengis.filter.sort.SortBy;
-import org.opengis.geometry.Envelope;
-import org.opengis.util.GenericName;
+import org.opengis.filter.expression.Expression;
 
 
 /**
@@ -70,13 +72,11 @@ final class FeatureSubset extends AbstractFeatureSet {
     }
 
     /**
-     * Inherit parent FeatureSet identifier.
-     *
-     * @return parent FeatureSet identifier.
+     * Returns {@code null} since this resource is a computation result.
      */
     @Override
     public GenericName getIdentifier() {
-        return source.getIdentifier();
+        return null;
     }
 
     /**
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 91f221e..af7a0d9 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
@@ -16,27 +16,26 @@
  */
 package org.apache.sis.internal.storage.wkt;
 
-import java.io.IOException;
+import java.util.List;
+import java.util.Arrays;
+import java.util.ArrayList;
 import java.io.Reader;
-import java.text.ParseException;
+import java.io.IOException;
 import java.text.ParsePosition;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import org.apache.sis.internal.storage.MetadataBuilder;
+import java.text.ParseException;
+import org.opengis.metadata.Metadata;
+import org.opengis.referencing.ReferenceSystem;
 import org.apache.sis.internal.storage.Resources;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.UnsupportedStorageException;
+import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.storage.URIDataStore;
 import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.setup.OptionKey;
-import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.StorageConnector;
-import org.apache.sis.storage.UnsupportedStorageException;
 import org.apache.sis.util.CharSequences;
-import org.opengis.metadata.Metadata;
-import org.opengis.referencing.ReferenceSystem;
-import org.opengis.util.GenericName;
 
 
 /**
@@ -96,16 +95,6 @@ final class Store extends URIDataStore {
     }
 
     /**
-     * WKT files have no unique identifiers.
-     *
-     * @return null
-     */
-    @Override
-    public GenericName getIdentifier() {
-        return null;
-    }
-
-    /**
      * Parses the objects, if not already done. Note that {@link #objects} may still be empty
      * if an exception has been thrown at this invocation time or in previous invocation.
      *
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 db4851a..76b9f55 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
@@ -16,36 +16,31 @@
  */
 package org.apache.sis.internal.storage.xml;
 
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Reader;
-import java.util.Collections;
 import java.util.Map;
+import java.util.Collections;
 import java.util.logging.LogRecord;
+import java.io.Closeable;
+import java.io.Reader;
+import java.io.InputStream;
+import java.io.IOException;
 import javax.xml.bind.JAXBException;
 import javax.xml.transform.stream.StreamSource;
-import org.apache.sis.internal.referencing.DefinitionVerifier;
-import org.apache.sis.internal.storage.MetadataBuilder;
-import org.apache.sis.internal.storage.URIDataStore;
-import org.apache.sis.internal.system.Loggers;
-import org.apache.sis.metadata.iso.DefaultMetadata;
-import org.apache.sis.referencing.NamedIdentifier;
-import org.apache.sis.setup.OptionKey;
-import org.apache.sis.storage.DataStoreException;
+import org.opengis.metadata.Metadata;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.ReferenceSystem;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+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.metadata.iso.DefaultMetadata;
 import org.apache.sis.util.logging.WarningListener;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.xml.XML;
-import org.opengis.metadata.Identifier;
-import org.opengis.metadata.Metadata;
-import org.opengis.metadata.citation.Citation;
-import org.opengis.metadata.identification.Identification;
-import org.opengis.referencing.ReferenceSystem;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.util.FactoryException;
-import org.opengis.util.GenericName;
+import org.apache.sis.internal.system.Loggers;
+import org.apache.sis.internal.storage.URIDataStore;
+import org.apache.sis.internal.storage.MetadataBuilder;
+import org.apache.sis.internal.referencing.DefinitionVerifier;
+import org.apache.sis.setup.OptionKey;
 
 
 /**
@@ -109,32 +104,6 @@ final class Store extends URIDataStore {
     }
 
     /**
-     * Returns the first identifier in the metadata.
-     *
-     * @return first metadata identifier or null if parsing fails or if there are
-     *         no identifiers.
-     */
-    @Override
-    public GenericName getIdentifier() {
-        try {
-            Metadata metadata = getMetadata();
-            if (metadata != null) {
-                for (Identification idt : metadata.getIdentificationInfo()) {
-                    Citation citation = idt.getCitation();
-                    if (citation != null) {
-                        for (Identifier id : citation.getIdentifiers()) {
-                            return NamedIdentifier.castOrCopy(id);
-                        }
-                    }
-                }
-            }
-        } catch (DataStoreException ex) {
-            return null;
-        }
-        return null;
-    }
-
-    /**
      * Returns the input stream or reader set in the given source, or {@code null} if none.
      */
     private static Closeable input(final StreamSource source) {
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 4543e89..510ea56 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
@@ -16,19 +16,21 @@
  */
 package org.apache.sis.storage;
 
-import java.util.IdentityHashMap;
 import java.util.Locale;
 import java.util.Map;
+import java.util.IdentityHashMap;
 import java.util.NoSuchElementException;
-import org.apache.sis.internal.storage.Resources;
-import org.apache.sis.internal.util.Citations;
-import org.apache.sis.util.ArgumentChecks;
+import org.opengis.util.ScopedName;
+import org.opengis.util.GenericName;
+import org.opengis.metadata.Metadata;
+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.opengis.metadata.Metadata;
-import org.opengis.metadata.identification.Identification;
-import org.opengis.parameter.ParameterValueGroup;
+import org.apache.sis.internal.storage.AbstractResource;
+import org.apache.sis.internal.storage.StoreUtilities;
+import org.apache.sis.internal.storage.Resources;
 
 
 /**
@@ -163,6 +165,58 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
     }
 
     /**
+     * Returns the parameters used to open this data store.
+     * The collection of legal parameters is implementation-dependent
+     * ({@linkplain org.apache.sis.parameter.DefaultParameterValue#getDescriptor() their description}
+     * is given by {@link DataStoreProvider#getOpenParameters()}),
+     * but should contain at least a parameter named {@value org.apache.sis.storage.DataStoreProvider#LOCATION}
+     * with a {@link java.net.URI}, {@link java.nio.file.Path} or {@link javax.sql.DataSource} value.
+     *
+     * <p>In the event a data store must be closed and reopened later, those parameters can be stored in a file or
+     * database and used for {@linkplain DataStoreProvider#open(ParameterValueGroup) creating a new store} later.</p>
+     *
+     * <p>In some cases, for stores reading in-memory data or other inputs that can not fit with
+     * {@code ParameterDescriptorGroup} requirements (for example an {@link java.io.InputStream}
+     * connected to unknown or no {@link java.net.URL}), this method may return null.</p>
+     *
+     * @return parameters used for opening this {@code DataStore}, or {@code null} if not available.
+     *
+     * @see DataStoreProvider#getOpenParameters()
+     *
+     * @since 0.8
+     */
+    public abstract ParameterValueGroup getOpenParameters();
+
+    /**
+     * Sets the locale to use for formatting warnings and other messages.
+     * In a client-server architecture, it should be the locale on the <em>client</em> side.
+     *
+     * <p>This locale is used on a <cite>best-effort</cite> basis; whether messages will honor this locale or not
+     * depends on the code that logged warnings or threw exceptions. In Apache SIS implementation, this locale has
+     * better chances to be honored by the {@link DataStoreException#getLocalizedMessage()} method rather than
+     * {@code getMessage()}. See {@code getLocalizedMessage()} javadoc for more information.</p>
+     *
+     * @param locale  the new locale to use.
+     *
+     * @see DataStoreException#getLocalizedMessage()
+     */
+    public synchronized void setLocale(final Locale locale) {
+        ArgumentChecks.ensureNonNull("locale", locale);
+        this.locale = locale;
+    }
+
+    /**
+     * The locale to use for formatting warnings and other messages. This locale if for user interfaces
+     * only – it has no effect on the data to be read or written from/to the data store.
+     *
+     * <p>The default value is the {@linkplain Locale#getDefault() system default locale}.</p>
+     */
+    @Override
+    public synchronized Locale getLocale() {
+        return locale;
+    }
+
+    /**
      * Returns a short name or label for this data store.
      * The returned name can be used in user interfaces or in error messages.
      * It may be a title in natural language, but should be relatively short.
@@ -177,13 +231,17 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
      * <p>This method should never throw an exception since it may be invoked for producing error
      * messages, in which case throwing an exception here would mask the original exception.</p>
      *
-     * <p>Default implementation returns the {@link StorageConnector#getStorageName()} value,
+     * <p>This method differs from {@link #getIdentifier()} in that it is typically a file name
+     * known at construction time instead than a property read from metadata.
+     * Default implementation returns the {@link StorageConnector#getStorageName()} value,
      * or {@code null} if this data store has been created by the no-argument constructor.
-     * Note that this default value may change in any future SIS version. Subclasses should
-     * override this method if they can provide a better name.</p>
+     * Subclasses should override this method if they can provide a better name.</p>
      *
      * @return a short name of label for this data store, or {@code null} if unknown.
      *
+     * @see #getIdentifier()
+     * @see #getLocale()
+     *
      * @since 0.8
      */
     public String getDisplayName() {
@@ -191,56 +249,51 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
     }
 
     /**
-     * The locale to use for formatting warnings and other messages. This locale if for user interfaces
-     * only – it has no effect on the data to be read or written from/to the data store.
+     * Returns an identifier for the root resource of this data store, or {@code null} if none.
+     * If this data store contains many resources (as in an {@link Aggregate}),
+     * the returned identifier shall be different than the identifiers of those child resources.
+     * In other words, the following equality shall hold without ambiguity:
      *
-     * <p>The default value is the {@linkplain Locale#getDefault() system default locale}.</p>
-     */
-    @Override
-    public synchronized Locale getLocale() {
-        return locale;
-    }
-
-    /**
-     * Sets the locale to use for formatting warnings and other messages.
-     * In a client-server architecture, it should be the locale on the <em>client</em> side.
+     * {@preformat java
+     *     findResource(getIdentifier().toString()) == this
+     * }
      *
-     * <p>This locale is used on a <cite>best-effort</cite> basis; whether messages will honor this locale or not
-     * depends on the code that logged warnings or threw exceptions. In Apache SIS implementation, this locale has
-     * better chances to be honored by the {@link DataStoreException#getLocalizedMessage()} method rather than
-     * {@code getMessage()}. See {@code getLocalizedMessage()} javadoc for more information.</p>
+     * Note that this identifier is not guaranteed to be unique between different {@code DataStore} instances;
+     * it only needs to be unique among the resources provided by this data store instance.
      *
-     * @param locale  the new locale to use.
+     * <div class="section">Default implementation</div>
+     * <p>The default implementation searches for an identifier in the metadata,
+     * at the location shown below, provided that conditions are met:</p>
      *
-     * @see DataStoreException#getLocalizedMessage()
-     */
-    public synchronized void setLocale(final Locale locale) {
-        ArgumentChecks.ensureNonNull("locale", locale);
-        this.locale = locale;
-    }
-
-    /**
-     * Returns the parameters used to open this data store.
-     * The collection of legal parameters is implementation-dependent
-     * ({@linkplain org.apache.sis.parameter.DefaultParameterValue#getDescriptor() their description}
-     * is given by {@link DataStoreProvider#getOpenParameters()}),
-     * but should contain at least a parameter named {@value org.apache.sis.storage.DataStoreProvider#LOCATION}
-     * with a {@link java.net.URI}, {@link java.nio.file.Path} or {@link javax.sql.DataSource} value.
+     * <blockquote>
+     * <p><b>Path:</b> {@link Resource#getMetadata() metadata} /
+     * {@link org.apache.sis.metadata.iso.DefaultMetadata#getIdentificationInfo() identificationInfo} /
+     * {@link org.apache.sis.metadata.iso.identification.AbstractIdentification#getCitation() citation} /
+     * {@link org.apache.sis.metadata.iso.citation.DefaultCitation#getIdentifiers() identifier}</p>
      *
-     * <p>In the event a data store must be closed and reopened later, those parameters can be stored in a file or
-     * database and used for {@linkplain DataStoreProvider#open(ParameterValueGroup) creating a new store} later.</p>
+     * <p><b>Condition:</b> default implementation returns a non-null identifier only if exactly one
+     * {@code citation} is found at above path. If two or more {@code citation} instances are found,
+     * the identification is considered ambiguous and {@code null} is returned.</p>
      *
-     * <p>In some cases, for stores reading in-memory data or other inputs that can not fit with
-     * {@code ParameterDescriptorGroup} requirements (for example an {@link java.io.InputStream}
-     * connected to unknown or no {@link java.net.URL}), this method may return null.</p>
+     * <p><b>Selection:</b> the first identifier implementing the {@code GenericName} interface is returned.
+     * If there is no such identifier, then a {@link org.apache.sis.referencing.NamedIdentifier} is created
+     * from the first identifier. If there is no identifier at all, then {@code null} is returned.</p>
+     * </blockquote>
      *
-     * @return parameters used for opening this {@code DataStore}, or {@code null} if not available.
+     * Subclasses are encouraged to override this method with more efficient implementations.
      *
-     * @see DataStoreProvider#getOpenParameters()
+     * @return an identifier for the root resource of this data store, or {@code null} if none.
+     * @throws DataStoreException if an error occurred while fetching the identifier.
      *
-     * @since 0.8
+     * @see #getMetadata()
+     * @see #getDisplayName()
+     *
+     * @since 1.0
      */
-    public abstract ParameterValueGroup getOpenParameters();
+    @Override
+    public GenericName getIdentifier() throws DataStoreException {
+        return AbstractResource.identifier(getMetadata());
+    }
 
     /**
      * Returns information about the data store as a whole. The returned metadata object can contain
@@ -251,20 +304,14 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
      * @return information about resources in the data store, or {@code null} if none.
      * @throws DataStoreException if an error occurred while reading the data.
      *
-     * @see Resource#getMetadata()
+     * @see #getIdentifier()
      */
     @Override
     public abstract Metadata getMetadata() throws DataStoreException;
 
     /**
-     * Searches for a resource identified by the given identifier.
-     * The given identifier should match the following metadata element of a resource:
-     *
-     * <blockquote>{@link Resource#getMetadata() metadata} /
-     * {@link org.apache.sis.metadata.iso.DefaultMetadata#getIdentificationInfo() identificationInfo} /
-     * {@link org.apache.sis.metadata.iso.identification.AbstractIdentification#getCitation() citation} /
-     * {@link org.apache.sis.metadata.iso.citation.DefaultCitation#getIdentifiers() identifier}</blockquote>
-     *
+     * Searches for a resource identified by the given identifier. The given identifier should be the string
+     * representation of the return value of {@link Resource#getIdentifier()} on the desired resource.
      * Implementation may also accept aliases for convenience. For example if the full name of a resource
      * is {@code "foo:bar"}, then this method may accept {@code "bar"} as a synonymous of {@code "foo:bar"}
      * provided that it does not introduce ambiguity.
@@ -272,14 +319,16 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
      * <p>The default implementation verifies if above criterion matches to this {@code DataStore}
      * (which is itself a resource), then iterates recursively over {@link Aggregate} components
      * if this data store is an aggregate.
-     * If a match is found without ambiguity, the associated resource is returned.
-     * Otherwise an exception is thrown. Subclasses are encouraged to override this method with a more efficient
-     * implementation.</p>
+     * If a match is found without ambiguity, the associated resource is returned. Otherwise an exception is thrown.
+     * Subclasses are encouraged to override this method with a more efficient implementation.</p>
      *
      * @param  identifier  identifier of the resource to fetch. Must be non-null.
      * @return resource associated to the given identifier (never {@code null}).
      * @throws IllegalNameException if no resource is found for the given identifier, or if more than one resource is found.
      * @throws DataStoreException if another kind of error occurred while searching resources.
+     *
+     * @see Resource#getIdentifier()
+     * @see FeatureNaming
      */
     public Resource findResource(final String identifier) throws DataStoreException {
         ArgumentChecks.ensureNonEmpty("identifier", identifier);
@@ -287,8 +336,7 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
         if (resource != null) {
             return resource;
         }
-        throw new IllegalNameException(Resources.forLocale(getLocale())
-                .getString(Resources.Keys.ResourceNotFound_2, getDisplayName(), identifier));
+        throw new IllegalNameException(StoreUtilities.resourceNotFound(this, identifier));
     }
 
     /**
@@ -304,15 +352,12 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
             final Map<Resource,Boolean> visited) throws DataStoreException
     {
         if (candidate != null && visited.put(candidate, Boolean.TRUE) == null) {
-            final Metadata metadata = candidate.getMetadata();
-            if (metadata != null) {
-                for (final Identification identification : metadata.getIdentificationInfo()) {
-                    if (identification != null) {                                                   // Paranoiac check.
-                        if (Citations.identifierMatches(identification.getCitation(), null, identifier)) {
-                            return candidate;
-                        }
-                    }
+            GenericName name = candidate.getIdentifier();
+            if (name != null) {
+                do if (identifier.equals(name.toString())) {
+                    return candidate;
                 }
+                while ((name instanceof ScopedName) && name != (name = ((ScopedName) name).tail()));
             }
             if (candidate instanceof Aggregate) {
                 Resource result = null;
@@ -384,4 +429,15 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable {
      */
     @Override
     public abstract void close() throws DataStoreException;
+
+    /**
+     * 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.
+     *
+     * @return a string representation of this data store for debugging purpose.
+     */
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + '[' + getDisplayName() + ']';
+    }
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/IllegalNameException.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/IllegalNameException.java
index d854510..d12951e 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/IllegalNameException.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/IllegalNameException.java
@@ -30,6 +30,9 @@ import org.apache.sis.util.resources.Vocabulary;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see DataStore#findResource(String)
+ *
  * @version 0.8
  * @since   0.8
  * @module
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/Resource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/Resource.java
index f29ce87..5a62011 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/Resource.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/Resource.java
@@ -53,27 +53,29 @@ import org.apache.sis.storage.event.ChangeListener;
  */
 public interface Resource {
     /**
-     * Returns the resource primary identifier.
+     * Returns the resource persistent identifier.
      * This identifier can be used to uniquely identify a resource in the containing {@link DataStore}.
      * For this identifier to be reliable the following conditions must hold:
      *
      * <ul>
-     *   <li>It must be unique in the {@link DataStore} which contains it, if there is one.</li>
-     *   <li>It's value must not change after closing and reopening the {@link DataStore} on the same data.</li>
-     *   <li>This identifier must be consistent with the value returned by {@link #getMetadata()}
-     *       in the field at path {@literal identificationInfo/citation/identifier}.
-     *   </li>
+     *   <li>It shall be unique in the {@link DataStore} which contains it, if there is one.</li>
+     *   <li>It's value shall not change after closing and reopening the {@link DataStore} on the same data.</li>
+     *   <li>It should be consistent with the <code>{@linkplain #getMetadata()}/​identificationInfo/​citation/​identifier</code> value.</li>
      * </ul>
      *
-     * If any of the above conditions is not met, this identifier should be null.
+     * If any of above conditions is not met, then this identifier should be {@code null}.
      * This case may happen when a resource is an intermediate result of an ongoing process
      * or is a temporary resource generated on-the-fly, for example a sensor event.
      *
-     * @return an identifier unique within the data store, or {@code null} if the resource has no reliable identifier.
+     * @return a persistent identifier unique within the data store, or {@code null} if this resource has no such identifier.
+     * @throws DataStoreException if an error occurred while fetching the identifier.
+     *
+     * @see DataStore#getIdentifier()
+     * @see DataStore#findResource(String)
      *
      * @since 1.0
      */
-    GenericName getIdentifier();
+    GenericName getIdentifier() throws DataStoreException;
 
     /**
      * Returns information about this resource.
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 93815d5..77cf42f 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
@@ -16,11 +16,10 @@
  */
 package org.apache.sis.storage;
 
-import org.apache.sis.storage.event.ChangeEvent;
-import org.apache.sis.storage.event.ChangeListener;
 import org.opengis.metadata.Metadata;
 import org.opengis.parameter.ParameterValueGroup;
-import org.opengis.util.GenericName;
+import org.apache.sis.storage.event.ChangeEvent;
+import org.apache.sis.storage.event.ChangeListener;
 
 
 /**
@@ -45,15 +44,6 @@ final strictfp class DataStoreMock extends DataStore {
     }
 
     /**
-     * Mock data store has no identifier.
-     * @return null
-     */
-    @Override
-    public GenericName getIdentifier() {
-        return null;
-    }
-
-    /**
      * Returns the display name specified at construction time.
      */
     @Override
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 fc6aae2..7c8db04 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
@@ -16,36 +16,37 @@
  */
 package org.apache.sis.internal.storage.gpx;
 
-import java.io.UncheckedIOException;
-import java.net.URISyntaxException;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
-import org.apache.sis.internal.storage.AbstractResource;
-import org.apache.sis.internal.storage.xml.stream.StaxDataStore;
-import org.apache.sis.internal.system.DefaultFactories;
-import org.apache.sis.metadata.iso.citation.DefaultCitation;
-import org.apache.sis.metadata.iso.distribution.DefaultFormat;
-import org.apache.sis.setup.GeometryLibrary;
-import org.apache.sis.setup.OptionKey;
-import org.apache.sis.storage.ConcurrentReadException;
-import org.apache.sis.storage.DataStoreContentException;
-import org.apache.sis.storage.DataStoreException;
+import java.io.UncheckedIOException;
+import java.net.URISyntaxException;
+import org.opengis.util.NameFactory;
+import org.opengis.util.FactoryException;
+import org.opengis.geometry.Envelope;
+import org.opengis.metadata.Metadata;
+import org.opengis.metadata.distribution.Format;
 import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.storage.IllegalNameException;
 import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.ConcurrentReadException;
+import org.apache.sis.storage.IllegalNameException;
+import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.internal.storage.AbstractResource;
+import org.apache.sis.internal.storage.xml.stream.StaxDataStore;
+import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Version;
-import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.setup.OptionKey;
+import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.util.iso.DefaultNameFactory;
 import org.apache.sis.util.iso.SimpleInternationalString;
+import org.apache.sis.metadata.iso.citation.DefaultCitation;
+import org.apache.sis.metadata.iso.distribution.DefaultFormat;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
-import org.opengis.geometry.Envelope;
-import org.opengis.metadata.Metadata;
-import org.opengis.metadata.distribution.Format;
-import org.opengis.util.FactoryException;
-import org.opengis.util.GenericName;
-import org.opengis.util.NameFactory;
 
 
 /**
@@ -147,16 +148,6 @@ public final class Store extends StaxDataStore implements FeatureSet {
     }
 
     /**
-     * Returns GPX parent feature type name.
-     *
-     * @return feature type name.
-     */
-    @Override
-    public GenericName getIdentifier() {
-        return types.parent.getName();
-    }
-
-    /**
      * Returns information about the dataset as a whole.
      *
      * @return information about the dataset, or {@code null} if none.


Mime
View raw message