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 2023/11/22 16:30:46 UTC

(sis) 01/03: Partial support of `WritableAggregate` in GeoTIFF store. For now, only adding resources is supported, not removing them.

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 c13e1339eb942fb05a764219d7dadf3043703b8c
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Nov 22 11:23:39 2023 +0100

    Partial support of `WritableAggregate` in GeoTIFF store.
    For now, only adding resources is supported, not removing them.
---
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  36 ++++++-
 .../sis/storage/geotiff/GeoTiffStoreProvider.java  |   5 +-
 .../org/apache/sis/storage/geotiff/Reader.java     |   8 ++
 .../apache/sis/storage/geotiff/WritableStore.java  |  83 +++++++++++++++
 .../org/apache/sis/storage/geotiff/Writer.java     |  16 +++
 .../apache/sis/storage/base/SimpleAggregate.java   |  57 ++++++++++
 .../sis/storage/base/WritableAggregateSupport.java | 118 +++++++++++++++++++++
 .../WritableGridCoverageSupport.java}              |  29 +++--
 .../org/apache/sis/storage/esri/WritableStore.java |   6 +-
 .../apache/sis/storage/image/WritableResource.java |   6 +-
 .../org/apache/sis/storage/internal/Resources.java |   6 ++
 .../sis/storage/internal/Resources.properties      |   1 +
 .../sis/storage/internal/Resources_fr.properties   |   1 +
 13 files changed, 346 insertions(+), 26 deletions(-)

diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
index 55befa758f..50abb3c10c 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -50,6 +50,7 @@ import org.apache.sis.storage.IllegalNameException;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.base.StoreUtilities;
 import org.apache.sis.storage.base.URIDataStore;
+import org.apache.sis.storage.base.GridResourceWrapper;
 import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
 import org.apache.sis.storage.event.StoreListeners;
@@ -513,13 +514,15 @@ public class GeoTiffStore extends DataStore implements Aggregate {
      */
     private Writer writer() throws DataStoreException, IOException {
         assert Thread.holdsLock(this);
+        final Reader r = reader;
         Writer w = writer;
         if (w == null) {
-            final Reader r = reader;
             if (r == null) {
                 throw new DataStoreClosedException(getLocale(), Constants.GEOTIFF, StandardOpenOption.WRITE);
             }
             writer = w = new Writer(r);
+        } else if (r != null) {
+            w.moveAfterExisting(r);
         }
         return w;
     }
@@ -677,15 +680,17 @@ public class GeoTiffStore extends DataStore implements Aggregate {
      * @param  image     the image to encode.
      * @param  grid      mapping from pixel coordinates to "real world" coordinates, or {@code null} if none.
      * @param  metadata  title, author and other information, or {@code null} if none.
+     * @return the effectively added resource. Using this resource may cause data to be reloaded.
      * @throws ReadOnlyStorageException if this data store is read-only.
      * @throws DataStoreException if the given {@code image} has a property which is not supported by this writer,
      *         or if an error occurred while writing to the output stream.
      *
      * @since 1.5
      */
-    public synchronized void append(final RenderedImage image, final GridGeometry grid, final Metadata metadata)
+    public synchronized GridCoverageResource append(final RenderedImage image, final GridGeometry grid, final Metadata metadata)
             throws DataStoreException
     {
+        final int index;
         try {
             @SuppressWarnings("LocalVariableHidesMemberVariable") final Writer writer = writer();
             @SuppressWarnings("LocalVariableHidesMemberVariable") final Reader reader = this.reader;
@@ -700,12 +705,34 @@ public class GeoTiffStore extends DataStore implements Aggregate {
             if (reader != null) {
                 reader.offsetOfWrittenIFD(offsetIFD);
             }
+            index = writer.imageIndex++;
         } catch (IOException e) {
             throw new DataStoreException(errors().getString(Errors.Keys.CanNotWriteFile_2, Constants.GEOTIFF, getDisplayName()), e);
         }
         if (components != null) {
             components.incrementSize(1);
         }
+        /*
+         * Returns a thin wrapper with only a reference to this store and the image index.
+         * The actual loading of the effectively added resource will be done only if requested.
+         */
+        return new GridResourceWrapper() {
+            /** The lock to use for synchronization purposes. */
+            @Override protected Object getSynchronizationLock() {
+                return GeoTiffStore.this;
+            }
+
+            /** Loads the effectively added resource when first requested. */
+            @Override protected GridCoverageResource createSource() throws DataStoreException {
+                try {
+                    synchronized (GeoTiffStore.this) {
+                        return reader().getImage(index);
+                    }
+                } catch (IOException e) {
+                    throw new DataStoreException(errorIO(e));
+                }
+            }
+        };
     }
 
     /**
@@ -715,6 +742,7 @@ public class GeoTiffStore extends DataStore implements Aggregate {
      *
      * @param  coverage  the grid coverage to encode.
      * @param  metadata  title, author and other information, or {@code null} if none.
+     * @return the effectively added resource. Using this resource may cause data to be reloaded.
      * @throws SubspaceNotSpecifiedException if the given grid coverage is not a two-dimensional slice.
      * @throws ReadOnlyStorageException if this data store is read-only.
      * @throws DataStoreException if the given {@code image} has a property which is not supported by this writer,
@@ -722,8 +750,8 @@ public class GeoTiffStore extends DataStore implements Aggregate {
      *
      * @since 1.5
      */
-    public void append(final GridCoverage coverage, final Metadata metadata) throws DataStoreException {
-        append(coverage.render(null), coverage.getGridGeometry(), metadata);
+    public GridCoverageResource append(final GridCoverage coverage, final Metadata metadata) throws DataStoreException {
+        return append(coverage.render(null), coverage.getGridGeometry(), metadata);
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
index e8fa8bb3bc..573b40de9f 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
@@ -53,7 +53,7 @@ import org.apache.sis.parameter.ParameterBuilder;
  */
 @StoreMetadata(formatName    = Constants.GEOTIFF,
                fileSuffixes  = {"tiff", "tif"},
-               capabilities  = {Capability.READ, Capability.WRITE},
+               capabilities  = {Capability.READ, Capability.WRITE, Capability.CREATE},
                resourceTypes = {Aggregate.class, GridCoverageResource.class})
 public class GeoTiffStoreProvider extends DataStoreProvider {
     /**
@@ -162,6 +162,9 @@ public class GeoTiffStoreProvider extends DataStoreProvider {
      */
     @Override
     public DataStore open(final StorageConnector connector) throws DataStoreException {
+        if (URIDataStore.Provider.isWritable(connector, false)) {
+            return new WritableStore(this, connector);
+        }
         return new GeoTiffStore(this, connector);
     }
 
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java
index cbc96163e9..c6f7f390aa 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java
@@ -394,6 +394,14 @@ final class Reader extends IOBase {
         }
     }
 
+    /**
+     * Returns the number of images currently in the cache. This is not necessarily
+     * the total number of images in the TIFF file, unless {@link #endOfFile} is true.
+     */
+    final int getImageCacheSize() {
+        return images.size();
+    }
+
     /**
      * Returns the potentially pyramided <cite>Image File Directories</cite> (IFDs) at the given index.
      * If the pyramid has already been initialized, then it is returned.
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/WritableStore.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/WritableStore.java
new file mode 100644
index 0000000000..b7a7ea79d8
--- /dev/null
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/WritableStore.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.geotiff;
+
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.WritableAggregate;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.base.WritableAggregateSupport;
+
+
+/**
+ * A GeoTIFF data store with write capabilities.
+ *
+ * @author  Erwan Roussel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+final class WritableStore extends GeoTiffStore implements WritableAggregate {
+    /**
+     * 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.
+     *
+     * @param  provider   the factory that created this {@code WritableStore} instance, or {@code null} if unspecified.
+     * @param  connector  information about the storage (URL, stream, <i>etc</i>).
+     * @throws DataStoreException if an error occurred while opening the GeoTIFF file.
+     */
+    public WritableStore(final GeoTiffStoreProvider provider, final StorageConnector connector) throws DataStoreException {
+        super(provider, connector);
+    }
+
+    /**
+     * Adds a new {@code GridCoverageResource} in this {@code Aggregate}.
+     * The given {@link Resource} will be copied, and the <cite>effectively added</cite> resource returned.
+     *
+     * @param  resource  the resource to copy in this {@code Aggregate}.
+     * @return the effectively added resource. Using this resource may cause data to be reloaded.
+     * @throws DataStoreException if the given resource cannot be stored in this {@code Aggregate}.
+     */
+    @Override
+    public Resource add(final Resource resource) throws DataStoreException {
+        final var helper = new WritableAggregateSupport(this);
+        if (resource instanceof Aggregate) {
+            return helper.writeComponents((Aggregate) resource);
+        }
+        final GridCoverageResource gr = helper.asGridCoverage(resource);
+        return append(gr.read(null, null), gr.getMetadata());
+    }
+
+    /**
+     * Removes a {@code Resource} from this {@code Aggregate}.
+     * The given resource should be one of the instances returned by {@link #components()}.
+     * This operation is destructive in two aspects:
+     *
+     * <ul>
+     *   <li>The {@link Resource} and it's data will be deleted from the {@link DataStore}.</li>
+     *   <li>The given resource may become invalid and should not be used anymore after this method call.</li>
+     * </ul>
+     *
+     * @param  resource  child resource to remove from this {@code Aggregate}.
+     * @throws DataStoreException if the given resource could not be removed.
+     */
+    @Override
+    public void remove(Resource resource) throws DataStoreException {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+}
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
index 95d3d27a08..fd15ba2fcf 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
@@ -133,6 +133,12 @@ final class Writer extends IOBase implements Flushable {
      */
     private final boolean isBigTIFF;
 
+    /**
+     * Index of the image to write. This information is not needed by the writer, but is
+     * needed by {@link WritableStore} for determining the "effectively added resource".
+     */
+    int imageIndex;
+
     /**
      * Offset where to write the next image, or {@code null} if writing a mandatory image (the first one).
      * If null, the IFD offset is assumed already written and the {@linkplain #output} already at that position.
@@ -209,8 +215,18 @@ final class Writer extends IOBase implements Flushable {
         } catch (ClassCastException e) {
             throw new ReadOnlyStorageException(store.readOrWriteOnly(0), e);
         }
+        moveAfterExisting(reader);
+    }
+
+    /**
+     * Prepares the writer to write after the last images.
+     *
+     * @param  reader  the reader of images.
+     */
+    final void moveAfterExisting(final Reader reader) throws IOException, DataStoreException {
         Class<? extends Number> type = isBigTIFF ? Long.class : Integer.class;
         nextIFD = UpdatableWrite.ofZeroAt(reader.offsetOfWritableIFD(), type);
+        imageIndex = reader.getImageCacheSize();
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/SimpleAggregate.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/SimpleAggregate.java
new file mode 100644
index 0000000000..203e936da8
--- /dev/null
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/SimpleAggregate.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.base;
+
+import java.util.List;
+import java.util.Collection;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.AbstractResource;
+import org.apache.sis.util.internal.UnmodifiableArrayList;
+
+
+/**
+ * An aggregate with a list of components determined in advance.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+final class SimpleAggregate extends AbstractResource implements Aggregate {
+    /**
+     * Components of this aggregate as an unmodifiable collection.
+     */
+    private final List<Resource> components;
+
+    /**
+     * Creates a new resource, potentially as a child of another resource.
+     *
+     * @param  parent      the parent resource, or {@code null} if none.
+     * @param  components  components of this aggregate. This collection is copied.
+     */
+    public SimpleAggregate(final Resource parent, final Collection<? extends Resource> components) {
+        super(parent);
+        this.components = UnmodifiableArrayList.wrap(components.toArray(Resource[]::new));
+    }
+
+    /**
+     * {@return the components of this aggregate}.
+     */
+    @Override
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public Collection<Resource> components() {
+        return components;
+    }
+}
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/WritableAggregateSupport.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/WritableAggregateSupport.java
new file mode 100644
index 0000000000..651a8666ef
--- /dev/null
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/WritableAggregateSupport.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.base;
+
+import java.util.Locale;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.lang.reflect.Modifier;
+import org.apache.sis.storage.Aggregate;
+import org.opengis.util.GenericName;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.Localized;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.WritableAggregate;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.IncompatibleResourceException;
+import org.apache.sis.storage.internal.Resources;
+import org.apache.sis.util.collection.BackingStoreException;
+
+
+/**
+ * Helper classes for the management of {@link WritableAggregate}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class WritableAggregateSupport implements Localized {
+    /**
+     * The resource where to write.
+     */
+    private final WritableAggregate target;
+
+    /**
+     * Creates a new helper class.
+     *
+     * @param  target  the resource where to write.
+     */
+    public WritableAggregateSupport(final WritableAggregate target) {
+        this.target = target;
+    }
+
+    /**
+     * {@return the locale used by the targetresource for error messages, or {@code null} if unknown}.
+     */
+    @Override
+    public final Locale getLocale() {
+        return (target instanceof Localized) ? ((Localized) target).getLocale() : null;
+    }
+
+    /**
+     * Writes the components of the given aggregate.
+     *
+     * @param  resource  the aggregate to write.
+     * @return the effectively added aggregate.
+     * @throws DataStoreException if an error occurred while writing a component.
+     */
+    public Resource writeComponents(final Aggregate resource) throws DataStoreException {
+        try {
+            final Collection<? extends Resource> components = resource.components();
+            final var effectives = new ArrayList<Resource>(components.size());
+            for (final Resource component : components) {
+                effectives.add(target.add(component));
+            }
+            return new SimpleAggregate(target, effectives);
+        } catch (BackingStoreException e) {
+            throw e.unwrapOrRethrow(DataStoreException.class);
+        }
+    }
+
+    /**
+     * Returns the given resource as a grid coverage, or throws an exception if it cannot be cast.
+     *
+     * @param  resource  the resource which is required to be a grid coverage resource.
+     * @return the given resource after cast.
+     * @throws IncompatibleResourceException if the given resource is not for a grid coverage.
+     */
+    public GridCoverageResource asGridCoverage(final Resource resource) throws DataStoreException {
+        ArgumentChecks.ensureNonNull("resource", resource);
+        if (resource instanceof GridCoverageResource) {
+            return (GridCoverageResource) resource;
+        }
+        throw new IncompatibleResourceException(message(GridCoverageResource.class, resource));
+    }
+
+    /**
+     * Returns the error message for a resource that cannot be added to an aggregate.
+     *
+     * @param  expected  the expected type of resource.
+     * @param  actual    the actual resource.
+     * @return the error message to give to the exception to be thrown.
+     */
+    private String message(final Class<? extends Resource> expected, final Resource actual) throws DataStoreException {
+        Class<? extends Resource> type = actual.getClass();
+        for (Class<? extends Resource> t : Classes.getLeafInterfaces(type, Resource.class)) {
+            if (Modifier.isPublic(t.getModifiers())) {
+                type = t;
+                break;
+            }
+        }
+        return Resources.forLocale(getLocale()).getString(Resources.Keys.IllegalResourceTypeForAggregate_3,
+                target.getIdentifier().map(GenericName::toString).orElse(Classes.getShortName(actual.getClass())), expected, type);
+    }
+}
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/WritableResourceSupport.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/WritableGridCoverageSupport.java
similarity index 91%
rename from endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/WritableResourceSupport.java
rename to endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/WritableGridCoverageSupport.java
index a36e06002e..02f9b86e97 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/WritableResourceSupport.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/WritableGridCoverageSupport.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.sis.storage.internal;
+package org.apache.sis.storage.base;
 
 import java.util.Locale;
 import java.io.IOException;
@@ -34,6 +34,7 @@ import org.apache.sis.storage.ReadOnlyStorageException;
 import org.apache.sis.storage.ResourceAlreadyExistsException;
 import org.apache.sis.storage.IncompatibleResourceException;
 import org.apache.sis.storage.WritableGridCoverageResource;
+import org.apache.sis.storage.internal.Resources;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
@@ -52,11 +53,11 @@ import org.opengis.coverage.CannotEvaluateException;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-public final class WritableResourceSupport implements Localized {
+public final class WritableGridCoverageSupport implements Localized {
     /**
      * The resource where to write.
      */
-    private final GridCoverageResource resource;
+    private final GridCoverageResource target;
 
     /**
      * {@code true} if the {@link WritableGridCoverageResource.CommonOption.REPLACE} option has been specified.
@@ -73,11 +74,11 @@ public final class WritableResourceSupport implements Localized {
     /**
      * Creates a new helper class for the given options.
      *
-     * @param  resource  the resource where to write.
-     * @param  options   configuration of the write operation.
+     * @param  target   the resource where to write.
+     * @param  options  configuration of the write operation.
      */
-    public WritableResourceSupport(final GridCoverageResource resource, final WritableGridCoverageResource.Option[] options) {
-        this.resource = resource;
+    public WritableGridCoverageSupport(final GridCoverageResource target, final WritableGridCoverageResource.Option[] options) {
+        this.target = target;
         ArgumentChecks.ensureNonNull("options", options);
         for (final WritableGridCoverageResource.Option option : options) {
             replace |= WritableGridCoverageResource.CommonOption.REPLACE.equals(option);
@@ -91,13 +92,11 @@ public final class WritableResourceSupport implements Localized {
     }
 
     /**
-     * Returns the locale used by the resource for error messages, or {@code null} if unknown.
-     *
-     * @return the locale used by the resource for error messages, or {@code null} if unknown.
+     * {@return the locale used by the target resource for error messages, or {@code null} if unknown}.
      */
     @Override
     public final Locale getLocale() {
-        return (resource instanceof Localized) ? ((Localized) resource).getLocale() : null;
+        return (target instanceof Localized) ? ((Localized) target).getLocale() : null;
     }
 
     /**
@@ -144,7 +143,7 @@ public final class WritableResourceSupport implements Localized {
         } else if (replace || isEmpty(input)) {
             return true;
         } else {
-            Object identifier = resource.getIdentifier().orElse(null);
+            Object identifier = target.getIdentifier().orElse(null);
             if (identifier == null && input != null) identifier = input.filename;
             throw new ResourceAlreadyExistsException(Resources.forLocale(getLocale())
                     .getString(Resources.Keys.ResourceAlreadyExists_1, identifier));
@@ -175,7 +174,7 @@ public final class WritableResourceSupport implements Localized {
      * @throws DataStoreException if an error occurred while reading or updating the coverage.
      */
     public final GridCoverage update(final GridCoverage coverage) throws DataStoreException {
-        final GridCoverage existing = resource.read(null, null);
+        final GridCoverage existing = target.read(null, null);
         final CoverageCombiner combiner = new CoverageCombiner(existing);
         try {
             if (!combiner.acceptAll(coverage)) {
@@ -217,8 +216,8 @@ public final class WritableResourceSupport implements Localized {
      * @throws DataStoreException if an error occurred while preparing the error message.
      */
     public final String canNotWrite() throws DataStoreException {
-        Object identifier = resource.getIdentifier().orElse(null);
-        if (identifier == null) identifier = Classes.getShortClassName(resource);
+        Object identifier = target.getIdentifier().orElse(null);
+        if (identifier == null) identifier = Classes.getShortClassName(target);
         return Resources.forLocale(getLocale()).getString(Resources.Keys.CanNotWriteResource_1, identifier);
     }
 
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/WritableStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/WritableStore.java
index 0ecbda3df3..b0eaced8f3 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/WritableStore.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/WritableStore.java
@@ -33,7 +33,7 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreReferencingException;
 import org.apache.sis.storage.WritableGridCoverageResource;
 import org.apache.sis.storage.IncompatibleResourceException;
-import org.apache.sis.storage.internal.WritableResourceSupport;
+import org.apache.sis.storage.base.WritableGridCoverageSupport;
 import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
@@ -114,7 +114,7 @@ final class WritableStore extends AsciiGridStore implements WritableGridCoverage
      */
     private static SequenceType getAffineCoefficients(
             final Map<String,Object> header, final GridGeometry gg,
-            final WritableResourceSupport h) throws DataStoreException
+            final WritableGridCoverageSupport h) throws DataStoreException
     {
         String xll = XLLCORNER;
         String yll = YLLCORNER;
@@ -190,7 +190,7 @@ final class WritableStore extends AsciiGridStore implements WritableGridCoverage
      */
     @Override
     public synchronized void write(GridCoverage coverage, final Option... options) throws DataStoreException {
-        final WritableResourceSupport h = new WritableResourceSupport(this, options);   // Does argument validation.
+        final var h = new WritableGridCoverageSupport(this, options);       // Does argument validation.
         final int band = 0;                                 // May become configurable in a future version.
         try {
             /*
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableResource.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableResource.java
index 761efdb517..18999f115b 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableResource.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WritableResource.java
@@ -23,7 +23,7 @@ import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.WritableGridCoverageResource;
-import org.apache.sis.storage.internal.WritableResourceSupport;
+import org.apache.sis.storage.base.WritableGridCoverageSupport;
 import org.apache.sis.storage.internal.Resources;
 import org.apache.sis.storage.event.StoreListeners;
 
@@ -53,8 +53,8 @@ final class WritableResource extends WorldFileResource implements WritableGridCo
      */
     @Override
     public void write(GridCoverage coverage, final Option... options) throws DataStoreException {
-        final WritableResourceSupport h = new WritableResourceSupport(this, options);   // Does argument validation.
-        final WritableStore store = (WritableStore) store();
+        final var h = new WritableGridCoverageSupport(this, options);       // Does argument validation.
+        final var store = (WritableStore) store();
         try {
             synchronized (store) {
                 if (getImageIndex() != WorldFileStore.MAIN_IMAGE || (store.isMultiImages() != 0 && !h.replace(null))) {
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
index 4c22d38232..63506fd42f 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
@@ -275,6 +275,12 @@ public class Resources extends IndexedResourceBundle {
          */
         public static final short IllegalOutputTypeForWriter_2 = 9;
 
+        /**
+         * The aggregate “{0}” does not accept resources of type ‘{2}’. An instance of ‘{1}’ was
+         * expected.
+         */
+        public static final short IllegalResourceTypeForAggregate_3 = 80;
+
         /**
          * All coverages must have the same grid geometry.
          */
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
index 977693931c..f76fb8118f 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
@@ -62,6 +62,7 @@ IllegalEventType_1                = This resource should not fire events of type
 IllegalFeatureType_2              = The {0} data store does not accept features of type \u201c{1}\u201d.
 IllegalInputTypeForReader_2       = The {0} reader does not accept inputs of type \u2018{1}\u2019.
 IllegalOutputTypeForWriter_2      = The {0} writer does not accept outputs of type \u2018{1}\u2019.
+IllegalResourceTypeForAggregate_3 = The aggregate \u201c{0}\u201d does not accept resources of type \u2018{2}\u2019. An instance of \u2018{1}\u2019 was expected.
 IncompatibleGridGeometry          = All coverages must have the same grid geometry.
 InconsistentNameComponents_2      = Components of the \u201c{1}\u201d name are inconsistent with those of the name previously binded in \u201c{0}\u201d data store.
 InvalidExpression_2               = Invalid or unsupported \u201c{1}\u201d expression at index {0}.
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties
index c096e18dfc..b950c2eb63 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties
@@ -67,6 +67,7 @@ IllegalEventType_1                = Cette ressource ne devrait pas lancer des \u
 IllegalFeatureType_2              = Le format {0} ne stocke pas de donn\u00e9es de type \u00ab\u202f{1}\u202f\u00bb.
 IllegalInputTypeForReader_2       = Le lecteur {0} n\u2019accepte pas des entr\u00e9s de type \u2018{1}\u2019.
 IllegalOutputTypeForWriter_2      = L\u2019encodeur {0} n\u2019accepte pas des sorties de type \u2018{1}\u2019.
+IllegalResourceTypeForAggregate_3 = L\u2019agr\u00e9gat \u00ab\u202f{0}\u202f\u00bb n\u2019accepte pas des ressources de type \u2018{2}\u2019. Une instance de \u2018{1}\u2019 \u00e9tait attendue.
 IncompatibleGridGeometry          = Toutes les couvertures de donn\u00e9es doivent avoir la m\u00eame g\u00e9om\u00e9trie de grille.
 InvalidExpression_2               = Expression \u00ab\u202f{1}\u202f\u00bb invalide ou non-support\u00e9e \u00e0 l\u2019index {0}.
 InconsistentNameComponents_2      = Les \u00e9l\u00e9ments qui composent le nom \u00ab\u202f{1}\u202f\u00bb ne sont pas coh\u00e9rents avec ceux du nom qui avait \u00e9t\u00e9 pr\u00e9c\u00e9demment li\u00e9 dans les donn\u00e9es de \u00ab\u202f{0}\u202f\u00bb.