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

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

commit 0665f030cdcc6f5b2bb0625ea9fc922b27ddbf02
Merge: 54a8cfd70f e55d695764
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Mon Nov 27 19:17:10 2023 +0100

    Merge branch 'geoapi-3.1'

 buildSrc/build.gradle.kts                          |  24 +-
 .../org/apache/sis/buildtools/book/Assembler.java  |   0
 .../apache/sis/buildtools/book/BookException.java  |   0
 .../org/apache/sis/buildtools/book/Characters.java |   0
 .../apache/sis/buildtools/book/CodeColorizer.java  |   0
 .../apache/sis/buildtools/book/package-info.java   |   0
 .../sis/buildtools/coding/ReorganizeImports.java   |  78 +++----
 .../buildtools/coding/VerifyVersionInJavadoc.java  |   0
 .../apache/sis/buildtools/coding/package-info.java |   0
 .../org/apache/sis/buildtools/doclet/Doclet.java   |   0
 .../org/apache/sis/buildtools/doclet/Include.java  |   0
 .../org/apache/sis/buildtools/doclet/Rewriter.java |   0
 .../org/apache/sis/buildtools/doclet/Taglet.java   |   0
 .../apache/sis/buildtools/doclet/package-info.java |   0
 .../apache/sis/buildtools/gradle/Assembler.java    |   0
 .../apache/sis/buildtools/gradle/BuildHelper.java  |   0
 .../apache/sis/buildtools/gradle/Conventions.java  |   0
 .../apache/sis/buildtools/gradle/Dependency.java   |   0
 .../apache/sis/buildtools/gradle/JavaMaker.java    |   0
 .../sis/buildtools/gradle/ModularCompilation.java  |   0
 .../apache/sis/buildtools/gradle/ModularJAR.java   |   0
 .../sis/buildtools/gradle/ModularJavadoc.java      |   0
 .../sis/buildtools/gradle/ModularPublishing.java   |   0
 .../sis/buildtools/gradle/ModularSources.java      |   0
 .../apache/sis/buildtools/gradle/ModularTest.java  |   0
 .../sis/buildtools/gradle/SISBuildException.java   |   0
 .../org/apache/sis/buildtools/gradle/UnoPkg.java   |   0
 .../apache/sis/buildtools/gradle/ZipWriter.java    |   0
 .../apache/sis/buildtools/gradle/package-info.java |   0
 .../org/apache/sis/buildtools/maven/Element.java   |   0
 .../org/apache/sis/buildtools/maven/Generator.java |   0
 .../apache/sis/buildtools/maven/package-info.java  |   0
 .../sis/buildtools/maven/workaround/Argument.java  |   0
 .../maven/workaround/ProxyGenerator.java           |   0
 .../buildtools/maven/workaround/package-info.java  |   0
 .../resources/IndexedResourceCompiler.java         |   0
 .../sis/buildtools/resources/package-info.java     |   0
 .../main/kotlin/sis.library-conventions.gradle.kts |  68 ++++++
 .../org/apache/sis/buildtools/book/GEOAPI.lst      |   0
 .../org/apache/sis/buildtools/book/OGC.lst         |   0
 .../org/apache/sis/buildtools/book/SIS.lst         |   0
 .../org/apache/sis/buildtools/book/XML_PREFIX.lst  |   0
 .../resources/IndexedResourceCompilerTest.java     |   0
 endorsed/build.gradle.kts                          |  27 +--
 .../main/org/apache/sis/console/package-info.java  |   2 +-
 .../sis/coverage/grid/DimensionAppender.java       | 186 ++++++++++++++++
 .../sis/coverage/grid/DimensionalityReduction.java |  15 +-
 .../sis/coverage/grid/GridCoverageProcessor.java   |  85 +++++++
 .../org/apache/sis/coverage/grid/GridExtent.java   |  69 +++++-
 .../org/apache/sis/coverage/grid/GridGeometry.java |  63 +++++-
 .../sis/coverage/grid/j2d/SampleModelFactory.java  |   4 +-
 .../org/apache/sis/feature/internal/Resources.java |  10 +
 .../sis/feature/internal/Resources.properties      |   2 +
 .../sis/feature/internal/Resources_fr.properties   |   2 +
 .../org/apache/sis/filter/AssociationValue.java    |   7 +-
 .../org/apache/sis/filter/ComparisonFilter.java    |   2 +-
 .../main/org/apache/sis/filter/PropertyValue.java  |  18 +-
 .../apache/sis/filter/internal/FunctionNames.java  |   2 +-
 .../main/org/apache/sis/filter/internal/XPath.java |  74 +++++--
 .../main/org/apache/sis/filter/sqlmm/Registry.java |   4 +-
 .../apache/sis/geometry/wrapper/j2d/Polyline.java  |   4 +-
 .../main/org/apache/sis/image/MaskedImage.java     |   2 +-
 .../main/org/apache/sis/image/PixelIterator.java   |   2 +-
 .../main/org/apache/sis/image/PlanarImage.java     |   6 +-
 .../apache/sis/image/processing/TiledProcess.java  |   6 +-
 .../sis/coverage/grid/DimensionAppenderTest.java   | 131 +++++++++++
 .../coverage/grid/DimensionalityReductionTest.java |   4 +-
 .../apache/sis/coverage/grid/GridExtentTest.java   | 115 +++++-----
 .../apache/sis/coverage/grid/GridGeometryTest.java | 140 +++++++-----
 .../test/org/apache/sis/filter/XPathTest.java      |  20 ++
 .../test/org/apache/sis/image/TiledImageMock.java  |   6 +-
 .../gazetteer/GeohashReferenceSystem.java          |   6 +-
 .../main/org/apache/sis/referencing/CommonCRS.java |  68 ++++--
 .../org/apache/sis/referencing/package-info.java   |   2 +-
 .../org/apache/sis/referencing/util/Formulas.java  |   2 +-
 .../org/apache/sis/referencing/CommonCRSTest.java  |   1 +
 .../apache/sis/storage/landsat/LandsatStore.java   |   2 +-
 .../apache/sis/storage/landsat/MetadataReader.java |   2 +-
 .../org/apache/sis/storage/geotiff/DataSubset.java |   2 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |   5 +-
 .../sis/storage/geotiff/MultiResolutionImage.java  |   4 +-
 .../org/apache/sis/storage/geotiff/Writer.java     |   2 +-
 .../geotiff/inflater/CompressionChannel.java       |   4 +-
 .../geotiff/inflater/HorizontalPredictor.java      |   4 +-
 .../sis/storage/geotiff/inflater/Inflater.java     |   5 +-
 .../storage/geotiff/inflater/PredictorChannel.java |   4 +-
 .../sis/storage/geotiff/writer/TileMatrix.java     |   4 +-
 .../org/apache/sis/storage/geotiff/WriterTest.java |   9 +-
 .../org/apache/sis/storage/netcdf/base/Axis.java   |   4 +-
 .../sis/storage/sql/postgis/RasterReader.java      |   4 +-
 .../org.apache.sis.storage/main/module-info.java   |   1 +
 .../main/org/apache/sis/io/stream/IOUtilities.java |  16 ++
 .../main/org/apache/sis/io/stream/Region.java      |   2 +-
 .../sis/storage/AbstractGridCoverageResource.java  |  21 +-
 .../main/org/apache/sis/storage/FeatureQuery.java  |  18 +-
 .../apache/sis/storage/GridCoverageResource.java   |  23 +-
 .../org/apache/sis/storage/StorageConnector.java   |  35 ++-
 .../aggregate/ConcatenatedGridCoverage.java        |  27 ++-
 .../aggregate/ConcatenatedGridResource.java        |   5 +-
 .../sis/storage/aggregate/CoverageAggregator.java  | 162 +++++++++++++-
 .../sis/storage/aggregate/DimensionAppender.java   | 246 +++++++++++++++++++++
 .../apache/sis/storage/aggregate/package-info.java |   2 +-
 .../apache/sis/storage/base/TiledGridCoverage.java |   2 +-
 .../org/apache/sis/storage/csv/StoreProvider.java  |   2 +-
 .../apache/sis/storage/esri/CharactersView.java    |   6 +-
 .../org/apache/sis/storage/esri/RasterStore.java   |  14 --
 .../apache/sis/storage/esri/RawRasterReader.java   |   2 +-
 .../apache/sis/storage/esri/RawRasterStore.java    |   2 +-
 .../main/org/apache/sis/storage/folder/Store.java  |  46 ++--
 .../apache/sis/storage/folder/WritableStore.java   |   5 +-
 .../src/org.apache.sis.util/main/module-info.java  |   2 +
 .../main/org/apache/sis/math/DecimalFunctions.java |  15 +-
 .../main/org/apache/sis/math/Fraction.java         |   9 +-
 .../main/org/apache/sis/math/MathFunctions.java    |   7 +-
 .../org/apache/sis/measure/QuantityFormat.java     |  20 +-
 .../apache/sis/measure/SexagesimalConverter.java   |   3 +-
 .../main/org/apache/sis/measure/package-info.java  |   2 +-
 .../sis/pending/jdk/{JDK17.java => JDK13.java}     |  42 +---
 .../main/org/apache/sis/pending/jdk/JDK16.java     |  28 ++-
 .../main/org/apache/sis/pending/jdk/JDK18.java     |  65 ++++++
 .../main/org/apache/sis/pending/jdk/JDK19.java     |  24 +-
 .../main/org/apache/sis/system/Modules.java        |   5 -
 .../main/org/apache/sis/util/ArraysExt.java        |  39 +++-
 .../main/org/apache/sis/util/Classes.java          |  19 +-
 .../org/apache/sis/util/collection/Containers.java |   4 +
 .../org/apache/sis/util/internal/DoubleDouble.java |   3 +-
 .../org/apache/sis/util/internal/Numerics.java     |  87 +++-----
 .../main/org/apache/sis/util/logging/Logging.java  |  17 +-
 .../org/apache/sis/util/resources/Vocabulary.java  |  10 +
 .../sis/util/resources/Vocabulary.properties       |   2 +
 .../sis/util/resources/Vocabulary_fr.properties    |   2 +
 .../org/apache/sis/math/MathFunctionsTest.java     |  12 +-
 .../org/apache/sis/util/internal/NumericsTest.java |  11 +-
 incubator/build.gradle.kts                         |  24 +-
 .../sis/storage/shapefile/ShapefileStore.java      |  66 +++++-
 .../apache/sis/storage/shapefile/dbf/DBFField.java |   2 +-
 .../sis/storage/shapefile/dbf/DBFHeader.java       |   2 +-
 .../sis/storage/shapefile/dbf/DBFReader.java       |   4 +-
 .../sis/storage/shapefile/shp/ShapeHeader.java     |   2 +-
 .../sis/storage/shapefile/ShapefileStoreTest.java  | 196 ++++++++--------
 optional/build.gradle.kts                          |  25 +--
 141 files changed, 1992 insertions(+), 706 deletions(-)

diff --cc buildSrc/src/main/kotlin/sis.library-conventions.gradle.kts
index 0000000000,75fca2a089..230a5d5810
mode 000000,100644..100644
--- a/buildSrc/src/main/kotlin/sis.library-conventions.gradle.kts
+++ b/buildSrc/src/main/kotlin/sis.library-conventions.gradle.kts
@@@ -1,0 -1,68 +1,68 @@@
+ /*
+  * 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.
+  */
+ group = "org.apache.sis"
 -version = "1.x-SNAPSHOT"
++version = "1.5-SNAPSHOT"
+ 
+ /*
+  * "org.apache.sis.buildtools" is a custom Gradle plugin for building a 
project with Module Source Hierarchy
+  * as specified in 
https://docs.oracle.com/en/java/javase/21/docs/specs/man/javac.html#directory-hierarchies
+  * The expected hierarchy is:
+  *
+  *   endorsed
+  *     ├─ build
+  *     └─ src
+  *         ├─ org.apache.sis.metadata
+  *         │    ├─ main
+  *         │    │   ├─ module-info.java
+  *         │    │   └─ org/apache/sis/metadata/…
+  *         │    └─ test
+  *         │        └─ org/apache/sis/metadata/…
+  *         ├─ org.apache.sis.referencing
+  *         │    ├─ main
+  *         │    │   ├─ module-info.java
+  *         │    │   └─ org/apache/sis/referencing/…
+  *         │    └─ test
+  *         │        └─ org/apache/sis/referencing/…
+  *         └─ etc.
+  */
+ plugins {
+     `java-library`
+     `maven-publish`
+     signing
+ }
+ 
+ /*
+  * Configuration of the repositories where to deploy artifacts.
+  */
+ publishing {
+     repositories {
+         maven {
+             name = "Apache"
+             url = uri(if (version.toString().endsWith("SNAPSHOT"))
+                       
"https://repository.apache.org/content/repositories/snapshots"; else
+                       
"https://repository.apache.org/service/local/staging/deploy/maven2";)
+             credentials {
+                 val asfNexusUsername = 
providers.gradleProperty("asfNexusUsername")
+                 val asfNexusPassword = 
providers.gradleProperty("asfNexusPassword")
+                 if (asfNexusUsername.isPresent() && 
asfNexusPassword.isPresent()) {
+                     username = asfNexusUsername.get()
+                     password = asfNexusPassword.get()
+                 }
+             }
+         }
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionAppender.java
index 0000000000,d3fac44fe9..788de0ff10
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionAppender.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DimensionAppender.java
@@@ -1,0 -1,184 +1,186 @@@
+ /*
+  * 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.coverage.grid;
+ 
+ import java.awt.image.RenderedImage;
+ import org.opengis.util.FactoryException;
+ import org.apache.sis.image.DataType;
+ import org.apache.sis.util.ArraysExt;
 -import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.util.internal.Numerics;
+ import org.apache.sis.feature.internal.Resources;
+ import org.apache.sis.coverage.SubspaceNotSpecifiedException;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coverage.CannotEvaluateException;
++// Specific to the main branch:
++import org.apache.sis.coverage.CannotEvaluateException;
++import org.opengis.geometry.MismatchedDimensionException;
+ 
+ 
+ /**
+  * A grid coverage with extra dimensions appended.
+  * All extra dimensions have a grid size of one cell.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ final class DimensionAppender extends GridCoverage {
+     /**
+      * The source grid coverage for which to append extra dimensions.
+      */
+     private final GridCoverage source;
+ 
+     /**
+      * The dimensions added to the source grid coverage.
+      * Should have a grid size of one cell in all dimensions.
+      */
+     private final GridGeometry dimToAdd;
+ 
+     /**
+      * Creates a new dimension appender for the given grid coverage.
+      * This constructor does not verify the grid geometry validity.
+      * It is caller's responsibility to verify that the size is 1 cell.
+      *
+      * @param  source    the source grid coverage for which to append extra 
dimensions.
+      * @param  dimToAdd  the dimensions to add to the source grid coverage.
+      * @throws FactoryException if the compound CRS cannot be created.
+      * @throws IllegalArgumentException if the concatenation results in 
duplicated
+      *         {@linkplain GridExtent#getAxisType(int) grid axis types}.
+      */
+     private DimensionAppender(final GridCoverage source, final GridGeometry 
dimToAdd) throws FactoryException {
+         super(source, new GridGeometry(source.getGridGeometry(), dimToAdd));
+         this.source   = source;
+         this.dimToAdd = dimToAdd;
+     }
+ 
+     /**
+      * Creates a grid coverage augmented with the given dimensions.
+      * The grid extent of {@code dimToAdd} shall have a grid size of one cell 
in all dimensions.
+      *
+      * @param  source    the source grid coverage for which to append extra 
dimensions.
+      * @param  dimToAdd  the dimensions to add to the source grid coverage.
+      * @throws FactoryException if the compound CRS cannot be created.
+      * @throws IllegalGridGeometryException if a dimension has more than one 
grid cell.
+      * @throws IllegalArgumentException if the concatenation results in 
duplicated
+      *         {@linkplain GridExtent#getAxisType(int) grid axis types}.
+      */
+     static GridCoverage create(GridCoverage source, GridGeometry dimToAdd) 
throws FactoryException {
+         final GridExtent extent = dimToAdd.getExtent();
+         int i = extent.getDimension();
+         if (i == 0) {
+             return source;
+         }
+         do {
+             final long size = extent.getSize(--i);
+             if (size != 1) {
+                 throw new 
IllegalGridGeometryException(Resources.format(Resources.Keys.NotASlice_2,
+                         extent.getAxisIdentification(i,i), size));
+             }
+         } while (i != 0);
+         if (source instanceof DimensionAppender) {
+             final var a = (DimensionAppender) source;
+             dimToAdd = new GridGeometry(a.dimToAdd, dimToAdd);
+             source = a.source;
+         }
+         return new DimensionAppender(source, dimToAdd);
+     }
+ 
+     /**
+      * Returns a grid coverage with a subset of the grid dimensions, or 
{@code null} if not possible by this method.
+      *
+      * @param  gridAxesToPass  the grid dimensions to keep. Indices must be 
in strictly increasing order.
+      * @return a grid coverage with the specified dimensions, or {@code null}.
+      * @throws FactoryException if the compound CRS cannot be created.
+      */
+     final GridCoverage selectDimensions(final int[] gridAxesToPass) throws 
FactoryException {
+         final int sourceDim = source.getGridGeometry().getDimension();
+         final int dimension = gridAxesToPass.length;
+         if (dimension < sourceDim || gridAxesToPass[0] != 0 || 
gridAxesToPass[sourceDim - 1] != sourceDim - 1) {
+             return null;
+         }
+         if (dimension == sourceDim) {
+             return source;
+         }
+         final int[] selected = new int[dimension - sourceDim];
+         for (int i=sourceDim; i<dimension; i++) {
+             selected[i - sourceDim] = gridAxesToPass[i] - sourceDim;
+         }
+         return create(source, dimToAdd.selectDimensions(selected));
+     }
+ 
+     /**
+      * Returns the data type identifying the primitive type used for storing 
sample values in each band.
+      */
+     @Override
+     final DataType getBandType() {
+         return source.getBandType();
+     }
+ 
+     /**
+      * Creates the grid coverage instance for the converted or packed values.
+      * This method is invoked only when first needed, and the result is 
cached by the caller.
+      */
+     @Override
+     protected GridCoverage createConvertedValues(final boolean converted) {
+         try {
+             return new 
DimensionAppender(source.forConvertedValues(converted), dimToAdd);
+         } catch (FactoryException e) {
+             throw new CannotEvaluateException(e.getMessage(), e);
+         }
+     }
+ 
+     /**
+      * Creates a new function for computing or interpolating sample values at 
given locations.
+      * This implementation returns the evaluator of the source coverage on 
the assumption that
+      * it should be able to do dimensionality reduction of the coordinates 
given to it.
+      */
+     @Override
+     public Evaluator evaluator() {
+         return source.evaluator();
+     }
+ 
+     /**
+      * Returns a two-dimensional slice of grid data as a rendered image.
+      *
+      * @param  sliceExtent  a subspace of this grid coverage where all 
dimensions except two have a size of 1 cell.
+      *         May be {@code null} if this grid coverage has only two 
dimensions with a size greater than 1 cell.
+      * @return the grid slice as a rendered image. Image location is relative 
to {@code sliceExtent}.
+      */
+     @Override
+     public RenderedImage render(GridExtent sliceExtent) {
+         if (sliceExtent != null) {
+             final int sourceDim = source.getGridGeometry().getDimension();
+             final int dimension = dimToAdd.getDimension() + sourceDim;
 -            ArgumentChecks.ensureDimensionMatches("sliceExtent", dimension, 
sliceExtent);
++            if (dimension != sliceExtent.getDimension()) {
++                throw new MismatchedDimensionException();
++            }
+             for (int i=sourceDim; i<dimension; i++) {
+                 final long size = sliceExtent.getSize(i);
+                 if (size != 1) {
+                     throw new 
SubspaceNotSpecifiedException(Resources.format(Resources.Keys.NoNDimensionalSlice_3,
+                                 sourceDim, 
sliceExtent.getAxisIdentification(i,i), Numerics.toUnsignedDouble(size)));
+                 }
+                 if (dimToAdd.extent != null) {
+                     final long actual = sliceExtent.getLow(i);
+                     final long expected = dimToAdd.extent.getLow(i - 
sourceDim);
+                     if (actual != expected) {
+                         throw new 
DisjointExtentException(sliceExtent.getAxisIdentification(i,i), expected, 
expected, actual, actual);
+                     }
+                 }
+             }
+             sliceExtent = sliceExtent.selectDimensions(ArraysExt.range(0, 
sourceDim));
+         }
+         return source.render(sliceExtent);
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
index 57fd762ce1,d58c9da9df..a6fb8b707e
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridExtent.java
@@@ -93,10 -92,10 +94,10 @@@ import org.apache.sis.coverage.PointOut
   * @author  Martin Desruisseaux (IRD, Geomatys)
   * @author  Alexis Manin (Geomatys)
   * @author  Johann Sorel (Geomatys)
-  * @version 1.4
+  * @version 1.5
   * @since   1.0
   */
 -public class GridExtent implements GridEnvelope, LenientComparable, 
Serializable {
 +public class GridExtent implements Serializable, LenientComparable {
      /**
       * Serial number for inter-operability with different versions.
       */
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
index c3ae920f32,71fd0754e3..9e1848e87c
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
@@@ -24,17 -24,16 +24,16 @@@ import java.util.Optional
  import org.apache.sis.feature.Features;
  import org.apache.sis.feature.builder.FeatureTypeBuilder;
  import org.apache.sis.feature.builder.PropertyTypeBuilder;
- import org.apache.sis.filter.internal.XPath;
  import org.apache.sis.math.FunctionProperty;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureAssociationRole;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.PropertyNotFoundException;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.ValueReference;
 +// Specific to the main branch:
 +import org.opengis.util.ScopedName;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.AbstractIdentifiedType;
 +import org.apache.sis.feature.DefaultAssociationRole;
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.pending.geoapi.filter.Name;
 +import org.apache.sis.pending.geoapi.filter.ValueReference;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/Registry.java
index c1ed330c19,fe74f5d3f7..11d52cb6de
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/Registry.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/Registry.java
@@@ -20,10 -20,11 +20,10 @@@ import java.util.Arrays
  import java.util.Collection;
  import org.apache.sis.geometry.wrapper.Geometries;
  import org.apache.sis.filter.internal.FunctionRegister;
- import org.apache.sis.pending.jdk.JDK17;
+ import org.apache.sis.pending.jdk.JDK16;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.capability.AvailableFunction;
 +// Specific to the main branch:
 +import org.apache.sis.filter.Expression;
  
  
  /**
@@@ -61,9 -62,21 +61,9 @@@ public final class Registry implements 
       */
      @Override
      public Collection<String> getNames() {
-         return JDK17.toList(Arrays.stream(SQLMM.values()).map(SQLMM::name));
+         return JDK16.toList(Arrays.stream(SQLMM.values()).map(SQLMM::name));
      }
  
 -    /**
 -     * Describes the parameters of a function.
 -     *
 -     * @param  name  name of the function to describe.
 -     * @return description of the function parameters.
 -     * @throws IllegalArgumentException if function name is unknown..
 -     */
 -    @Override
 -    public AvailableFunction describe(final String name) {
 -        return SQLMM.valueOf(name).description(library);
 -    }
 -
      /**
       * Create a new function of the given name with given parameters.
       * It is caller's responsibility to ensure that the given array is 
non-null,
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PixelIterator.java
index 217a7c69c5,7a13096dad..fae6178ac5
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PixelIterator.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PixelIterator.java
@@@ -46,8 -46,11 +46,8 @@@ import org.apache.sis.measure.NumberRan
  import org.apache.sis.util.internal.Numerics;
  import org.apache.sis.feature.internal.Resources;
  import org.apache.sis.coverage.grid.j2d.ImageUtilities;
- import static org.apache.sis.util.internal.Numerics.ceilDiv;
+ import static org.apache.sis.pending.jdk.JDK18.ceilDiv;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coverage.grid.SequenceType;
 -
  
  /**
   * An iterator over sample values in a raster or an image.  This iterator 
makes easier to read and write efficiently
diff --cc 
endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DimensionAppenderTest.java
index 0000000000,b3e40a1eec..0476c35d4a
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DimensionAppenderTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DimensionAppenderTest.java
@@@ -1,0 -1,131 +1,131 @@@
+ /*
+  * 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.coverage.grid;
+ 
+ import java.time.Instant;
+ import java.time.Duration;
+ import java.awt.image.BufferedImage;
+ import org.opengis.referencing.datum.PixelInCell;
+ import org.opengis.referencing.operation.Matrix;
+ import org.opengis.referencing.operation.MathTransform;
+ import org.opengis.referencing.operation.TransformException;
+ import org.apache.sis.referencing.operation.matrix.Matrix3;
+ import org.apache.sis.referencing.operation.matrix.Matrix4;
+ import org.apache.sis.referencing.operation.matrix.Matrices;
+ import org.apache.sis.referencing.operation.transform.MathTransforms;
+ import org.apache.sis.util.ArraysExt;
+ 
+ // Test dependencies
+ import org.junit.Test;
+ import static org.junit.jupiter.api.Assertions.*;
+ import org.apache.sis.test.TestCase;
+ import org.apache.sis.referencing.crs.HardCodedCRS;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import static org.opengis.test.Assert.assertMatrixEquals;
++// Specific to the main branch:
++import static org.apache.sis.test.GeoapiAssert.assertMatrixEquals;
+ 
+ 
+ /**
+  * Tests {@link DimensionAppender}. This is partially the converse of {@link 
DimensionalityReductionTest}.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class DimensionAppenderTest extends TestCase {
+     /**
+      * Creates a new test case.
+      */
+     public DimensionAppenderTest() {
+     }
+ 
+     /**
+      * Returns a grid coverage to use as a starting point.
+      *
+      * @param  width   image width in pixels.
+      * @param  height  image height in pixels.
+      */
+     private static GridCoverage initial(final int width, final int height) {
+         var extent = new GridExtent(width, height);
+         var gridToCRS = new Matrix3(
+                 4, 0, 100,
+                 0, 3, -20,
+                 0, 0,   1);
+ 
+         var gg = new GridGeometry(extent, PixelInCell.CELL_CORNER, 
MathTransforms.linear(gridToCRS), HardCodedCRS.WGS84);
+         return new GridCoverage2D(gg, null, new BufferedImage(width, height, 
BufferedImage.TYPE_BYTE_BINARY));
+     }
+ 
+     /**
+      * Asserts that the grid geometry of the given coverage has the expected 
properties.
+      *
+      * @param actual        the coverage for which to verify the grid 
geometry.
+      * @param gridToCRS     expected "grid to CRS" transform as a matrix.
+      * @param gridIndices   expected lower grid coordinate values.
+      */
+     private static void assertGridGeometryEquals(final GridCoverage actual, 
final Matrix gridToCRS, final long... gridIndices) {
+         final GridGeometry gg = actual.getGridGeometry();
+         assertMatrixEquals("gridToCRS", gridToCRS, 
MathTransforms.getMatrix(gg.getGridToCRS(PixelInCell.CELL_CORNER)), STRICT);
+         assertArrayEquals(gridIndices, 
gg.getExtent().getLow().getCoordinateValues());
+     }
+ 
+     /**
+      * Verifies that the conversion of lower grid coordinates to CRS produces 
the expected values.
+      *
+      * @param actual     the coverage for which to verify the coordinate 
conversion.
+      * @param  expected  expected coordinates in units of the CRS.
+      * @throws TransformException if the conversion failed.
+      */
+     private static void verifyTransformLower(final GridCoverage actual, final 
double... expected) throws TransformException {
+         final GridGeometry gg = actual.getGridGeometry();
+         final MathTransform gridToCRS = 
gg.getGridToCRS(PixelInCell.CELL_CORNER);
+         final double[] coordinates = 
ArraysExt.copyAsDoubles(gg.getExtent().getLow().getCoordinateValues());
+         gridToCRS.transform(coordinates, 0, coordinates, 0, 1);
+         assertArrayEquals(expected, coordinates);
+     }
+ 
+     /**
+      * Tests the {@link GridCoverageProcessor} convenience methods.
+      *
+      * @throws TransformException if the coordinate conversion failed.
+      */
+     @Test
+     public void testUsingProcessor() throws TransformException {
+         final var coverage2D = initial(16, 8);
+         final var processor  = new GridCoverageProcessor();
+         final var coverage3D = processor.appendDimension(coverage2D, 260, 5, 
HardCodedCRS.GRAVITY_RELATED_HEIGHT);
+         assertSame(coverage2D, processor.selectGridDimensions(coverage3D, 0, 
1));
+         verifyTransformLower(coverage3D, 100, -20, 260);
+         assertGridGeometryEquals(coverage3D, new Matrix4(
+                 4, 0, 0, 100,
+                 0, 3, 0, -20,
+                 0, 0, 5,   0,
+                 0, 0, 0,   1), 0, 0, 52);
+ 
+         final var coverage4D = processor.appendDimension(coverage3D, 
Instant.parse("2022-06-18T00:00:00Z"), Duration.parse("P21D"));
+         assertSame(coverage2D, processor.selectGridDimensions(coverage3D, 0, 
1));
+         verifyTransformLower(coverage4D, 100, -20, 260, 19748);
+         assertGridGeometryEquals(coverage4D, Matrices.create(5, 5, new 
double[] {
+                 4, 0, 0,  0, 100,
+                 0, 3, 0,  0, -20,
+                 0, 0, 5,  0,   0,
+                 0, 0, 0, 21,   8,
+                 0, 0, 0, 0,    1}), 0, 0, 52, 940);
+ 
+         // Easy way to check that the correct dimensions were selected.
+         verifyTransformLower(processor.selectGridDimensions(coverage4D, 0, 1, 
2), 100, -20, 260);
+         verifyTransformLower(processor.selectGridDimensions(coverage4D, 0, 1, 
3), 100, -20, 19748);
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/GeohashReferenceSystem.java
index 631c19bdb2,a69353edce..2304842712
--- 
a/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/GeohashReferenceSystem.java
+++ 
b/endorsed/src/org.apache.sis.referencing.gazetteer/main/org/apache/sis/referencing/gazetteer/GeohashReferenceSystem.java
@@@ -41,10 -41,14 +41,10 @@@ import org.apache.sis.util.Utilities
  import org.apache.sis.util.Workaround;
  import org.apache.sis.util.ComparisonMode;
  import org.apache.sis.util.ArgumentChecks;
- import org.apache.sis.util.internal.Numerics;
  import org.apache.sis.util.resources.Errors;
  import org.apache.sis.util.resources.Vocabulary;
+ import org.apache.sis.pending.jdk.JDK18;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.referencing.gazetteer.Location;
 -import org.opengis.referencing.gazetteer.LocationType;
 -
  
  /**
   * Geographic coordinates represented as <cite>geohashes</cite> strings.
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/DimensionAppender.java
index 0000000000,d25ac1af63..21eb277837
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/DimensionAppender.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/DimensionAppender.java
@@@ -1,0 -1,244 +1,246 @@@
+ /*
+  * 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.aggregate;
+ 
+ import java.util.Arrays;
+ import java.util.List;
+ import java.util.Optional;
+ import org.opengis.util.GenericName;
+ import org.opengis.util.FactoryException;
+ import org.opengis.metadata.Metadata;
+ import org.opengis.referencing.datum.PixelInCell;
+ import org.opengis.referencing.operation.TransformException;
+ import org.apache.sis.coverage.SampleDimension;
+ import org.apache.sis.coverage.grid.GridExtent;
+ import org.apache.sis.coverage.grid.GridGeometry;
+ import org.apache.sis.coverage.grid.GridCoverage;
+ import org.apache.sis.coverage.grid.GridCoverageProcessor;
+ import org.apache.sis.coverage.grid.IllegalGridGeometryException;
+ import org.apache.sis.storage.Query;
+ import org.apache.sis.storage.GridCoverageResource;
+ import org.apache.sis.storage.RasterLoadingStrategy;
+ import org.apache.sis.storage.DataStoreException;
+ import org.apache.sis.storage.DataStoreReferencingException;
+ import org.apache.sis.storage.base.StoreUtilities;
+ import org.apache.sis.storage.event.StoreEvent;
+ import org.apache.sis.storage.event.StoreListener;
+ import org.apache.sis.storage.internal.Resources;
 -import org.apache.sis.util.ArraysExt;
+ import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.util.logging.Logging;
+ 
+ 
+ /**
+  * A wrapper over an existing grid coverage resource with dimensions appended.
+  * This wrapper delegates the work to {@link GridCoverageProcessor} after a 
coverage has been read.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ final class DimensionAppender implements GridCoverageResource {
+     /**
+      * The grid coverage processor to use for creating grid coverages with 
extra dimensions.
+      */
+     private final GridCoverageProcessor processor;
+ 
+     /**
+      * The source grid coverage resource for which to append extra dimensions.
+      */
+     private final GridCoverageResource source;
+ 
+     /**
+      * The dimensions added to the source grid coverage.
+      * Should have a grid size of one cell in all dimensions.
+      */
+     private final GridGeometry dimToAdd;
+ 
+     /**
+      * The grid geometry with dimensions appended.
+      * Created when first requested.
+      *
+      * @see #getGridGeometry()
+      */
+     private volatile GridGeometry gridGeometry;
+ 
+     /**
+      * Creates a new dimension appender for the given grid coverage resource.
+      * This constructor does not verify the grid geometry validity.
+      * It is caller's responsibility to verify that the size is 1 cell.
+      *
+      * @param  processor  the grid coverage processor to use for creating 
grid coverages with extra dimensions.
+      * @param  source     the source grid coverage for which to append extra 
dimensions.
+      * @param  dimToAdd   the dimensions to add to the source grid coverage.
+      */
+     private DimensionAppender(final GridCoverageProcessor processor, final 
GridCoverageResource source, final GridGeometry dimToAdd) {
+         this.processor = processor;
+         this.source    = source;
+         this.dimToAdd  = dimToAdd;
+     }
+ 
+     /**
+      * Creates a grid coverage resource augmented with the given dimensions.
+      * The grid extent of {@code dimToAdd} shall have a grid size of one cell 
in all dimensions.
+      *
+      * @param  processor  the grid coverage processor to use for creating 
grid coverages with extra dimensions.
+      * @param  source     the source grid coverage for which to append extra 
dimensions.
+      * @param  dimToAdd   the dimensions to add to the source grid coverage.
+      * @throws IllegalGridGeometryException if a dimension has more than one 
grid cell.
+      */
+     static GridCoverageResource create(final GridCoverageProcessor processor, 
GridCoverageResource source, GridGeometry dimToAdd) {
+         ArgumentChecks.ensureNonNull("source", source);
+         final GridExtent extent = dimToAdd.getExtent();
+         int i = extent.getDimension();
+         if (i == 0) {
+             return source;
+         }
+         do {
+             final long size = extent.getSize(--i);
+             if (size != 1) {
+                 Object name = extent.getAxisType(i).orElse(null);
+                 if (name == null) name = i;
+                 throw new 
IllegalGridGeometryException(Resources.format(Resources.Keys.NoSliceSpecified_2,
 name, size));
+             }
+         } while (i != 0);
+         if (source instanceof DimensionAppender) try {
+             final var a = (DimensionAppender) source;
+             dimToAdd = new GridGeometry(a.dimToAdd, dimToAdd);
+             source = a.source;
+         } catch (FactoryException e) {
+             throw new IllegalGridGeometryException(e.getMessage(), e);
+         }
+         return new DimensionAppender(processor, source, dimToAdd);
+     }
+ 
+     /**
+      * Returns the identifier of the original resource.
+      */
+     @Override
+     public Optional<GenericName> getIdentifier() throws DataStoreException {
+         return source.getIdentifier();
+     }
+ 
+     /**
+      * Returns the metadata of the original resource.
+      */
+     @Override
+     public Metadata getMetadata() throws DataStoreException {
+         return source.getMetadata();
+     }
+ 
+     /**
+      * Returns the sample dimensions of the original resource.
+      * Those dimensions are not impacted by the change of domain dimensions.
+      */
+     @Override
+     public List<SampleDimension> getSampleDimensions() throws 
DataStoreException {
+         return source.getSampleDimensions();
+     }
+ 
+     /**
+      * Returns the grid geometry of the original resource augmented with the 
dimensions to append.
+      */
+     @Override
+     public GridGeometry getGridGeometry() throws DataStoreException {
+         GridGeometry gg = gridGeometry;
+         if (gg == null) try {
+             gg = new GridGeometry(source.getGridGeometry(), dimToAdd);
+             gridGeometry = gg;
+         } catch (FactoryException | RuntimeException e) {
+             throw new DataStoreReferencingException(e.getMessage(), e);
+         }
+         return gg;
+     }
+ 
+     /**
+      * Returns a subset of this grid coverage resource.
+      * The result will have the same "dimensions to add" than this resource.
+      *
+      * @param  query  the query to execute.
+      * @return subset of this coverage resource.
+      */
+     @Override
+     public GridCoverageResource subset(final Query query) throws 
DataStoreException {
+         final GridCoverageResource subset = source.subset(query);
+         if (subset == source) return this;
+         return new DimensionAppender(processor, subset, dimToAdd);
+     }
+ 
+     /**
+      * Reads the data and wraps the result with the dimensions to add.
+      */
+     @Override
+     public GridCoverage read(GridGeometry domain, int... ranges) throws 
DataStoreException {
+         return processor.appendDimensions(source.read(domain, ranges), 
dimToAdd);
+     }
+ 
+     /**
+      * Returns an indication about when the "physical" loading of raster data 
will happen.
+      */
+     @Override
+     public RasterLoadingStrategy getLoadingStrategy() throws 
DataStoreException {
+         return source.getLoadingStrategy();
+     }
+ 
+     /**
+      * Sets the preferred strategy about when to do the "physical" loading of 
raster data.
+      */
+     @Override
+     public boolean setLoadingStrategy(RasterLoadingStrategy strategy) throws 
DataStoreException {
+         return source.setLoadingStrategy(strategy);
+     }
+ 
+     /**
+      * Registers a listener to notify when the specified kind of event occurs 
in this resource or in children.
+      */
+     @Override
+     public <T extends StoreEvent> void addListener(Class<T> eventType, 
StoreListener<? super T> listener) {
+         source.addListener(eventType, listener);
+     }
+ 
+     /**
+      * Unregisters a listener previously added to this resource for the given 
type of events.
+      */
+     @Override
+     public <T extends StoreEvent> void removeListener(Class<T> eventType, 
StoreListener<? super T> listener) {
+         source.removeListener(eventType, listener);
+     }
+ 
+     /**
+      * Returns a string representation of this wrapper for debugging purposes.
+      */
+     @Override
+     public String toString() {
+         final var sb = new StringBuilder(40);
+         sb.append(source).append(" + dimensions[");
+         final GridExtent extent = dimToAdd.getExtent();
 -        final double[] coordinates = 
ArraysExt.copyAsDoubles(extent.getLow().getCoordinateValues());
++        final double[] coordinates = new double[extent.getDimension()];
++        for (int i=0; i<coordinates.length; i++) {
++            coordinates[i] = extent.getLow(i);
++        }
+         try {
+             
dimToAdd.getGridToCRS(PixelInCell.CELL_CORNER).transform(coordinates, 0, 
coordinates, 0, 1);
+         } catch (RuntimeException | TransformException e) {
+             // Should never happen because the transform should be linear.
+             Logging.unexpectedException(StoreUtilities.LOGGER, 
DimensionAppender.class, "toString", e);
+             Arrays.fill(coordinates, Double.NaN);
+         }
+         for (int i=0; i<coordinates.length; i++) {
+             if (i != 0) sb.append(", ");
+             extent.getAxisType(i).ifPresent((type) -> 
sb.append(type.name()).append('='));
+             sb.append(coordinates[i]);
+         }
+         return sb.append(']').toString();
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java
index 085c3cc974,c78ffa373b..6dce073f48
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/TiledGridCoverage.java
@@@ -46,10 -46,10 +46,10 @@@ import org.apache.sis.storage.tiling.Ti
  import org.apache.sis.storage.internal.Resources;
  import org.apache.sis.util.collection.WeakValueHashMap;
  import org.apache.sis.util.resources.Errors;
- import static org.apache.sis.util.internal.Numerics.ceilDiv;
+ import static org.apache.sis.pending.jdk.JDK18.ceilDiv;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.coverage.CannotEvaluateException;
 +// Specific to the main branch:
 +import org.apache.sis.coverage.CannotEvaluateException;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java
index e493a8d8ed,f6821a7fd4..58b1996488
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/Store.java
@@@ -269,8 -260,11 +260,11 @@@ class Store extends DataStore implement
      public synchronized Metadata getMetadata() {
          if (metadata == null) {
              final MetadataBuilder mb = new MetadataBuilder();
 -            mb.addResourceScope(ScopeCode.COLLECTION, 
Resources.formatInternational(Resources.Keys.DirectoryContent_1, 
getDisplayName()));
 +            mb.addResourceScope(ScopeCode.valueOf("COLLECTION"), 
Resources.formatInternational(Resources.Keys.DirectoryContent_1, 
getDisplayName()));
-             mb.addLanguage(locale, encoding, MetadataBuilder.Scope.RESOURCE);
+             mb.addLanguage(configuration.getOption(OptionKey.LOCALE),
+                            configuration.getOption(OptionKey.ENCODING),
+                            MetadataBuilder.Scope.RESOURCE);
+ 
              final GenericName identifier = identifier(null);
              String name = null;
              if (identifier != null) {
diff --cc 
incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
index 5d2a1958ca,303b6d6f20..928948f815
--- 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
@@@ -99,20 -103,22 +103,21 @@@ import org.apache.sis.storage.shapefile
  import org.apache.sis.storage.shapefile.shp.ShapeType;
  import org.apache.sis.storage.shapefile.shp.ShapeWriter;
  import org.apache.sis.storage.shapefile.shx.IndexWriter;
+ import org.apache.sis.util.ArraysExt;
  import org.apache.sis.util.collection.BackingStoreException;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.util.CodeList;
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.LogicalOperator;
 -import org.opengis.filter.LogicalOperatorName;
 -import org.opengis.filter.SpatialOperatorName;
 -import org.opengis.filter.ValueReference;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.feature.AbstractIdentifiedType;
 +import org.apache.sis.feature.DefaultAttributeType;
 +import org.apache.sis.filter.Expression;
 +import org.apache.sis.filter.Filter;
 +import org.apache.sis.pending.geoapi.filter.Literal;
 +import org.apache.sis.pending.geoapi.filter.LogicalOperator;
 +import org.apache.sis.pending.geoapi.filter.LogicalOperatorName;
 +import org.apache.sis.pending.geoapi.filter.SpatialOperatorName;
 +import org.apache.sis.pending.geoapi.filter.ValueReference;
  
  
  /**
@@@ -391,9 -410,10 +409,10 @@@ public final class ShapefileStore exten
                              final ShapeRecord shpRecord = shpreader.next();
                              if (shpRecord == null) return false;
                              //move dbf to record offset, some shp record 
might have been skipped because of filter
-                             dbfreader.moveToOffset(header.headerSize + 
(shpRecord.recordNumber-1) * header.recordSize);
+                             long offset = (long)header.headerSize + 
((long)(shpRecord.recordNumber-1)) * ((long)header.recordSize);
+                             dbfreader.moveToOffset(offset);
                              final DBFRecord dbfRecord = dbfreader.next();
 -                            final Feature next = type.newInstance();
 +                            final AbstractFeature next = type.newInstance();
                              next.setPropertyValue(GEOMETRY_NAME, 
shpRecord.geometry);
                              for (int i = 0; i < dbfPropertiesIndex.length; 
i++) {
                                  
next.setPropertyValue(header.fields[dbfPropertiesIndex[i]].fieldName, 
dbfRecord.fields[i]);
@@@ -574,13 -597,17 +598,17 @@@
  
              final ShapeHeader shpHeader = new ShapeHeader();
              final DBFHeader dbfHeader = new DBFHeader();
-             Charset charset = StandardCharsets.UTF_8;
+             final Charset charset = userDefinedCharSet == null ? 
StandardCharsets.UTF_8 : userDefinedCharSet;
              CoordinateReferenceSystem crs = 
CommonCRS.WGS84.normalizedGeographic();
  
 -            for (PropertyType pt : newType.getProperties(true)) {
 -                if (pt instanceof AttributeType) {
 -                    final AttributeType at = (AttributeType) pt;
 +            for (AbstractIdentifiedType pt : newType.getProperties(true)) {
 +                if (pt instanceof DefaultAttributeType) {
 +                    final DefaultAttributeType at = (DefaultAttributeType) pt;
                      final Class valueClass = at.getValueClass();
+ 
+                     Integer length = 
AttributeConvention.getMaximalLengthCharacteristic(newType, pt);
+                     if (length == 0) length = 255;
+ 
                      if (Geometry.class.isAssignableFrom(valueClass)) {
                          if (shpHeader.shapeType != 0) {
                              throw new DataStoreException("Shapefile format 
can only contain one geometry");
diff --cc 
incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
index 35b95f1a28,6d0152954f..f2c3c8ff0d
--- 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
@@@ -50,49 -52,50 +51,50 @@@ public class ShapefileStoreTest 
      @Test
      public void testStream() throws URISyntaxException, DataStoreException {
          final URL url = 
ShapefileStoreTest.class.getResource("/org/apache/sis/storage/shapefile/point.shp");
-         final ShapefileStore store = new 
ShapefileStore(Paths.get(url.toURI()));
- 
-         //check feature type
-         final DefaultFeatureType type = store.getType();
-         assertEquals("point", type.getName().toString());
-         assertEquals(9, type.getProperties(true).size());
-         assertNotNull(type.getProperty("sis:identifier"));
-         assertNotNull(type.getProperty("sis:envelope"));
-         assertNotNull(type.getProperty("sis:geometry"));
-         final var geomProp    = (DefaultAttributeType) 
type.getProperty("geometry");
-         final var idProp      = (DefaultAttributeType) type.getProperty("id");
-         final var textProp    = (DefaultAttributeType) 
type.getProperty("text");
-         final var integerProp = (DefaultAttributeType) 
type.getProperty("integer");
-         final var floatProp   = (DefaultAttributeType) 
type.getProperty("float");
-         final var dateProp    = (DefaultAttributeType) 
type.getProperty("date");
-         assertEquals(Point.class, geomProp.getValueClass());
-         assertEquals(Long.class, idProp.getValueClass());
-         assertEquals(String.class, textProp.getValueClass());
-         assertEquals(Long.class, integerProp.getValueClass());
-         assertEquals(Double.class, floatProp.getValueClass());
-         assertEquals(LocalDate.class, dateProp.getValueClass());
- 
-         try (Stream<AbstractFeature> stream = store.features(false)) {
-             Iterator<AbstractFeature> iterator = stream.iterator();
-             assertTrue(iterator.hasNext());
-             AbstractFeature feature1 = iterator.next();
-             assertEquals(1L, feature1.getPropertyValue("id"));
-             assertEquals("text1", feature1.getPropertyValue("text"));
-             assertEquals(10L, feature1.getPropertyValue("integer"));
-             assertEquals(20.0, feature1.getPropertyValue("float"));
-             assertEquals(LocalDate.of(2023, 10, 27), 
feature1.getPropertyValue("date"));
-             Point pt1 = (Point) feature1.getPropertyValue("geometry");
- 
-             assertTrue(iterator.hasNext());
-             AbstractFeature feature2 = iterator.next();
-             assertEquals(2L, feature2.getPropertyValue("id"));
-             assertEquals("text2", feature2.getPropertyValue("text"));
-             assertEquals(40L, feature2.getPropertyValue("integer"));
-             assertEquals(60.0, feature2.getPropertyValue("float"));
-             assertEquals(LocalDate.of(2023, 10, 28), 
feature2.getPropertyValue("date"));
-             Point pt2 = (Point) feature2.getPropertyValue("geometry");
- 
-             assertFalse(iterator.hasNext());
+         try (final ShapefileStore store = new 
ShapefileStore(Paths.get(url.toURI()))) {
+ 
+             //check feature type
 -            final FeatureType type = store.getType();
++            final DefaultFeatureType type = store.getType();
+             assertEquals("point", type.getName().toString());
+             assertEquals(9, type.getProperties(true).size());
+             assertNotNull(type.getProperty("sis:identifier"));
+             assertNotNull(type.getProperty("sis:envelope"));
+             assertNotNull(type.getProperty("sis:geometry"));
 -            final var geomProp    = (AttributeType) 
type.getProperty("geometry");
 -            final var idProp      = (AttributeType) type.getProperty("id");
 -            final var textProp    = (AttributeType) type.getProperty("text");
 -            final var integerProp = (AttributeType) 
type.getProperty("integer");
 -            final var floatProp   = (AttributeType) type.getProperty("float");
 -            final var dateProp    = (AttributeType) type.getProperty("date");
++            final var geomProp    = (DefaultAttributeType) 
type.getProperty("geometry");
++            final var idProp      = (DefaultAttributeType) 
type.getProperty("id");
++            final var textProp    = (DefaultAttributeType) 
type.getProperty("text");
++            final var integerProp = (DefaultAttributeType) 
type.getProperty("integer");
++            final var floatProp   = (DefaultAttributeType) 
type.getProperty("float");
++            final var dateProp    = (DefaultAttributeType) 
type.getProperty("date");
+             assertEquals(Point.class, geomProp.getValueClass());
+             assertEquals(Long.class, idProp.getValueClass());
+             assertEquals(String.class, textProp.getValueClass());
+             assertEquals(Long.class, integerProp.getValueClass());
+             assertEquals(Double.class, floatProp.getValueClass());
+             assertEquals(LocalDate.class, dateProp.getValueClass());
+ 
 -            try (Stream<Feature> stream = store.features(false)) {
 -                Iterator<Feature> iterator = stream.iterator();
++            try (Stream<AbstractFeature> stream = store.features(false)) {
++                Iterator<AbstractFeature> iterator = stream.iterator();
+                 assertTrue(iterator.hasNext());
 -                Feature feature1 = iterator.next();
++                AbstractFeature feature1 = iterator.next();
+                 assertEquals(1L, feature1.getPropertyValue("id"));
+                 assertEquals("text1", feature1.getPropertyValue("text"));
+                 assertEquals(10L, feature1.getPropertyValue("integer"));
+                 assertEquals(20.0, feature1.getPropertyValue("float"));
+                 assertEquals(LocalDate.of(2023, 10, 27), 
feature1.getPropertyValue("date"));
+                 Point pt1 = (Point) feature1.getPropertyValue("geometry");
+ 
+                 assertTrue(iterator.hasNext());
 -                Feature feature2 = iterator.next();
++                AbstractFeature feature2 = iterator.next();
+                 assertEquals(2L, feature2.getPropertyValue("id"));
+                 assertEquals("text2", feature2.getPropertyValue("text"));
+                 assertEquals(40L, feature2.getPropertyValue("integer"));
+                 assertEquals(60.0, feature2.getPropertyValue("float"));
+                 assertEquals(LocalDate.of(2023, 10, 28), 
feature2.getPropertyValue("date"));
+                 Point pt2 = (Point) feature2.getPropertyValue("geometry");
+ 
+                 assertFalse(iterator.hasNext());
+             }
          }
      }
  
@@@ -102,32 -105,33 +104,33 @@@
      @Test
      public void testEnvelopeFilter() throws URISyntaxException, 
DataStoreException {
          final URL url = 
ShapefileStoreTest.class.getResource("/org/apache/sis/storage/shapefile/point.shp");
-         final ShapefileStore store = new 
ShapefileStore(Paths.get(url.toURI()));
- 
-         final DefaultFilterFactory<AbstractFeature, Object, Object> ff = 
DefaultFilterFactory.forFeatures();
- 
-         final GeneralEnvelope env = new 
GeneralEnvelope(CommonCRS.WGS84.normalizedGeographic());
-         env.setRange(0, 2, 3);
-         env.setRange(1, 42, 43);
- 
-         final FeatureQuery query = new FeatureQuery();
-         query.setSelection(ff.bbox(ff.property("geometry"), env));
-         FeatureSet featureset = store.subset(query);
-         //ensure we obtained an optimized version
-         
assertEquals("org.apache.sis.storage.shapefile.ShapefileStore$AsFeatureSet", 
featureset.getClass().getName());
- 
-         try (Stream<AbstractFeature> stream = featureset.features(false)) {
-             Iterator<AbstractFeature> iterator = stream.iterator();
-             assertTrue(iterator.hasNext());
-             AbstractFeature feature = iterator.next();
-             assertEquals(2L, feature.getPropertyValue("id"));
-             assertEquals("text2", feature.getPropertyValue("text"));
-             assertEquals(40L, feature.getPropertyValue("integer"));
-             assertEquals(60.0, feature.getPropertyValue("float"));
-             assertEquals(LocalDate.of(2023, 10, 28), 
feature.getPropertyValue("date"));
-             Point pt2 = (Point) feature.getPropertyValue("geometry");
- 
-             assertFalse(iterator.hasNext());
+         try (final ShapefileStore store = new 
ShapefileStore(Paths.get(url.toURI()))) {
+ 
 -            final FilterFactory<Feature, Object, Object> ff = 
DefaultFilterFactory.forFeatures();
++            final DefaultFilterFactory<AbstractFeature, Object, Object> ff = 
DefaultFilterFactory.forFeatures();
+ 
+             final GeneralEnvelope env = new 
GeneralEnvelope(CommonCRS.WGS84.normalizedGeographic());
+             env.setRange(0, 2, 3);
+             env.setRange(1, 42, 43);
+ 
+             final FeatureQuery query = new FeatureQuery();
+             query.setSelection(ff.bbox(ff.property("geometry"), env));
+             FeatureSet featureset = store.subset(query);
+             //ensure we obtained an optimized version
+             
assertEquals("org.apache.sis.storage.shapefile.ShapefileStore$AsFeatureSet", 
featureset.getClass().getName());
+ 
 -            try (Stream<Feature> stream = featureset.features(false)) {
 -                Iterator<Feature> iterator = stream.iterator();
++            try (Stream<AbstractFeature> stream = featureset.features(false)) 
{
++                Iterator<AbstractFeature> iterator = stream.iterator();
+                 assertTrue(iterator.hasNext());
 -                Feature feature = iterator.next();
++                AbstractFeature feature = iterator.next();
+                 assertEquals(2L, feature.getPropertyValue("id"));
+                 assertEquals("text2", feature.getPropertyValue("text"));
+                 assertEquals(40L, feature.getPropertyValue("integer"));
+                 assertEquals(60.0, feature.getPropertyValue("float"));
+                 assertEquals(LocalDate.of(2023, 10, 28), 
feature.getPropertyValue("date"));
+                 Point pt2 = (Point) feature.getPropertyValue("geometry");
+ 
+                 assertFalse(iterator.hasNext());
+             }
          }
      }
  
@@@ -137,28 -141,41 +140,41 @@@
      @Test
      public void testFieldFilter() throws URISyntaxException, 
DataStoreException {
          final URL url = 
ShapefileStoreTest.class.getResource("/org/apache/sis/storage/shapefile/point.shp");
-         final ShapefileStore store = new 
ShapefileStore(Paths.get(url.toURI()));
- 
- 
-         final FeatureQuery query = new FeatureQuery();
-         query.setProjection("text", "float");
-         FeatureSet featureset = store.subset(query);
-         //ensure we obtained an optimized version
-         
assertEquals("org.apache.sis.storage.shapefile.ShapefileStore$AsFeatureSet", 
featureset.getClass().getName());
- 
-         try (Stream<AbstractFeature> stream = featureset.features(false)) {
-             Iterator<AbstractFeature> iterator = stream.iterator();
-             assertTrue(iterator.hasNext());
-             AbstractFeature feature1 = iterator.next();
-             assertEquals("text1", feature1.getPropertyValue("text"));
-             assertEquals(20.0, feature1.getPropertyValue("float"));
- 
-             assertTrue(iterator.hasNext());
-             AbstractFeature feature2 = iterator.next();
-             assertEquals("text2", feature2.getPropertyValue("text"));
-             assertEquals(60.0, feature2.getPropertyValue("float"));
+         try (final ShapefileStore store = new 
ShapefileStore(Paths.get(url.toURI()))) {
+             final FeatureQuery query = new FeatureQuery();
+             query.setProjection("text", "float");
+             FeatureSet featureset = store.subset(query);
+             //ensure we obtained an optimized version
+             
assertEquals("org.apache.sis.storage.shapefile.ShapefileStore$AsFeatureSet", 
featureset.getClass().getName());
+ 
 -            try (Stream<Feature> stream = featureset.features(false)) {
 -                Iterator<Feature> iterator = stream.iterator();
++            try (Stream<AbstractFeature> stream = featureset.features(false)) 
{
++                Iterator<AbstractFeature> iterator = stream.iterator();
+                 assertTrue(iterator.hasNext());
 -                Feature feature1 = iterator.next();
++                AbstractFeature feature1 = iterator.next();
+                 assertEquals("text1", feature1.getPropertyValue("text"));
+                 assertEquals(20.0, feature1.getPropertyValue("float"));
+ 
+                 assertTrue(iterator.hasNext());
 -                Feature feature2 = iterator.next();
++                AbstractFeature feature2 = iterator.next();
+                 assertEquals("text2", feature2.getPropertyValue("text"));
+                 assertEquals(60.0, feature2.getPropertyValue("float"));
+ 
+                 assertFalse(iterator.hasNext());
+             }
+         }
+     }
  
-             assertFalse(iterator.hasNext());
+     @Test
+     public void testFiles() throws URISyntaxException, DataStoreException {
+         final URL url = 
ShapefileStoreTest.class.getResource("/org/apache/sis/storage/shapefile/point.shp");
+         try (final ShapefileStore store = new 
ShapefileStore(Paths.get(url.toURI()))) {
+             Path[] componentFiles = store.getComponentFiles();
+             assertEquals(5, componentFiles.length);
+             componentFiles[0].toString().endsWith("point.shp");
+             componentFiles[1].toString().endsWith("point.shx");
+             componentFiles[2].toString().endsWith("point.dbf");
+             componentFiles[3].toString().endsWith("point.prj");
+             componentFiles[4].toString().endsWith("point.cpg");
          }
      }
  

Reply via email to