You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2018/12/08 18:01:53 UTC

[sis] 04/04: Implements GridCoverageResource.getSampleDimension() in GeoTIFF reader too. It provides us more tests.

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

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

commit 000b6e0c7d37aa0ac01e2de19f0ac97505d05416
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Dec 8 18:55:38 2018 +0100

    Implements GridCoverageResource.getSampleDimension() in GeoTIFF reader too. It provides us more tests.
---
 .../java/org/apache/sis/coverage/CategoryList.java | 19 +++--
 .../org/apache/sis/coverage/SampleDimension.java   | 63 +++++++++++-----
 .../main/java/org/apache/sis/coverage/ToNaN.java   | 10 +++
 .../main/java/org/apache/sis/setup/OptionKey.java  |  2 +-
 .../org/apache/sis/util/resources/Vocabulary.java  |  5 ++
 .../sis/util/resources/Vocabulary.properties       |  1 +
 .../sis/util/resources/Vocabulary_fr.properties    |  1 +
 .../apache/sis/storage/geotiff/GeoTiffStore.java   | 83 ++++++++++++++++++++--
 .../sis/storage/geotiff/ImageFileDirectory.java    | 27 ++++++-
 .../apache/sis/storage/netcdf/GridResource.java    |  2 +-
 .../apache/sis/internal/storage/folder/Store.java  |  3 +
 .../sis/internal/storage/io/ChannelFactory.java    | 15 +++-
 .../java/org/apache/sis/storage/Aggregate.java     |  2 +-
 .../org/apache/sis/storage/StorageConnector.java   |  7 +-
 14 files changed, 201 insertions(+), 39 deletions(-)

diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
index 8a3ec8a..d85874f 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/CategoryList.java
@@ -107,10 +107,11 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
      * The {@code CategoryList} that describes values after {@linkplain #getTransferFunction() transfer function}
      * has been applied, or if this {@code CategoryList} is already converted then the original {@code CategoryList}.
      * Never null, but may be {@code this} if the transfer function is the identity function.
+     * May also be {@link #EMPTY} if this category list has no quantitative category.
      *
-     * <p>This field establishes a bidirectional navigation between sample values and real values.
-     * This is in contrast with methods named {@code converted()}, which establish a unidirectional
-     * navigation from sample values to real values.</p>
+     * <p>Exempt for the {@link #EMPTY} special case, this field establishes a bidirectional navigation between
+     * sample values and real values. This is in contrast with methods named {@code converted()}, which establish
+     * a unidirectional navigation from sample values to real values.</p>
      *
      * @see Category#converse
      * @see SampleDimension#converse
@@ -198,14 +199,20 @@ final class CategoryList extends AbstractList<Category> implements MathTransform
          */
         Category extrapolation = null;
         if (converse == null) {
-            boolean hasConversion = false;
+            boolean hasConversion   = false;
+            boolean hasQuantitative = false;
             final Category[] convertedCategories = new Category[categories.length];
             for (int i=0; i < convertedCategories.length; i++) {
                 final Category category = categories[i];
-                hasConversion |= (category != category.converse);
+                hasConversion   |= (category != category.converse);
+                hasQuantitative |= (category.converse.range != null);
                 convertedCategories[i] = category.converse;
             }
-            converse = hasConversion ? new CategoryList(convertedCategories, this) : this;
+            if (hasQuantitative) {
+                converse = hasConversion ? new CategoryList(convertedCategories, this) : this;
+            } else {
+                converse = EMPTY;
+            }
         } else {
             for (int i=categories.length; --i >= 0;) {
                 final Category category = categories[i];
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
index d7616b4..cc8bce8 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@ -176,7 +176,7 @@ public class SampleDimension implements Serializable {
         this.name       = name;
         this.background = background;
         this.categories = list;
-        if (list.range == null) {               // !hasQuantitative() inlined since we can not yet invoke that method.
+        if (list.converse.range == null) {      // !hasQuantitative() inlined since we can not yet invoke that method.
             transferFunction = null;
             converse = null;
         } else if (list == list.converse) {
@@ -535,18 +535,30 @@ public class SampleDimension implements Serializable {
         }
 
         /**
-         * Creates a range for the given number. We use the static factory methods instead than the
-         * {@link NumberRange} constructor for sharing existing range instances. This is also a way
+         * Sets the name of the sample dimension as a band number.
+         * This method should be used only when no more descriptive name is available.
+         *
+         * @param  band  sequence identifier of the sample dimension to create.
+         * @return {@code this}, for method call chaining.
+         */
+        public Builder setName(final int band) {
+            dimensionName = Vocabulary.formatInternational(Vocabulary.Keys.Band_1, band);
+            return this;
+        }
+
+        /**
+         * Creates a range for the given minimum and maximum values. We use the static factory methods instead
+         * than the {@link NumberRange} constructor for sharing existing range instances. This is also a way
          * to ensure that the number type is one of the primitive wrappers.
          */
-        private static NumberRange<?> range(final Number sample) {
-            switch (Numbers.getEnumConstant(sample.getClass())) {
-                case Numbers.BYTE:    {byte   v = sample.byteValue();   return NumberRange.create(v, true, v, true);}
-                case Numbers.SHORT:   {short  v = sample.shortValue();  return NumberRange.create(v, true, v, true);}
-                case Numbers.INTEGER: {int    v = sample.intValue();    return NumberRange.create(v, true, v, true);}
-                case Numbers.LONG:    {long   v = sample.longValue();   return NumberRange.create(v, true, v, true);}
-                case Numbers.FLOAT:   {float  v = sample.floatValue();  return NumberRange.create(v, true, v, true);}
-                default:              {double v = sample.doubleValue(); return NumberRange.create(v, true, v, true);}
+        private static NumberRange<?> range(final Class<?> type, final Number minimum, final Number maximum) {
+            switch (Numbers.getEnumConstant(type)) {
+                case Numbers.BYTE:    return NumberRange.create(minimum.byteValue(),   true, maximum.byteValue(),   true);
+                case Numbers.SHORT:   return NumberRange.create(minimum.shortValue(),  true, maximum.shortValue(),  true);
+                case Numbers.INTEGER: return NumberRange.create(minimum.intValue(),    true, maximum.intValue(),    true);
+                case Numbers.LONG:    return NumberRange.create(minimum.longValue(),   true, maximum.longValue(),   true);
+                case Numbers.FLOAT:   return NumberRange.create(minimum.floatValue(),  true, maximum.floatValue(),  true);
+                default:              return NumberRange.create(minimum.doubleValue(), true, maximum.doubleValue(), true);
             }
         }
 
@@ -566,7 +578,7 @@ public class SampleDimension implements Serializable {
             if (name == null) {
                 name = Vocabulary.formatInternational(Vocabulary.Keys.FillValue);
             }
-            final NumberRange<?> samples = range(sample);
+            final NumberRange<?> samples = range(sample.getClass(), sample, sample);
             background = samples.getMinValue();
             toNaN.background = background.doubleValue();
             categories.add(new Category(name, samples, null, null, toNaN));
@@ -673,19 +685,20 @@ public class SampleDimension implements Serializable {
         }
 
         /**
-         * Adds a qualitative category for samples of the given value.
+         * Adds a qualitative category for samples in the given range of values.
          *
          * <div class="note"><b>Implementation note:</b>
          * this convenience method delegates to {@link #addQualitative(CharSequence, NumberRange)}.</div>
          *
-         * @param  name    the category name as a {@link String} or {@link InternationalString} object,
-         *                 or {@code null} for a default "no data" name.
-         * @param  sample  the sample value. Can not be NaN.
+         * @param  name     the category name as a {@link String} or {@link InternationalString} object,
+         *                  or {@code null} for a default "no data" name.
+         * @param  minimum  the minimum sample value, inclusive. Can not be NaN.
+         * @param  maximum  the maximum sample value, inclusive. Can not be NaN.
          * @return {@code this}, for method call chaining.
-         * @throws IllegalArgumentException if the given value is NaN.
+         * @throws IllegalArgumentException if a given value is NaN or if the range is empty.
          */
-        public Builder addQualitative(final CharSequence name, final Number sample) {
-            return addQualitative(name, range(sample));
+        public Builder addQualitative(final CharSequence name, final Number minimum, final Number maximum) {
+            return addQualitative(name, range(Numbers.widestClass(minimum, maximum), minimum, maximum));
         }
 
         /**
@@ -850,5 +863,17 @@ defName:    if (name == null) {
             }
             return new SampleDimension(name, background, categories);
         }
+
+        /**
+         * Reset this builder to the same state than after construction.
+         * The sample dimension name, background values and all categories are discarded.
+         * This method can be invoked when the same builder is reused for creating more than one sample dimension.
+         */
+        public void clear() {
+            dimensionName = null;
+            background    = null;
+            categories.clear();
+            toNaN.clear();
+        }
     }
 }
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/ToNaN.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/ToNaN.java
index 50d040a..4a3fc3e 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/ToNaN.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/ToNaN.java
@@ -49,6 +49,16 @@ final class ToNaN extends HashSet<Integer> implements DoubleToIntFunction {
     }
 
     /**
+     * Sets this function to the same state than after construction.
+     * This method is invoked when the same builder is reused for creating many sample dimensions.
+     */
+    @Override
+    public void clear() {
+        super.clear();
+        background = Double.NaN;
+    }
+
+    /**
      * Returns a NaN ordinal value for the given sample value.
      * The returned value can be given to {@link MathFunctions#toNanFloat(int)}.
      */
diff --git a/core/sis-utility/src/main/java/org/apache/sis/setup/OptionKey.java b/core/sis-utility/src/main/java/org/apache/sis/setup/OptionKey.java
index 63cd642..2aa8cc5 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/setup/OptionKey.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/setup/OptionKey.java
@@ -38,7 +38,7 @@ import org.apache.sis.internal.system.Modules;
  * For example most data file formats read by SIS do not require the user to specify the character encoding, since the
  * encoding it is often given in the file header or in the format specification. However if SIS needs to read plain
  * text files <em>and</em> the default platform encoding is not suitable, then the user can specify the desired encoding
- * explicitely using the {@link #ENCODING} option.
+ * explicitly using the {@link #ENCODING} option.
  *
  * <p>All options are <em>hints</em> and may be silently ignored. For example most {@code DataStore}s will ignore the
  * {@code ENCODING} option if irrelevant to their format, or if the encoding is specified in the data file header.</p>
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
index 83167db..9818540 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java
@@ -107,6 +107,11 @@ public final class Vocabulary extends IndexedResourceBundle {
         public static final short AxisChanges = 8;
 
         /**
+         * Band {0}
+         */
+        public static final short Band_1 = 161;
+
+        /**
          * Barometric altitude
          */
         public static final short BarometricAltitude = 9;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
index 6577d2f..fe381c3 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties
@@ -24,6 +24,7 @@ AngularMinutes          = Minutes
 AngularSeconds          = Seconds
 Attributes              = Attributes
 AxisChanges             = Axis changes
+Band_1                  = Band {0}
 BarometricAltitude      = Barometric altitude
 Cardinality             = Cardinality
 CausedBy_1              = Caused by {0}
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
index 7fe1ebb..60d3d4e 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -31,6 +31,7 @@ AngularMinutes          = Minutes
 AngularSeconds          = Secondes
 Attributes              = Attributs
 AxisChanges             = Changements d\u2019axes
+Band_1                  = Bande {0}
 BarometricAltitude      = Altitude barom\u00e9trique
 Cardinality             = Cardinalit\u00e9
 CausedBy_1              = Caus\u00e9e par {0}
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 cd8f677..ea256a1 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
@@ -17,10 +17,13 @@
 package org.apache.sis.storage.geotiff;
 
 import java.util.Locale;
-import java.io.IOException;
+import java.util.Iterator;
+import java.util.Collection;
+import java.util.NoSuchElementException;
+import java.util.logging.LogRecord;
 import java.net.URI;
+import java.io.IOException;
 import java.nio.charset.Charset;
-import java.util.logging.LogRecord;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.StandardOpenOption;
 import org.opengis.util.NameSpace;
@@ -31,7 +34,8 @@ 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.GridCoverageResource;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.DataStoreException;
@@ -41,6 +45,7 @@ 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.referencing.LazySet;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
 import org.apache.sis.internal.storage.io.IOUtilities;
 import org.apache.sis.internal.storage.MetadataBuilder;
@@ -49,6 +54,7 @@ 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.collection.BackingStoreException;
 import org.apache.sis.util.resources.Errors;
 
 
@@ -62,7 +68,7 @@ import org.apache.sis.util.resources.Errors;
  * @since   0.8
  * @module
  */
-public class GeoTiffStore extends DataStore {
+public class GeoTiffStore extends DataStore implements Aggregate {
     /**
      * The encoding of strings in the metadata. The string specification said that is shall be US-ASCII,
      * but Apache SIS nevertheless let the user specifies an alternative encoding if needed.
@@ -96,6 +102,13 @@ public class GeoTiffStore extends DataStore {
     private Metadata metadata;
 
     /**
+     * Description of images in this GeoTIFF files. This collection is created only when first needed.
+     *
+     * @see #components()
+     */
+    private Collection<GridCoverageResource> components;
+
+    /**
      * Creates a new GeoTIFF store from the given file, URL or stream object.
      * This constructor invokes {@link StorageConnector#closeAllExcept(Object)},
      * keeping open only the needed resource.
@@ -139,8 +152,6 @@ public class GeoTiffStore extends DataStore {
      * (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() {
@@ -229,6 +240,64 @@ public class GeoTiffStore extends DataStore {
     }
 
     /**
+     * Returns descriptions of all images in this GeoTIFF file.
+     * Images are not immediately loaded.
+     *
+     * <p>If an error occurs during iteration in the returned collection,
+     * an unchecked {@link BackingStoreException} will be thrown with a {@link DataStoreException} as its cause.</p>
+     *
+     * @return descriptions of all images in this GeoTIFF file.
+     * @throws DataStoreException if an error occurred while fetching the image descriptions.
+     *
+     * @since 1.0
+     */
+    @Override
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public Collection<GridCoverageResource> components() throws DataStoreException {
+        if (components == null) {
+            components = new LazySet<GridCoverageResource>(new Iterator<GridCoverageResource>() {
+                /** Index of the next image to fetch, or -1 if we fetched all of them. */
+                private int index;
+
+                /** Value to be returned by {@link #next()}, or {@cod null} if not yet determined. */
+                private GridCoverageResource next;
+
+                /** Returns {@code true} if there is more resources. */
+                @Override public boolean hasNext() {
+                    if (next != null) {
+                        return true;
+                    }
+                    final int i = index;
+                    if (i >= 0) {
+                        index = -1;                     // Set now in case of failure.
+                        try {
+                            next = reader().getImageFileDirectory(i);
+                        } catch (IOException e) {
+                            throw new BackingStoreException(errorIO(e));
+                        } catch (DataStoreException e) {
+                            throw new BackingStoreException(e);
+                        }
+                        if (next != null) {
+                            index = i+1;
+                            return true;
+                        }
+                    }
+                    return false;
+                }
+
+                /** Returns the next element. */
+                @Override public GridCoverageResource next() {
+                    if (!hasNext()) throw new NoSuchElementException();
+                    final GridCoverageResource r = next;
+                    next = null;
+                    return r;
+                }
+            });
+        }
+        return components;      // Safe to return because unmodifiable.
+    }
+
+    /**
      * Returns the image at the given index. Images numbering starts at 1.
      *
      * @param  sequence  string representation of the image index, starting at 1.
@@ -236,7 +305,7 @@ public class GeoTiffStore extends DataStore {
      * @throws DataStoreException if the requested image can not be obtained.
      */
     @Override
-    public Resource findResource(final String sequence) throws DataStoreException {
+    public GridCoverageResource findResource(final String sequence) throws DataStoreException {
         Exception cause;
         int index;
         try {
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 edfb70b..022d5c2 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
@@ -29,15 +29,18 @@ import javax.measure.quantity.Length;
 import org.opengis.metadata.citation.DateType;
 import org.opengis.util.FactoryException;
 import org.opengis.util.GenericName;
+import org.opengis.util.InternationalString;
 import org.apache.sis.internal.geotiff.Resources;
 import org.apache.sis.internal.storage.MetadataBuilder;
 import org.apache.sis.internal.storage.AbstractGridResource;
 import org.apache.sis.internal.storage.io.ChannelDataInput;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.math.Vector;
 import org.apache.sis.measure.Units;
 
@@ -329,10 +332,19 @@ final class ImageFileDirectory extends AbstractGridResource {
      *   <li>{@link GridGeometryBuilder#asciiParameters}</li>
      *   <li>{@link GridGeometryBuilder#modelTiePoints}</li>
      * </ul>
+     *
+     * @see #getGridGeometry()
      */
     private GridGeometryBuilder referencing;
 
     /**
+     * The sample dimensions, or {@code null} if not yet created.
+     *
+     * @see #getSampleDimensions()
+     */
+    private List<SampleDimension> sampleDimensions;
+
+    /**
      * Returns {@link #referencing}, created when first needed. We delay its creation since
      * this object is not needed for ordinary TIFF files (i.e. without the GeoTIFF extension).
      */
@@ -1248,8 +1260,21 @@ final class ImageFileDirectory extends AbstractGridResource {
      * Returns the ranges of sample values together with the conversion from samples to real values.
      */
     @Override
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public List<SampleDimension> getSampleDimensions() throws DataStoreContentException {
-        throw new DataStoreContentException("Not supported yet.");
+        if (sampleDimensions == null) {
+            final SampleDimension[] dimensions = new SampleDimension[samplesPerPixel];
+            final SampleDimension.Builder builder = new SampleDimension.Builder();
+            final InternationalString name = Vocabulary.formatInternational(Vocabulary.Keys.Value);
+            for (int band = 0; band < samplesPerPixel;) {
+                builder.addQualitative(name, minValues.get(Math.min(band, minValues.size()-1)),
+                                             maxValues.get(Math.min(band, maxValues.size()-1)));
+                dimensions[band] = builder.setName(++band).build();
+                builder.clear();
+            }
+            sampleDimensions = UnmodifiableArrayList.wrap(dimensions);
+        }
+        return sampleDimensions;        // Safe because unmodifiable.
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
index 68906c6..6b1ba58 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
@@ -210,7 +210,7 @@ final class GridResource extends AbstractGridResource implements ResourceOnFileS
                                 isFillValue = false;                              // Declare only one fill value.
                                 builder.setBackground(name, n);
                             } else {
-                                builder.addQualitative(name, n);
+                                builder.addQualitative(name, n, n);
                             }
                         }
                     }
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 61e1842..1bc6157 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
@@ -32,6 +32,7 @@ import java.nio.file.DirectoryStream;
 import java.nio.file.DirectoryIteratorException;
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.nio.file.OpenOption;
 import org.opengis.util.GenericName;
 import org.opengis.util.NameFactory;
 import org.opengis.util.NameSpace;
@@ -53,6 +54,7 @@ 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.io.ChannelFactory;
 import org.apache.sis.internal.storage.Resources;
 import org.apache.sis.storage.event.ChangeEvent;
 import org.apache.sis.storage.event.ChangeListener;
@@ -310,6 +312,7 @@ class Store extends DataStore implements StoreResource, Aggregate, DirectoryStre
                         connector.setOption(OptionKey.LOCALE,   locale);
                         connector.setOption(OptionKey.TIMEZONE, timezone);
                         connector.setOption(OptionKey.ENCODING, encoding);
+                        connector.setOption(OptionKey.OPEN_OPTIONS, new OpenOption[] {ChannelFactory.REQUIRE_REGULAR_FILE});
                         try {
                             if (componentProvider == null) {
                                 next = DataStores.open(connector);          // May throw UnsupportedStorageException.
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
index 0067cc2..a01fe08 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelFactory.java
@@ -68,7 +68,7 @@ import org.apache.sis.storage.ForwardOnlyStorageException;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -80,6 +80,17 @@ public abstract class ChannelFactory {
             StandardOpenOption.APPEND, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DELETE_ON_CLOSE);
 
     /**
+     * A customized option for instructing {@link #prepare(Object, String, boolean, OpenOption...)}
+     * to not try to open directories or non-existent files. By default, {@link Path} to non-regular
+     * files will cause an {@link IOException} to be thrown. But if this option is provided, then we
+     * will rather behave as if {@link Path} was an unknown type. This will cause store providers to
+     * not try to open that file, which gives the caller a chance to fallback on its own process.
+     */
+    public static final OpenOption REQUIRE_REGULAR_FILE = new OpenOption() {
+        @Override public String toString() {return "REQUIRE_REGULAR_FILE";}
+    };
+
+    /**
      * For subclass constructors.
      */
     ChannelFactory() {
@@ -240,7 +251,7 @@ public abstract class ChannelFactory {
         }
         if (storage instanceof Path) {
             final Path path = (Path) storage;
-            if (Files.isRegularFile(path)) {
+            if (!optionSet.remove(REQUIRE_REGULAR_FILE) || Files.isRegularFile(path)) {
                 return new ChannelFactory() {
                     @Override public ReadableByteChannel readable(String filename, WarningListeners<DataStore> listeners) throws IOException {
                         return Files.newByteChannel(path, optionSet);
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/Aggregate.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/Aggregate.java
index b977658..ef5a8e7 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/Aggregate.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/Aggregate.java
@@ -85,5 +85,5 @@ public interface Aggregate extends Resource {
      * @return all children resources that are components of this aggregate. Never {@code null}.
      * @throws DataStoreException if an error occurred while fetching the components.
      */
-    Collection<Resource> components() throws DataStoreException;
+    Collection<? extends Resource> components() throws DataStoreException;
 }
diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
index f80143b..2360335 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/StorageConnector.java
@@ -32,6 +32,7 @@ import java.nio.ByteBuffer;
 import java.nio.channels.Channel;
 import java.nio.channels.ReadableByteChannel;
 import java.nio.channels.SeekableByteChannel;
+import java.nio.file.NoSuchFileException;
 import javax.imageio.stream.ImageInputStream;
 import javax.imageio.ImageIO;
 import java.sql.Connection;
@@ -836,7 +837,11 @@ public class StorageConnector implements Serializable {
         } catch (DataStoreException e) {
             throw e;
         } catch (Exception e) {
-            throw new DataStoreException(Errors.format(Errors.Keys.CanNotOpen_1, getStorageName()), e);
+            short key = Errors.Keys.CanNotOpen_1;
+            if (e instanceof NoSuchFileException) {
+                key = Errors.Keys.FileNotFound_1;
+            }
+            throw new DataStoreException(Errors.format(key, getStorageName()), e);
         }
         return type.cast(view);
     }