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 556ca7b  Consolidation of the way pixel interleaved sample models are 
constructed for netCDF variables where one dimension is interpreted as bands. 
https://issues.apache.org/jira/browse/SIS-449
556ca7b is described below

commit 556ca7bc4b7d533621f19f7240b385bc8a639e28
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Mar 26 00:33:39 2019 +0100

    Consolidation of the way pixel interleaved sample models are constructed 
for netCDF variables where one dimension is interpreted as bands.
    https://issues.apache.org/jira/browse/SIS-449
---
 .../java/org/apache/sis/coverage/CategoryList.java |  19 ++-
 .../org/apache/sis/coverage/SampleDimension.java   |   2 +-
 .../apache/sis/coverage/grid/ImageRenderer.java    | 130 ++++++++++++-----
 .../sis/internal/raster/ColorModelFactory.java     |  21 ++-
 .../apache/sis/internal/raster/RasterFactory.java  |  27 ++--
 .../org/apache/sis/coverage/CategoryListTest.java  |  12 +-
 .../org/apache/sis/internal/netcdf/Raster.java     |  15 +-
 .../apache/sis/internal/netcdf/RasterResource.java | 161 +++++++++++++--------
 .../org/apache/sis/internal/netcdf/Variable.java   |  14 ++
 .../sis/internal/storage/AbstractGridResource.java |   2 +-
 .../sis/storage/DataStoreReferencingException.java |  10 +-
 .../org/apache/sis/storage/WritableAggregate.java  |  10 +-
 12 files changed, 286 insertions(+), 137 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 4242da6..37def06 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
@@ -159,13 +159,12 @@ final class CategoryList extends AbstractList<Category> 
implements MathTransform
      * The {@code categories} array should contain at least one element,
      * otherwise the {@link #EMPTY} constant should be used.
      *
-     * @param  categories  the list of categories. May be empty, but can not 
be null.
-     *                     This array is not cloned and is modified in-place.
+     * @param  categories  the list of categories. This array is not cloned 
and is modified in-place.
      * @param  converse    if we are creating the list of categories after 
conversion from samples to real values,
      *                     the original list before conversion. Otherwise 
{@code null}.
      * @throws IllegalArgumentException if two or more categories have 
overlapping sample value range.
      */
-    CategoryList(final Category[] categories, CategoryList converse) {
+    private CategoryList(final Category[] categories, CategoryList converse) {
         this.categories = categories;
         final int count = categories.length;
         /*
@@ -307,6 +306,20 @@ final class CategoryList extends AbstractList<Category> 
implements MathTransform
     }
 
     /**
+     * Constructs a category list using the specified array of categories.
+     * The {@code categories} array should contain at least one element,
+     * otherwise the {@link #EMPTY} constant should be used.
+     *
+     * <p>This is defined as a static method for allowing the addition of a 
caching mechanism in the future if desired.</p>
+     *
+     * @param  categories  the list of categories. This array is not cloned 
and is modified in-place.
+     * @throws IllegalArgumentException if two or more categories have 
overlapping sample value range.
+     */
+    static CategoryList create(final Category[] categories) {
+        return new CategoryList(categories, null);
+    }
+
+    /**
      * Returns the <cite>transfer function</cite> from sample values to real 
values, including conversion
      * of "no data" values to NaNs. Callers shall ensure that there is at 
least one quantitative category
      * before to invoke this method.
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 8d3e220..3f80221 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
@@ -168,7 +168,7 @@ public class SampleDimension implements Serializable {
         if (categories.isEmpty()) {
             list = CategoryList.EMPTY;
         } else {
-            list = new CategoryList(categories.toArray(new 
Category[categories.size()]), null);
+            list = CategoryList.create(categories.toArray(new 
Category[categories.size()]));
         }
         this.name       = name;
         this.background = background;
diff --git 
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java 
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
index b46fdd3..b505598 100644
--- 
a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
+++ 
b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java
@@ -93,7 +93,7 @@ public class ImageRenderer {
      *
      * <div class="note"><b>Note:</b> if those offsets exceed 32 bits integer 
capacity, then it may not be possible to build
      * an image for given {@code sliceExtent} from a single {@link 
DataBuffer}, because accessing sample values would exceed
-     * the* capacity of index in Java arrays. In those cases the image needs 
to be tiled.</div>
+     * the capacity of index in Java arrays. In those cases the image needs to 
be tiled.</div>
      */
     private final long offsetX, offsetY;
 
@@ -126,11 +126,19 @@ public class ImageRenderer {
     private final int height;
 
     /**
-     * Number of data elements between two samples in the data {@link 
#buffer}. This value is implicitly 1 in Java2D since
-     * {@link java.awt.image} supports <cite>pixel stride</cite> and 
<cite>scanline stride</cite> in {@link SampleModel},
-     * but does not support stride at the {@link DataBuffer} level. This is 
theoretically not needed since "sample stride"
-     * can be represented as {@link #pixelStride}. We allow this concept for 
the convenience of this builder, but at the
-     * end this value is incorporated into the pixel stride.
+     * Number of data elements between two samples in the data {@link 
#buffer}. A <cite>sample stride</cite> is defined
+     * in this class as the number of data elements between two samples in the 
data {@link #buffer}. This is implicitly
+     * 1 in Java2D because {@link java.awt.image} supports <cite>pixel 
stride</cite> and <cite>scanline stride</cite>
+     * in {@link SampleModel}, but does not support stride in {@link 
DataBuffer} banks. This is theoretically not needed
+     * because "sample stride" can be represented as {@link #pixelStride}. 
This is what we do in the end, but the concept
+     * of "sample stride" needs to exist temporarily in this builder before 
the final pixel stride is computed.
+     *
+     * <div class="note"><b>Note:</b>
+     * this stride is <strong>not</strong> equivalent to applying a 
subsampling on the image, because we do not divide
+     * the image width or height by the given stride. This field should be 
used only for describing a particular layout
+     * of data in the buffers.</div>
+     *
+     * @see #setInterleavedPixelOffsets(int, int[])
      */
     private int sampleStride;
 
@@ -141,7 +149,7 @@ public class ImageRenderer {
      *
      * @see java.awt.image.ComponentSampleModel#pixelStride
      */
-    private int pixelStride;
+    private final int pixelStride;
 
     /**
      * Number of data elements between a given sample and the corresponding 
sample in the same column of the next line.
@@ -150,7 +158,7 @@ public class ImageRenderer {
      *
      * @see java.awt.image.ComponentSampleModel#scanlineStride
      */
-    private int scanlineStride;
+    private final int scanlineStride;
 
     /**
      * The sample dimensions, to be used for defining the bands.
@@ -158,6 +166,22 @@ public class ImageRenderer {
     private final SampleDimension[] bands;
 
     /**
+     * Offset to add to index of sample values in each band in order to reach 
the value in the {@link DataBuffer} bank.
+     * This is closely related to {@link 
java.awt.image.ComponentSampleModel#bandOffsets} but not identical, because of
+     * the following differences:
+     *
+     * <ul>
+     *   <li>Another offset for {@link #offsetX} and {@link #offsetY} may need 
to be added
+     *       before to give the {@code bandOffsets} to {@link SampleModel} 
constructor.</li>
+     *   <li>If null, a default value is inferred depending on whether the 
{@link SampleModel}
+     *       to construct is banded or interleaved.</li>
+     * </ul>
+     *
+     * @see #setInterleavedPixelOffsets(int, int[])
+     */
+    private int[] bandOffsets;
+
+    /**
      * Bank indices for each band, or {@code null} for 0, 1, 2, 3….
      * If non-null, this array length must be equal to {@link #bands} array 
length.
      */
@@ -222,7 +246,6 @@ public class ImageRenderer {
          * At this point, the RenderedImage properties have been computed on 
the assumption
          * that the returned image will be a single tile. Now compute 
SampleModel properties.
          */
-        this.sampleStride = 1;
         long pixelStride  = 1;
         for (int i=0; i<xd; i++) {
             pixelStride = Math.multiplyExact(pixelStride, source.getSize(i));
@@ -247,11 +270,15 @@ public class ImageRenderer {
 
     /**
      * Ensures that the given number is equals to the expected number of bands.
+     * The given number shall be either 1 (case of interleaved sample model) or
+     * {@link #getNumBands()} (case of banded sample model).
      */
-    private void ensureExpectedBandCount(final int n) {
-        final int e = getNumBands();
-        if (n != e) {
-            throw new 
MismatchedCoverageRangeException(Resources.format(Resources.Keys.UnexpectedNumberOfBands_2,
 e, n));
+    private void ensureExpectedBandCount(final int n, final boolean acceptOne) 
{
+        if (!(n == 1 & acceptOne)) {
+            final int e = getNumBands();
+            if (n != e) {
+                throw new 
MismatchedCoverageRangeException(Resources.format(Resources.Keys.UnexpectedNumberOfBands_2,
 e, n));
+            }
         }
     }
 
@@ -277,7 +304,7 @@ public class ImageRenderer {
      */
     public void setData(final DataBuffer data) {
         ArgumentChecks.ensureNonNull("data", data);
-        ensureExpectedBandCount(data.getNumBanks());
+        ensureExpectedBandCount(data.getNumBanks(), true);
         buffer = data;
     }
 
@@ -310,7 +337,7 @@ public class ImageRenderer {
      */
     public void setData(final int dataType, final Buffer... data) {
         ArgumentChecks.ensureNonNull("data", data);
-        ensureExpectedBandCount(data.length);
+        ensureExpectedBandCount(data.length, true);
         final DataBuffer banks = RasterFactory.wrap(dataType, data);
         if (banks == null) {
             throw new 
IllegalArgumentException(Resources.format(Resources.Keys.UnknownDataType_1, 
dataType));
@@ -336,7 +363,7 @@ public class ImageRenderer {
      */
     public void setData(final Vector... data) {
         ArgumentChecks.ensureNonNull("data", data);
-        ensureExpectedBandCount(data.length);
+        ensureExpectedBandCount(data.length, true);
         final Buffer[] buffers = new Buffer[data.length];
         int dataType = DataBuffer.TYPE_UNDEFINED;
         for (int i=0; i<data.length; i++) {
@@ -355,26 +382,28 @@ public class ImageRenderer {
     }
 
     /**
-     * Specifies the number of data elements between two samples in the 
vectors specified by {@code setData(…)} methods.
-     * The default value is 1. A value of 2 (for example) instructs {@code 
ImageRenderer} to use the first value of the
-     * given data vectors, skip a value, use the next value, <i>etc.</i> In 
other words, this method applies a subsampling
-     * on the vectors specified to {@link #setData(Vector...)} or {@link 
#setData(int, Buffer...)}.
+     * Specifies the offsets to add to sample index in each band in order to 
reach the sample value in the {@link DataBuffer} bank.
+     * This method should be invoked when the data given to {@code setData(…)} 
contains only one {@link Vector}, {@link Buffer} or
+     * {@link DataBuffer} bank, and the bands in that unique bank are 
interleaved.
      *
-     * @param  stride  the number of data elements between each sample values 
in the data vectors.
-     * @throws ArithmeticException if the given stride is too large.
+     * <div class="note"><b>Example:</b>
+     * for an image having three bands named Red (R), Green (G) and Blue (B), 
if the sample values are stored in a single bank in a
+     * R₀,G₀,B₀, R₁,G₁,B₁, R₂,G₂,B₂, R₃,G₃,B₃, <i>etc.</i> fashion, then this 
method should be invoked as below:
      *
-     * @see java.awt.image.ComponentSampleModel#pixelStride
-     * @see java.awt.image.ComponentSampleModel#scanlineStride
+     * {@preformat java
+     *     setInterleavedPixelOffsets(3, new int[] {0, 1, 2});
+     * }
+     * </div>
+     *
+     * @param  pixelStride  the number of data elements between each pixel in 
the data vector or buffer.
+     * @param  bandOffsets  offsets to add to sample index in each band. This 
is typically {0, 1, 2, …}.
      */
-    public void setSampleStride(final int stride) {
-        if (stride != sampleStride) {
-            ArgumentChecks.ensureStrictlyPositive("stride", stride);
-            // Division by 'dataStride' is for cancelling effect of previous 
calls.
-            scanlineStride = Math.multiplyExact(scanlineStride / sampleStride, 
stride);
-            // If above operation did not fail, then following operation can 
not fail.
-            pixelStride = (pixelStride / sampleStride) * stride;
-            sampleStride = stride;
-        }
+    public void setInterleavedPixelOffsets(final int pixelStride, final int[] 
bandOffsets) {
+        ArgumentChecks.ensureStrictlyPositive("pixelStride", pixelStride);
+        ArgumentChecks.ensureNonNull("bandOffsets", bandOffsets);
+        ensureExpectedBandCount(bandOffsets.length, false);
+        this.sampleStride = pixelStride;
+        this.bandOffsets = bandOffsets.clone();
     }
 
     /**
@@ -390,14 +419,35 @@ public class ImageRenderer {
         if (buffer == null) {
             throw new 
IllegalStateException(Resources.format(Resources.Keys.UnspecifiedRasterData));
         }
-        // Number of data elements from the first element of the bank to the 
first sample of the band.
-        final int[] bandOffsets = new int[getNumBands()];
-        Arrays.fill(bandOffsets, Math.toIntExact(Math.addExact(
-                Math.multiplyExact(offsetX, pixelStride),
-                Math.multiplyExact(offsetY, scanlineStride))));
-
+        int ps = pixelStride;
+        int ls = scanlineStride;
+        if (bandOffsets != null) {
+            ls = Math.multiplyExact(ls, sampleStride);
+            ps *= sampleStride;                         // Can not fail if 
above operation did not fail.
+        }
+        /*
+         * Number of data elements from the first element of the bank to the 
first sample of the band.
+         * This is usually 0 for all bands, unless the upper-left corner 
(minX, minY) is not (0,0).
+         */
+        final int[] offsets = new int[getNumBands()];
+        Arrays.fill(offsets, Math.toIntExact(Math.addExact(
+                Math.multiplyExact(offsetX, ps),
+                Math.multiplyExact(offsetY, ls))));
+        /*
+         * Add the offset specified by the user (if any), or the default 
offset. The default is 0, 1, 2…
+         * for interleaved sample model (all bands in one bank) and 0, 0, 0… 
for banded sample model.
+         */
+        if (bandOffsets != null) {
+            for (int i=0; i<offsets.length; i++) {
+                offsets[i] = Math.addExact(offsets[i], bandOffsets[i]);
+            }
+        } else if (buffer.getNumBanks() == 1) {
+            for (int i=1; i<offsets.length; i++) {
+                offsets[i] = Math.addExact(offsets[i], i);
+            }
+        }
         final Point location = new Point(imageX, imageY);
-        return RasterFactory.createRaster(buffer, width, height, pixelStride, 
scanlineStride, bankIndices, bandOffsets, location);
+        return RasterFactory.createRaster(buffer, width, height, ps, ls, 
bankIndices, offsets, location);
     }
 
     /**
diff --git 
a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/ColorModelFactory.java
 
b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/ColorModelFactory.java
index e6ce4b8..cfe9d19 100644
--- 
a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/ColorModelFactory.java
+++ 
b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/ColorModelFactory.java
@@ -231,16 +231,21 @@ public final class ColorModelFactory {
          * Interpolates the colors in the color palette. Colors that do not 
fall
          * in the range of a category will be set to a transparent color.
          */
-        final int[] colorMap = new int[pieceStarts[categoryCount]];
+        final int[] colorMap;
         int transparent = -1;
-        for (int i=0; i<categoryCount; i++) {
-            final int[] colors = ARGB[i];
-            final int   lower  = pieceStarts[i  ];
-            final int   upper  = pieceStarts[i+1];
-            if (transparent < 0 && colors.length == 0) {
-                transparent = lower;
+        if (categoryCount <= 0) {
+            colorMap = ArraysExt.range(0, 256);
+        } else {
+            colorMap = new int[pieceStarts[categoryCount]];
+            for (int i=0; i<categoryCount; i++) {
+                final int[] colors = ARGB[i];
+                final int   lower  = pieceStarts[i  ];
+                final int   upper  = pieceStarts[i+1];
+                if (transparent < 0 && colors.length == 0) {
+                    transparent = lower;
+                }
+                expand(colors, colorMap, lower, upper);
             }
-            expand(colors, colorMap, lower, upper);
         }
         return createIndexColorModel(colorMap, numBands, visibleBand, 
transparent);
     }
diff --git 
a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/RasterFactory.java
 
b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/RasterFactory.java
index a16822b..3d877d2 100644
--- 
a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/RasterFactory.java
+++ 
b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/RasterFactory.java
@@ -57,6 +57,13 @@ public final class RasterFactory extends Static {
     /**
      * Wraps the given data buffer in a raster.
      * The sample model type is selected according the number of bands and the 
pixel stride.
+     * The number of bands is determined by {@code bandOffsets.length}, which 
should be one of followings:
+     *
+     * <ul>
+     *   <li>For banded sample model, all {@code bandOffsets} can be zero.</li>
+     *   <li>For interleaved sample model ({@code buffer.getNumBanks()} = 1), 
each band needs a different offset.
+     *       They may be 0, 1, 2, 3….</li>
+     * </ul>
      *
      * @param  buffer          buffer that contains the sample values.
      * @param  width           raster width in pixels.
@@ -64,7 +71,7 @@ public final class RasterFactory extends Static {
      * @param  pixelStride     number of data elements between two samples for 
the same band on the same line.
      * @param  scanlineStride  number of data elements between a given sample 
and the corresponding sample in the same column of the next line.
      * @param  bankIndices     bank indices for each band, or {@code null} for 
0, 1, 2, 3….
-     * @param  bandOffsets     number of data elements from the first element 
of the bank to the first sample of the band, or {@code null} for all 0.
+     * @param  bandOffsets     number of data elements from the first element 
of the bank to the first sample of the band.
      * @param  location        the upper-left corner of the raster, or {@code 
null} for (0,0).
      * @return a raster built from given properties.
      * @throws NullPointerException if {@code buffer} is {@code null}.
@@ -76,15 +83,12 @@ public final class RasterFactory extends Static {
     @SuppressWarnings("fallthrough")
     public static WritableRaster createRaster(final DataBuffer buffer,
             final int width, final int height, final int pixelStride, final 
int scanlineStride,
-            int[] bankIndices, int[] bandOffsets, final Point location)
+            int[] bankIndices, final int[] bandOffsets, final Point location)
     {
         /*
          * We do not verify the argument validity. Since this class is 
internal, caller should have done verification
          * itself. Furthermore those arguments are verified by WritableRaster 
constructors anyway.
          */
-        if (bandOffsets == null) {
-            bandOffsets = new int[buffer.getNumBanks()];
-        }
         final int dataType = buffer.getDataType();
         /*
          * This SampleModel variable is a workaround for WritableRaster static 
methods not supporting all data types.
@@ -96,9 +100,9 @@ public final class RasterFactory extends Static {
         final SampleModel model;
         if (buffer.getNumBanks() == 1 && (bankIndices == null || 
bankIndices[0] == 0)) {
             /*
-             * Sample data are stored for all bands in a single bank of the 
DataBuffer.
-             * Each sample of a pixel occupies one data element of the 
DataBuffer.
-             * The number of bands is inferred from bandOffsets.length.
+             * Sample data are stored for all bands in a single bank of the 
DataBuffer, in an interleaved fashion.
+             * Each sample of a pixel occupies one data element of the 
DataBuffer, with a different offset since
+             * the buffer beginning. The number of bands is inferred from 
bandOffsets.length.
              */
             switch (dataType) {
                 case DataBuffer.TYPE_BYTE:
@@ -119,13 +123,14 @@ public final class RasterFactory extends Static {
                 }
             }
         } else {
+            /*
+             * Sample data are stored in different banks (arrays) for each 
band. If all pixels are consecutive (pixelStride = 1),
+             * we have the classical banded sample model. Otherwise the type 
is not well identified; neither interleaved or banded.
+             */
             if (bankIndices == null) {
                 bankIndices = ArraysExt.range(0, bandOffsets.length);
             }
             if (pixelStride == 1) {
-                /*
-                 * All pixels are consecutive (pixelStride = 1) but may be on 
many bands.
-                 */
                 switch (dataType) {
                     case DataBuffer.TYPE_BYTE:
                     case DataBuffer.TYPE_USHORT:
diff --git 
a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java 
b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
index 5cc8347..d702247 100644
--- 
a/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
+++ 
b/core/sis-raster/src/test/java/org/apache/sis/coverage/CategoryListTest.java
@@ -82,7 +82,7 @@ public final strictfp class CategoryListTest extends TestCase 
{
             new Category("Again",   NumberRange.create(10, true, 10, true), 
null, null, toNaN)       // Range overlaps.
         };
         try {
-            assertNotConverted(new CategoryList(categories.clone(), null));
+            assertNotConverted(CategoryList.create(categories.clone()));
             fail("Should not have accepted range overlap.");
         } catch (IllegalArgumentException exception) {
             // This is the expected exception.
@@ -92,7 +92,7 @@ public final strictfp class CategoryListTest extends TestCase 
{
         }
         // Removes the wrong category. Now, construction should succeed.
         categories = Arrays.copyOf(categories, categories.length - 1);
-        assertNotConverted(new CategoryList(categories, null));
+        assertNotConverted(CategoryList.create(categories));
         assertSorted(Arrays.asList(categories));
     }
 
@@ -182,7 +182,7 @@ public final strictfp class CategoryListTest extends 
TestCase {
      */
     @Test
     public void testRanges() {
-        final CategoryList list = new CategoryList(categories(), null);
+        final CategoryList list = CategoryList.create(categories());
         assertSorted(list);
         assertTrue  ("isMinIncluded",           list.range.isMinIncluded());
         assertFalse ("isMaxIncluded",           list.range.isMaxIncluded());
@@ -205,7 +205,7 @@ public final strictfp class CategoryListTest extends 
TestCase {
     @DependsOnMethod("testBinarySearch")
     public void testSearch() {
         final Category[] categories = categories();
-        final CategoryList list = new CategoryList(categories.clone(), null);
+        final CategoryList list = CategoryList.create(categories.clone());
         assertTrue("containsAll", list.containsAll(Arrays.asList(categories)));
         /*
          * Checks category searches for values that are insides the range of a 
category.
@@ -253,7 +253,7 @@ public final strictfp class CategoryListTest extends 
TestCase {
     @DependsOnMethod("testSearch")
     public void testTransform() throws TransformException {
         final Random random = TestUtilities.createRandomNumberGenerator();
-        final CategoryList list = new CategoryList(categories(), null);
+        final CategoryList list = CategoryList.create(categories());
         /*
          * Checks conversions. We verified in 'testSearch()' that correct 
categories are found for those values.
          */
@@ -340,7 +340,7 @@ public final strictfp class CategoryListTest extends 
TestCase {
         for (int i=0; i<categories.length; i++) {
             categories[i] = categories[i].converse;
         }
-        final CategoryList list = new CategoryList(categories, null);
+        final CategoryList list = CategoryList.create(categories);
         assertSorted(list);
         for (int i=list.size(); --i >= 0;) {
             final Category category = list.get(i);
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
index 7f5b979..54fc45d 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Raster.java
@@ -55,6 +55,12 @@ final class Raster extends GridCoverage {
     private final int pixelStride;
 
     /**
+     * Offsets to add to sample index in each band, or {@code null} if none.
+     * This is non-null only if a variable dimension is used for the bands.
+     */
+    private final int[] bandOffsets;
+
+    /**
      * Name to display in error messages. Not to be used for processing.
      */
     private final String label;
@@ -62,13 +68,14 @@ final class Raster extends GridCoverage {
     /**
      * Creates a new raster from the given resource.
      */
-    Raster(final GridGeometry domain, final List<SampleDimension> range, final 
DataBuffer data, final int pixelStride,
-            final String label)
+    Raster(final GridGeometry domain, final List<SampleDimension> range, final 
DataBuffer data,
+            final int pixelStride, final int[] bandOffsets, final String label)
     {
         super(domain, range);
         this.data        = data;
         this.label       = label;
         this.pixelStride = pixelStride;
+        this.bandOffsets = bandOffsets;
     }
 
     /**
@@ -80,7 +87,9 @@ final class Raster extends GridCoverage {
         try {
             final ImageRenderer renderer = new ImageRenderer(this, target);
             renderer.setData(data);
-            renderer.setSampleStride(pixelStride);
+            if (bandOffsets != null) {
+                renderer.setInterleavedPixelOffsets(pixelStride, bandOffsets);
+            }
             return renderer.image();
         } catch (IllegalArgumentException | ArithmeticException | 
RasterFormatException e) {
             throw new 
CannotEvaluateException(Resources.format(Resources.Keys.CanNotRender_2, label, 
e), e);
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
index bd8135d..96259f8 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
@@ -18,7 +18,6 @@ package org.apache.sis.internal.netcdf;
 
 import java.util.Map;
 import java.util.List;
-import java.util.Arrays;
 import java.util.ArrayList;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -30,6 +29,7 @@ import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.internal.storage.AbstractGridResource;
 import org.apache.sis.internal.storage.ResourceOnFileSystem;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
+import org.apache.sis.internal.util.Strings;
 import org.apache.sis.internal.raster.RasterFactory;
 import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridExtent;
@@ -111,16 +111,20 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
     private final SampleDimension[] ranges;
 
     /**
-     * The netCDF variable wrapped by this resource. The length of this array 
shall be equal to {@code ranges.length}.
-     * The same variable may be repeated if it contains many bands, in which 
case the bands are in dimension at index
-     * {@link #bandDimension}.
+     * The netCDF variables for each sample dimensions. The length of this 
array shall be equal to {@code ranges.length},
+     * except if bands are stored as one variable dimension ({@link 
#bandDimension} ≧ 0) in which case the length shall
+     * be exactly 1. Accesses to this array need to take in account that the 
length may be only 1. Example:
+     *
+     * {@preformat java
+     *     Variable v = data[bandDimension >= 0 ? 0 : index];
+     * }
      */
     private final Variable[] data;
 
     /**
      * If one of {@link #data} dimension provides values for different bands, 
that dimension index. Otherwise -1.
      * This is an index in a list of dimensions in "natural" order (reverse of 
netCDF order).
-     * There is three ways to read the data, determined by the {@code 
bandDimension} value:
+     * There is three ways to read the data, determined by this {@code 
bandDimension} value:
      *
      * <ul>
      *   <li>{@code (bandDimension < 0)}: one variable per band (usual 
case).</li>
@@ -152,27 +156,22 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
      * @param  name      the name for the resource.
      * @param  grid      the grid geometry (size, CRS…) of the {@linkplain 
#data} cube.
      * @param  bands     the variables providing actual data. Shall contain at 
least one variable.
-     * @param  numBands  the number of bands, or -1 for using {@code 
bands.length}.
-     * @param  inner     if one of {@link #data} dimension provides values for 
different bands, that dimension index. Otherwise -1.
+     * @param  numBands  the number of bands. Shall be {@code bands.size()} 
except if {@code bandsDimension} ≧ 0.
+     * @param  bandDim   if one of {@link #data} dimension provides values for 
different bands, that dimension index. Otherwise -1.
      * @param  lock      the lock to use in {@code synchronized(lock)} 
statements.
      */
     private RasterResource(final Decoder decoder, final String name, final 
GridGeometry grid, final List<Variable> bands,
-            final int numBands, final int inner, final Object lock) throws 
IOException, DataStoreException
+            final int numBands, final int bandDim, final Object lock)
     {
         super(decoder.listeners);
-        data = bands.toArray(new Variable[numBands >= 0 ? numBands : 
bands.size()]);
-        for (int i=data.length; --i >= 0;) {
-            if (data[i] != null) {
-                Arrays.fill(data, i+1, data.length, data[i]);                  
 // Repeat the last variable for all bands.
-                break;
-            }
-        }
-        ranges        = new SampleDimension[data.length];
+        data          = bands.toArray(new Variable[bands.size()]);
+        ranges        = new SampleDimension[numBands];
         identifier    = decoder.nameFactory.createLocalName(decoder.namespace, 
name);
         location      = decoder.location;
         gridGeometry  = grid;
-        bandDimension = inner;
+        bandDimension = bandDim;
         this.lock     = lock;
+        assert data.length == (bandDimension >= 0 ? 1 : ranges.length);
     }
 
     /**
@@ -216,13 +215,19 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
             final int gridDimension = grid.getDimension();
             final int bandDimension, numBands;
             if (dataDimension != gridDimension) {
-                if (dataDimension != gridDimension + 1) {
-                    throw new 
DataStoreContentException(Resources.forLocale(decoder.listeners.getLocale())
-                            .getString(Resources.Keys.UnmappedDimensions_4, 
name, decoder.getFilename(), dataDimension, gridDimension));
-                }
                 bandDimension = variable.bandDimension;                        
    // One variable dimension is interpreted as bands.
                 Dimension dim = gridDimensions.get(dataDimension - 1 - 
bandDimension);  // Note: "natural" → netCDF index conversion.
                 numBands = Math.toIntExact(dim.length());
+                if (dataDimension != gridDimension + 1 || (bandDimension > 0 
&& bandDimension != gridDimension)) {
+                    /*
+                     * One of the following restrictions it not met for the 
requested data:
+                     *
+                     *   - Only 1 dimension can be used for bands. Variables 
with 2 or more band dimensions are not supported.
+                     *   - The dimension for bands shall be either the first 
or the last dimension; it can not be in the middle.
+                     */
+                    throw new 
DataStoreContentException(Resources.forLocale(decoder.listeners.getLocale())
+                            .getString(Resources.Keys.UnmappedDimensions_4, 
name, decoder.getFilename(), dataDimension, gridDimension));
+                }
             } else {
                 /*
                  * At this point we found a variable where all dimensions are 
in the CRS. This is the usual case;
@@ -236,7 +241,6 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
                  * name is the same and the grid geometries are the same.
                  */
                 bandDimension = -1;                                            
     // No dimension to be interpreted as bands.
-                numBands = -1;                                                 
     // To be determined by siblings.size().
                 final DataType type = variable.getDataType();
                 for (final String keyword : VECTOR_COMPONENT_NAMES) {
                     final int prefixLength = name.indexOf(keyword);
@@ -284,6 +288,7 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
                         }
                     }
                 }
+                numBands = siblings.size();
             }
             resources.add(new RasterResource(decoder, name.trim(), grid, 
siblings, numBands, bandDimension, lock));
             siblings.clear();
@@ -310,6 +315,14 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
     }
 
     /**
+     * Returns the variable at the given index. This method can be invoked 
when the caller has not verified
+     * if we are in the special case where all bands are in the same variable 
({@link #bandDimension} ≧ 0).
+     */
+    private Variable getVariable(final int i) {
+        return data[bandDimension >= 0 ? 0 : i];
+    }
+
+    /**
      * Returns the ranges of sample values together with the conversion from 
samples to real values.
      *
      * @return ranges of sample values together with their mapping to "real 
values".
@@ -324,7 +337,7 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
                 for (int i=0; i<ranges.length; i++) {
                     if (ranges[i] == null) {
                         if (builder == null) builder = new 
SampleDimension.Builder();
-                        ranges[i] = createSampleDimension(builder, data[i]);
+                        ranges[i] = createSampleDimension(builder, 
getVariable(i), i);
                         builder.clear();
                     }
                 }
@@ -340,9 +353,12 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
      *
      * @param  builder  the builder to use for creating the sample dimension.
      * @param  band     the data for which to create a sample dimension.
+     * @param  index    index in the variable dimension identified by {@link 
#bandDimension}.
      * @throws TransformException if an error occurred while using the 
transfer function.
      */
-    private SampleDimension createSampleDimension(final 
SampleDimension.Builder builder, final Variable band) throws TransformException 
{
+    private SampleDimension createSampleDimension(final 
SampleDimension.Builder builder, final Variable band, final int index)
+            throws TransformException
+    {
         /*
          * Take the minimum and maximum values as determined by Apache SIS 
through the Convention class.  The UCAR library
          * is used only as a fallback. We give precedence to the range 
computed by Apache SIS instead than the range given
@@ -427,7 +443,16 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
             }
             builder.addQualitative(name, n, n);
         }
-        return builder.setName(band.getName()).build();
+        /*
+         * At this point we have the list of all categories to put in the 
sample dimension.
+         * Now create the sample dimension using the variable short name as 
dimension name.
+         * The index is appended to the name only if bands are all in the same 
variable.
+         */
+        String name = band.getName();
+        if (bandDimension >= 0) {
+            name = Strings.toIndexed(name, index);
+        }
+        return builder.setName(name).build();
     }
 
     /**
@@ -444,7 +469,7 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
         if (domain == null) {
             domain = gridGeometry;
         }
-        final Variable first = data[rangeIndices.getFirstSpecified()];
+        final Variable first = data[bandDimension >= 0 ? 0 : 
rangeIndices.getFirstSpecified()];
         final DataType dataType = first.getDataType();
         if (bandDimension < 0) {
             for (int i=0; i<rangeIndices.getNumBands(); i++) {
@@ -465,56 +490,70 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
          */
         final DataBuffer imageBuffer;
         final SampleDimension[] bands = new 
SampleDimension[rangeIndices.getNumBands()];
+        int[] bandOffsets = null;                                              
     // By default, all bands start at index 0.
         try {
-            final Buffer[] sampleValues = new Buffer[bands.length];
-            final GridDerivation targetGeometry = 
gridGeometry.derive().subgrid(domain);
-            GridExtent areaOfInterest = targetGeometry.getIntersection();
-            final int[] scales = targetGeometry.getSubsamplings();
-            int[] subsamplings = scales;
+            GridDerivation targetGeometry = 
gridGeometry.derive().subgrid(domain);
+            GridExtent     areaOfInterest = targetGeometry.getIntersection();  
     // Pixel indices of data to read.
+            int[]          subsamplings   = targetGeometry.getSubsamplings();  
     // Slice to read or subsampling to apply.
+            int            numBuffers     = bands.length;                      
     // By default, one variable per band.
+            domain = targetGeometry.subsample(subsamplings).build();           
     // Adjust user-specified domain to data geometry.
             if (bandDimension >= 0) {
                 areaOfInterest = 
rangeIndices.insertBandDimension(areaOfInterest, bandDimension);
                 subsamplings   = rangeIndices.insertSubsampling  
(subsamplings,   bandDimension);
+                if (bandDimension == 0) {
+                    bandOffsets = new int[numBuffers];          // Will be set 
to non-zero values later.
+                }
+                numBuffers = 1;                                 // One 
variable for all bands.
             }
             /*
              * Iterate over netCDF variables in the order they appear in the 
file, not in the order requested
              * by the 'range' argument.  The intent is to perform sequential 
I/O as much as possible, without
-             * seeking backward.
+             * seeking backward. In the (uncommon) case where bands are one of 
the variable dimension instead
+             * than different variables, the reading of the whole variable 
occurs during the first iteration.
              */
-            Buffer values = null;
+            Buffer[] sampleValues = new Buffer[numBuffers];
             synchronized (lock) {
                 for (int i=0; i<bands.length; i++) {
-                    final int r = rangeIndices.getSourceIndex(i);              
     // In strictly increasing order.
-                    final Variable variable = data[r];
-                    SampleDimension sd = ranges[r];
-                    if (sd == null) {
-                        ranges[r] = sd = 
createSampleDimension(rangeIndices.builder(), variable);
+                    int indexInResource = rangeIndices.getSourceIndex(i);     
// In strictly increasing order.
+                    int indexInRaster   = rangeIndices.getTargetIndex(i);
+                    Variable variable   = getVariable(indexInResource);
+                    SampleDimension b   = ranges[indexInResource];
+                    if (b == null) {
+                        ranges[indexInResource] = b = 
createSampleDimension(rangeIndices.builder(), variable, i);
                     }
-                    if (bandDimension > 0) {
-                        // TODO: adjust 'areaOfInterest'.
-                        throw new UnsupportedOperationException();
+                    bands[indexInRaster] = b;
+                    if (bandOffsets != null) {
+                        bandOffsets[indexInRaster] = i;
+                        indexInRaster = 0;                  // Pixels 
interleaved in one bank: sampleValues.length = 1.
                     }
-                    if (bandDimension != 0 || values == null) {
+                    if (i < numBuffers) {
                         // Optional.orElseThrow() below should never fail 
since Variable.read(…) wraps primitive array.
-                        values = variable.read(areaOfInterest, 
subsamplings).buffer().get();
+                        sampleValues[indexInRaster] = 
variable.read(areaOfInterest, subsamplings).buffer().get();
                     }
-                    if (bandDimension == 0) {
-                        /*
-                         * This block is executed only if the band dimension 
is first, in which case we have interleaved
-                         * values like (band0, band1) for each pixel. Those 
values were read once for all in above block.
-                         * This block sets the offset of the first value to 
read, together with the buffer limit in order
-                         * to ensure that all buffers have the same number of 
remaining elements. The pixel stride is not
-                         * specified here; it will be specified later, at 
java.awt.image.SampleModel construction time.
-                         */
-                        if (i != 0) values = JDK9.duplicate(values);
-                        final int p = rangeIndices.getSubsampledIndex(i);
-                        values.position(p).limit(values.capacity() - 
bands.length + p + 1);
+                }
+            }
+            /*
+             * The following block is executed only if all bands are in a 
single variable, and the bands dimension is
+             * the last one (in "natural" order). In such case, the sample 
model to construct is a BandedSampleModel.
+             * Contrarily to PixelInterleavedSampleModel (the case when the 
band dimension is first), banded sample
+             * model force us to split the buffer in a buffer for each band.
+             */
+            if (bandDimension > 0) {                // Really > 0, not >= 0.
+                final int stride = Math.toIntExact(data[0].getBandStride());
+                Buffer values = sampleValues[0].limit(stride);
+                sampleValues = new Buffer[bands.length];
+                for (int i=0; i<sampleValues.length; i++) {
+                    if (i != 0) {
+                        values = JDK9.duplicate(values);
+                        final int p = values.limit();
+                        values.position(p).limit(Math.addExact(p, stride));
                     }
-                    final int p = rangeIndices.getTargetIndex(i);
-                    sampleValues[p] = values;
-                    bands[p] = sd;
+                    sampleValues[i] = values;
                 }
             }
-            domain = targetGeometry.subsample(scales).build();
+            /*
+             * Convert NIO Buffer into Java2D DataBuffer. May throw various 
RuntimeException.
+             */
             imageBuffer = RasterFactory.wrap(dataType.rasterDataType, 
sampleValues);
         } catch (IOException e) {
             throw new DataStoreException(e);
@@ -524,13 +563,15 @@ public final class RasterResource extends 
AbstractGridResource implements Resour
             final Throwable cause = e.getCause();
             if (cause instanceof TransformException) {
                 throw new DataStoreReferencingException(cause);
+            } else {
+                throw new DataStoreContentException(e);
             }
-            throw new DataStoreContentException(e);
         }
         if (imageBuffer == null) {
             throw new 
DataStoreContentException(Errors.getResources(getLocale()).getString(Errors.Keys.UnsupportedType_1,
 dataType.name()));
         }
-        return new Raster(domain, UnmodifiableArrayList.wrap(bands), 
imageBuffer, rangeIndices.getPixelStride(), first.getStandardName());
+        return new Raster(domain, UnmodifiableArrayList.wrap(bands), 
imageBuffer,
+                rangeIndices.getPixelStride(), bandOffsets, 
first.getStandardName());
     }
 
     /**
diff --git 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
index fb3e8ec..89eaca0 100644
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java
@@ -134,6 +134,7 @@ public abstract class Variable extends NamedElement {
      * If {@link #gridGeometry} has less dimensions than this variable, index 
of a grid dimension to take as raster bands.
      * Otherwise this field is left uninitialized. If set, the index is 
relative to "natural" order (reverse of netCDF order).
      *
+     * @see #getBandStride()
      * @see RasterResource#bandDimension
      */
     int bandDimension;
@@ -736,6 +737,19 @@ public abstract class Variable extends NamedElement {
     }
 
     /**
+     * Returns the number of sample values between two bands.
+     * This method is meaningful only if {@link #bandDimension} ≧ 0.
+     */
+    final long getBandStride() throws IOException, DataStoreException {
+        long length = 1;
+        final GridExtent extent = getGridGeometry().getExtent();
+        for (int i=bandDimension; --i >= 0;) {
+            length = Math.multiplyExact(length, extent.getSize(i));
+        }
+        return length;
+    }
+
+    /**
      * Returns the dimensions of this variable in the order they are declared 
in the netCDF file.
      * The dimensions are those of the grid, not the dimensions of the 
coordinate system.
      * In ISO 19123 terminology, {@link Dimension#length()} on each dimension 
give the upper corner
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
index 39817aa..66f9ec9 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractGridResource.java
@@ -227,7 +227,7 @@ public abstract class AbstractGridResource extends 
AbstractResource implements G
          *     subsamplings   = rangeIndices.insertSubsampling  (subsamplings, 
  bandDimension);
          *     data = myReadMethod(areaOfInterest, subsamplings);
          *     for (int i=0; i<numBands; i++) {
-         *         int bandIndexInTheDataWeJustRead = getSubsampledIndex(i);
+         *         int bandIndexInTheDataWeJustRead = 
rangeIndices.getSubsampledIndex(i);
          *     }
          * }
          *
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreReferencingException.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreReferencingException.java
index 7d7965a..bb23d6c 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreReferencingException.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreReferencingException.java
@@ -17,13 +17,14 @@
 package org.apache.sis.storage;
 
 import java.util.Locale;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.TransformException;
 
 
 /**
  * Thrown when a data store failed to construct the coordinate reference 
system (CRS)
  * or other positioning information. This exception is typically (but not 
necessarily)
- * caused by {@link org.opengis.util.FactoryException} or
- * {@link org.opengis.referencing.operation.TransformException}.
+ * caused by {@link FactoryException} or {@link TransformException}.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 0.8
@@ -47,6 +48,7 @@ public class DataStoreReferencingException extends 
DataStoreException {
 
     /**
      * Creates an exception with the specified cause and no details message.
+     * The given cause should (but is not required to) be a {@link 
FactoryException} or {@link TransformException}.
      *
      * @param cause  the cause for this exception.
      */
@@ -56,6 +58,7 @@ public class DataStoreReferencingException extends 
DataStoreException {
 
     /**
      * Creates an exception with the specified details message and cause.
+     * The given cause should (but is not required to) be a {@link 
FactoryException} or {@link TransformException}.
      *
      * @param message  the detail message.
      * @param cause    the cause for this exception.
@@ -69,6 +72,9 @@ public class DataStoreReferencingException extends 
DataStoreException {
      * Location in the file where the error occurred while be fetched from the 
given {@code store}
      * argument if possible. If the given store is not recognized, then it 
will be ignored.
      *
+     * <p>This constructor should be followed by a call to {@link 
#initCause(Throwable)}
+     * with a {@link FactoryException} or {@link TransformException} cause.</p>
+     *
      * @param locale    the locale of the message to be returned by {@link 
#getLocalizedMessage()}, or {@code null}.
      * @param format    short name or abbreviation of the data format (e.g. 
"CSV", "GML", "WKT", <i>etc</i>).
      * @param filename  name of the file or data store where the error 
occurred.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/WritableAggregate.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/WritableAggregate.java
index 45b9d3a..e8a4caf 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/WritableAggregate.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/WritableAggregate.java
@@ -54,9 +54,15 @@ public interface WritableAggregate extends Aggregate {
 
     /**
      * Removes a {@code Resource} from this {@code Aggregate}.
-     * This operation is destructive: the {@link Resource} and it's related 
data will be removed.
+     * The given resource should be one of the instances returned by {@link 
#components()}.
+     * This operation is destructive in two aspects:
      *
-     * @param  resource  child resource to remove, should not be null.
+     * <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.
      */
     void remove(Resource resource) throws DataStoreException;

Reply via email to