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

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

commit d65b2a0595a2db7a91c98716753ec1b01a49b9f5
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Sep 10 12:24:01 2021 +0200

    Provide a common ground between `FeatureQuery` and `CoverageQuery`.
---
 .../java/org/apache/sis/storage/CoverageQuery.java | 125 +++++++++++++++++++--
 .../org/apache/sis/storage/CoverageSubset.java     |   9 +-
 .../java/org/apache/sis/storage/FeatureQuery.java  |  44 ++++++++
 .../main/java/org/apache/sis/storage/Query.java    |  74 +++++++-----
 .../org/apache/sis/storage/CoverageQueryTest.java  |  38 ++++++-
 .../org/apache/sis/storage/FeatureQueryTest.java   |  19 +++-
 6 files changed, 263 insertions(+), 46 deletions(-)

diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageQuery.java 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageQuery.java
index c668af6..c188cf1 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageQuery.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageQuery.java
@@ -16,23 +16,40 @@
  */
 package org.apache.sis.storage;
 
+import java.util.List;
 import java.util.Arrays;
 import java.util.Objects;
 import java.io.Serializable;
 import java.math.RoundingMode;
+import org.opengis.util.GenericName;
+import org.opengis.util.InternationalString;
+import org.opengis.geometry.Envelope;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.apache.sis.measure.Angle;
 import org.apache.sis.measure.Latitude;
 import org.apache.sis.measure.Longitude;
 import org.apache.sis.measure.AngleFormat;
+import org.apache.sis.coverage.SampleDimension;
 import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridRoundingMode;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.resources.Vocabulary;
 
 
 /**
  * Definition of filtering to apply for fetching a subset of {@link 
GridCoverageResource}.
  * This query allows requesting a subset of the coverage domain and the range.
  *
+ * <h2>Terminology</h2>
+ * This class uses relational database terminology for consistency with 
generic queries:
+ * <ul>
+ *   <li>A <cite>selection</cite> is a filter choosing the cells or pixels to 
include in the subset.
+ *       In this context, the selection is the <cite>coverage 
domain</cite>.</li>
+ *   <li>A <cite>projection</cite> (not to be confused with map projection) is 
the set of sample values to keep.
+ *       In this context, the projection is the <cite>coverage range</cite> 
(i.e. set of sample dimensions).</li>
+ * </ul>
+ *
  * <h2>Optional values</h2>
  * All aspects of this query are optional and initialized to "none".
  * Unless otherwise specified, all methods accept a null argument or can 
return a null value, which means "none".
@@ -51,15 +68,23 @@ public class CoverageQuery extends Query implements 
Cloneable, Serializable {
 
     /**
      * Desired grid extent and resolution, or {@code null} for reading the 
whole domain.
+     * This is the "selection" in query terminology.
      */
     private GridGeometry domain;
 
     /**
      * 0-based indices of sample dimensions to read, or {@code null} for 
reading them all.
+     * This is the "projection" (not to be confused with map projection) in 
query terminology.
      */
     private int[] range;
 
     /**
+     * The {@linkplain #range} specified by names instead than indices.
+     * At most one of {@code range} and {@code rangeNames} shall be non-null.
+     */
+    private String[] rangeNames;
+
+    /**
      * Number of additional cells to read on each border of the source grid 
coverage.
      * If non-zero, this property expands the {@linkplain #domain} to be read 
by an amount
      * specified in unit of cells of the image to be read. Those cells do not 
necessarily
@@ -74,26 +99,67 @@ public class CoverageQuery extends Query implements 
Cloneable, Serializable {
     }
 
     /**
-     * Sets the desired grid extent and resolution.
+     * Sets the approximate area of cells or pixels to include in the subset.
+     * This convenience method creates a grid geometry containing only the 
given envelope.
+     * Note that the given envelope is approximate:
+     * Coverages may expand the envelope to an integer amount of tiles.
+     *
+     * @param  domain  the approximate area of interest, or {@code null} if 
none.
+     */
+    @Override
+    public void setSelection(final Envelope domain) {
+        GridGeometry g = null;
+        if (domain != null) {
+            g = new GridGeometry(null, null, domain, GridRoundingMode.NEAREST);
+        }
+        setSelection(g);
+    }
+
+    /**
+     * Sets the desired grid extent and resolution. The given domain is 
approximate:
+     * Coverages may use a different resolution and expand the envelope to an 
integer amount of tiles.
      *
      * @param  domain  desired grid extent and resolution, or {@code null} for 
reading the whole domain.
      */
-    public void setDomain(final GridGeometry domain) {
+    public void setSelection(final GridGeometry domain) {
         this.domain = domain;
     }
 
     /**
      * Returns the desired grid extent and resolution.
-     * This is the value set by the last call to {@link 
#setDomain(GridGeometry)}.
+     * This is the value set by the last call to {@link 
#setSelection(GridGeometry)}.
+     *
+     * <div class="note"><b>Note on terminology:</b>
+     * "selection" is the generic term used in queries for designating a 
subset of feature instances.
+     * In a grid coverage, feature instances are cells or pixels.
+     * So this concept maps to the <cite>coverage domain</cite>.</div>
      *
      * @return desired grid extent and resolution, or {@code null} for reading 
the whole domain.
      */
-    public GridGeometry getDomain() {
+    public GridGeometry getSelection() {
         return domain;
     }
 
     /**
-     * Sets the indices of samples dimensions to read.
+     * Sets the sample dimensions to read by their names.
+     *
+     * @param  range  sample dimensions to retrieve, or {@code null} to 
retrieve all properties.
+     * @throws IllegalArgumentException if a sample dimension is duplicated.
+     */
+    @Override
+    @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
+    public void setProjection(String... range) {
+        if (range != null) {
+            range = range.clone();
+            ArgumentChecks.ensureNonEmpty("range", range);
+            // Assign only after we verified that the argument is valid.
+        }
+        rangeNames = range;
+        this.range = null;
+    }
+
+    /**
+     * Sets the indices of samples dimensions to read (the <cite>coverage 
range</cite>).
      * A {@code null} value means to read all sample dimensions (no filtering 
on range).
      * If non-null, then the {@code range} array shall contain at least one 
element,
      * all elements must be positive and no value can be duplicated.
@@ -102,28 +168,61 @@ public class CoverageQuery extends Query implements 
Cloneable, Serializable {
      * @throws IllegalArgumentException if the given array is empty or 
contains negative or duplicated values.
      */
     @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter")
-    public void setRange(int... range) {
+    public void setProjection(int... range) {
         if (range != null) {
             range = range.clone();
             ArgumentChecks.ensureNonEmpty("range", range, 0, 
Integer.MAX_VALUE, true);
             // Assign only after we verified that the argument is valid.
         }
         this.range = range;
+        rangeNames = null;
     }
 
     /**
      * Returns the indices of samples dimensions to read, or {@code null} if 
there is no filtering on range.
      * If non-null, the returned array shall never be empty.
      *
+     * <div class="note"><b>Note on terminology:</b>
+     * "projection" (not to be confused with map projection) is the generic 
term used in queries
+     * for designating a subset of feature properties retained in each feature 
instances.
+     * In a coverage, this concept maps to the <cite>coverage 
range</cite>.</div>
+     *
      * @return 0-based indices of sample dimensions to read, or {@code null} 
for reading them all.
      */
-    public int[] getRange() {
+    public int[] getProjection() {
         return (range != null) ? range.clone() : null;
     }
 
     /**
+     * Converts the sample dimension names to sample dimension indices.
+     * This conversion depends on the resource on which the query will be 
applied.
+     *
+     * @param  source  the resource for which to to the conversion.
+     */
+    private void namesToIndices(final GridCoverageResource source) throws 
DataStoreException {
+        if (rangeNames != null) {
+            final List<SampleDimension> sd = source.getSampleDimensions();
+            final int numBands = sd.size();
+            range = new int[rangeNames.length];
+next:       for (int i=0; i<rangeNames.length; i++) {
+                final String name = rangeNames[i];
+                for (int j=0; j<numBands; j++) {
+                    if (name.equals(sd.get(j).getName().toString())) {
+                        range[i] = j;
+                        continue next;
+                    }
+                }
+                InternationalString id = 
source.getIdentifier().map(GenericName::toInternationalString)
+                            .orElseGet(() -> 
Vocabulary.formatInternational(Vocabulary.Keys.Unnamed));
+                throw new 
UnsupportedQueryException(Errors.format(Errors.Keys.PropertyNotFound_2, id, 
name));
+            }
+            rangeNames = null;
+        }
+    }
+
+    /**
      * Sets a number of additional cells to read on each border of the source 
grid coverage.
-     * If non-zero, this property expands the {@linkplain #getDomain() domain} 
to be read
+     * If non-zero, this property expands the {@link #getSelection() domain} 
to be read
      * by the specified margin.
      *
      * <h4>Unit of measurement</h4>
@@ -133,7 +232,7 @@ public class CoverageQuery extends Query implements 
Cloneable, Serializable {
      * the full image to be read from the resource. Cells are counted after 
subsampling,
      * e.g. cells are twice bigger if a subsampling of 2 is applied.
      * Those cells do not necessarily have the same size than the cells
-     * of the {@linkplain #getDomain() domain of this query}.
+     * of the {@link #getSelection() domain of this query}.
      *
      * <h4>Use case</h4>
      * At reading time it may be necessary to add a margin to the coverage 
extent.
@@ -163,11 +262,13 @@ public class CoverageQuery extends Query implements 
Cloneable, Serializable {
      *
      * @param  source  the coverage resource to filter.
      * @return a view over the given coverage resource containing only the 
given domain and range.
-     * @throws UnsupportedQueryException if this query contains filtering 
options not yet supported.
+     * @throws DataStoreException if an error occurred during creation of the 
subset.
      */
-    final GridCoverageResource execute(final GridCoverageResource source) 
throws UnsupportedQueryException {
+    final GridCoverageResource execute(final GridCoverageResource source) 
throws DataStoreException {
         ArgumentChecks.ensureNonNull("source", source);
-        return new CoverageSubset(source, clone());
+        final CoverageQuery query = clone();
+        query.namesToIndices(source);
+        return new CoverageSubset(source, query);
     }
 
     /**
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java
index 4ff419f..b93ef03 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/CoverageSubset.java
@@ -54,6 +54,7 @@ final class CoverageSubset extends AbstractGridResource {
 
     /**
      * Creates a new coverage resource by filtering the given coverage using 
the given query.
+     * This given query is stored as-is (it is not cloned neither optimized).
      *
      * @param source  the coverage resource instances which provides the data.
      * @param query   the domain and range to read from the {@code source} 
coverage.
@@ -99,7 +100,7 @@ final class CoverageSubset extends AbstractGridResource {
     private GridGeometry clip(final GridGeometry domain, final 
GridRoundingMode rounding, final GridClippingMode clipping)
             throws DataStoreException
     {
-        final GridGeometry areaOfInterest = query.getDomain();
+        final GridGeometry areaOfInterest = query.getSelection();
         if (domain == null) return areaOfInterest;
         if (areaOfInterest == null) return domain;
         try {
@@ -135,7 +136,7 @@ final class CoverageSubset extends AbstractGridResource {
     @Override
     public List<SampleDimension> getSampleDimensions() throws 
DataStoreException {
         final List<SampleDimension> dimensions = source.getSampleDimensions();
-        final int[] range = query.getRange();
+        final int[] range = query.getProjection();
         if (range == null) {
             return dimensions;
         }
@@ -155,7 +156,7 @@ final class CoverageSubset extends AbstractGridResource {
      * Loads a subset of the grid coverage represented by this resource.
      * The domain to be read by the resource is computed as below:
      * <ul>
-     *   <li>If the query specifies a {@linkplain CoverageQuery#getDomain() 
domain},
+     *   <li>If the query specifies a {@link CoverageQuery#getSelection() 
domain},
      *       the given domain is intersected with the query domain.</li>
      *   <li>If the query specifies a {@linkplain 
CoverageQuery#getSourceDomainExpansion() domain expansion},
      *       the given domain is expanded by the amount specified in the 
query.</li>
@@ -176,7 +177,7 @@ final class CoverageSubset extends AbstractGridResource {
          * specified `domain` but inside the source domain, which may be 
larger.
          */
         domain = clip(domain, GridRoundingMode.ENCLOSING, 
GridClippingMode.BORDER_EXPANSION);
-        final int[] qr = query.getRange();
+        final int[] qr = query.getProjection();
         if (range == null) {
             range = qr;
         } else if (qr != null) {
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
index 927ec64..c2b4e6c 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
@@ -25,11 +25,14 @@ import java.io.Serializable;
 import javax.measure.Quantity;
 import javax.measure.quantity.Length;
 import org.opengis.util.GenericName;
+import org.opengis.geometry.Envelope;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
 import org.apache.sis.feature.builder.PropertyTypeBuilder;
+import org.apache.sis.internal.feature.AttributeConvention;
 import org.apache.sis.internal.feature.FeatureExpression;
 import org.apache.sis.internal.filter.SortByComparator;
 import org.apache.sis.internal.storage.Resources;
+import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.filter.Optimization;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Classes;
@@ -39,6 +42,7 @@ import org.apache.sis.util.iso.Names;
 // Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
+import org.opengis.filter.FilterFactory;
 import org.opengis.filter.Filter;
 import org.opengis.filter.Expression;
 import org.opengis.filter.Literal;
@@ -150,6 +154,29 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
     }
 
     /**
+     * Sets the properties to retrieve by their names. This convenience method 
wraps the
+     * given names in {@link ValueReference} expressions without alias and 
delegates to
+     * {@link #setProjection(NamedExpression...)}.
+     *
+     * @param  properties  properties to retrieve, or {@code null} to retrieve 
all properties.
+     * @throws IllegalArgumentException if a property is duplicated.
+     */
+    @Override
+    public void setProjection(final String... properties) {
+        NamedExpression[] wrappers = null;
+        if (properties != null) {
+            final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
+            wrappers = new NamedExpression[properties.length];
+            for (int i=0; i<wrappers.length; i++) {
+                final String p = properties[i];
+                ArgumentChecks.ensureNonNullElement("properties", i, p);
+                wrappers[i] = new NamedExpression(ff.property(p));
+            }
+        }
+        setProjection(wrappers);
+    }
+
+    /**
      * Sets the properties to retrieve, or {@code null} if all properties 
shall be included in the query.
      * This convenience method wraps the given expression in {@link 
NamedExpression}s without alias and
      * delegates to {@link #setProjection(NamedExpression...)}.
@@ -213,6 +240,23 @@ public class FeatureQuery extends Query implements 
Cloneable, Serializable {
     }
 
     /**
+     * Sets the approximate area of feature instances to include in the subset.
+     * This convenience method creates a filter that checks if the bounding box
+     * of the feature's {@code "sis:geometry"} property interacts with the 
given envelope.
+     *
+     * @param  domain  the approximate area of interest, or {@code null} if 
none.
+     */
+    @Override
+    public void setSelection(final Envelope domain) {
+        Filter<? super Feature> filter = null;
+        if (domain != null) {
+            final FilterFactory<Feature,Object,?> ff = 
DefaultFilterFactory.forFeatures();
+            filter = ff.bbox(ff.property(AttributeConvention.GEOMETRY), 
domain);
+        }
+        setSelection(filter);
+    }
+
+    /**
      * Sets a filter for trimming feature instances.
      * Features that do not pass the filter are discarded.
      * Discarded features are not counted for the {@linkplain #setLimit(long) 
query limit}.
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/storage/Query.java 
b/storage/sis-storage/src/main/java/org/apache/sis/storage/Query.java
index 2aa4b8f..4d58d2f 100644
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/Query.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/Query.java
@@ -16,39 +16,36 @@
  */
 package org.apache.sis.storage;
 
-import org.opengis.feature.Feature;
+import org.opengis.geometry.Envelope;
+
+// Branch-dependent imports
 import org.opengis.filter.QueryExpression;
 
 
 /**
  * Definition of filtering to apply for fetching a resource subset.
  * Filtering can be applied on {@link FeatureSet} or on {@link 
GridCoverageResource}.
- * When applied on {@link FeatureSet}, filtering can happen in two domains:
- *
- * <ol>
- *   <li>By filtering the {@link Feature} instances.</li>
- *   <li>By filtering the {@linkplain 
org.apache.sis.feature.DefaultFeatureType#getProperty properties}
- *       in each feature instance.</li>
- * </ol>
- *
- * Compared to Java functional interfaces, the first domain is equivalent to 
using
- * <code>{@linkplain java.util.function.Predicate}&lt;{@linkplain 
Feature}&gt;</code>
- * while the second domain is equivalent to using
- * <code>{@linkplain java.util.function.UnaryOperator}&lt;{@linkplain 
Feature}&gt;</code>.
+ * A query contains at least two parts:
  *
- * <div class="note"><b>Note:</b>
- * it is technically possible to use {@code Query} for performing more generic 
feature transformations,
- * for example inserting new properties computed from other properties, but 
such {@code Query} usages
- * should be rare since transformations (or more generic processing) are the 
topic of another package.
- * Queries are rather descriptive objects used by {@link FeatureSet} to 
optimize search operations
- * as much as possible on the resource, using for example caches and 
indexes.</div>
+ * <ul>
+ *   <li><b>Selection</b> for choosing the feature instances to fetch.
+ *     This is equivalent to choosing rows in a database table.</li>
+ *   <li><b>Projection</b> (not to be confused with map projection) for 
choosing the
+ *     {@linkplain org.apache.sis.feature.DefaultFeatureType#getProperty 
feature properties} or the
+ *     {@linkplain org.apache.sis.coverage.SampleDimension coverage sample 
dimensions} to fetch.
+ *     This is equivalent to choosing columns in a database table.</li>
+ * </ul>
  *
  * Compared to the SQL language, {@code Query} contains the information in the 
{@code SELECT} and
  * {@code WHERE} clauses of a SQL statement. A {@code Query} typically 
contains filtering capabilities
  * and (sometime) simple attribute transformations. Well known query languages 
include SQL and CQL.
  *
+ * <h2>Optional values</h2>
+ * All aspects of this query are optional and initialized to "none".
+ * Unless otherwise specified, all methods accept a null argument or can 
return a null value, which means "none".
+ *
  * @author Johann Sorel (Geomatys)
- * @version 1.0
+ * @version 1.1
  *
  * @see FeatureSet#subset(Query)
  * @see GridCoverageResource#subset(Query)
@@ -57,15 +54,40 @@ import org.opengis.filter.QueryExpression;
  * @module
  */
 public abstract class Query implements QueryExpression {
-    /*
-     * Current version does not yet contain any field. But some fields may be 
added in the future.
-     * For example some methods from 
org.apache.sis.internal.storage.query.FeatureQuery may move here.
-     * We use an abstract class instead than an interface for that reason.
-     */
-
     /**
      * Creates a new, initially empty, query.
      */
     protected Query() {
     }
+
+    /**
+     * Sets the approximate area of feature instances or pixels to include in 
the subset.
+     * For feature set, the domain is materialized by a {@link 
org.opengis.filter.Filter}.
+     * For grid coverage resource, the given envelope specifies the coverage 
domain.
+     *
+     * <p>The given envelope is approximate.
+     * Features may test intersections using only bounding boxes instead of 
full geometries.
+     * Coverages may expand the envelope to an integer amount of tiles.</p>
+     *
+     * @param  domain  the approximate area of interest, or {@code null} if 
none.
+     *
+     * @since 1.1
+     */
+    public abstract void setSelection(Envelope domain);
+
+    /**
+     * Sets the properties to retrieve by their names. For features, the 
arguments are names of
+     * {@linkplain org.apache.sis.feature.DefaultFeatureType#getProperty 
feature properties}.
+     * For coverages, the arguments are names of
+     * {@linkplain 
org.apache.sis.coverage.BandedCoverage#getSampleDimensions() sample dimensions}.
+     *
+     * <p><b>Note:</b> in this context, the "projection" word come from 
relational database terminology.
+     * It is unrelated to <cite>map projection</cite>.</p>
+     *
+     * @param  properties  properties to retrieve, or {@code null} to retrieve 
all properties.
+     * @throws IllegalArgumentException if a property is duplicated.
+     *
+     * @since 1.1
+     */
+    public abstract void setProjection(String... properties);
 }
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/CoverageQueryTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/CoverageQueryTest.java
index eef4d96..305908c 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/CoverageQueryTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/CoverageQueryTest.java
@@ -99,7 +99,7 @@ public final strictfp class CoverageQueryTest extends 
TestCase {
     public void testWithSubgrid() throws DataStoreException {
         final GridGeometry subGrid = createSubGrid(0);
         final CoverageQuery query = new CoverageQuery();
-        query.setDomain(subGrid);
+        query.setSelection(subGrid);
 
         final GridCoverageResource subset = resource.subset(query);
         assertEquals(subGrid, subset.getGridGeometry());
@@ -116,7 +116,7 @@ public final strictfp class CoverageQueryTest extends 
TestCase {
         final int expansion = 3;
         final GridGeometry subGrid = createSubGrid(0);
         final CoverageQuery query = new CoverageQuery();
-        query.setDomain(subGrid);
+        query.setSelection(subGrid);
         query.setSourceDomainExpansion(expansion);
 
         final GridCoverageResource subset = resource.subset(query);
@@ -125,6 +125,40 @@ public final strictfp class CoverageQueryTest extends 
TestCase {
     }
 
     /**
+     * Tests using only the methods defined in the {@link Query} base class.
+     *
+     * @throws DataStoreException if query execution failed.
+     */
+    @Test
+    public void testQueryMethods() throws DataStoreException {
+        final GridGeometry subGrid = createSubGrid(0);
+        final Query query = new CoverageQuery();
+        query.setSelection(subGrid.getEnvelope());
+        query.setProjection("0");
+
+        final GridCoverageResource subset = resource.subset(query);
+        assertEquals(subGrid, subset.getGridGeometry());
+        verifyRead(subset, 0);
+    }
+
+    /**
+     * Tests using an invalid sample dimension name.
+     *
+     * @throws DataStoreException if query execution failed.
+     */
+    @Test
+    public void testInvalidName() throws DataStoreException {
+        final Query query = new CoverageQuery();
+        query.setProjection("Apple");
+        try {
+            resource.subset(query);
+            fail("Expected UnsupportedQueryException.");
+        } catch (UnsupportedQueryException e) {
+            assertTrue(e.getMessage().contains("Apple"));
+        }
+    }
+
+    /**
      * Verifies that the read operation adds the expected margins.
      */
     private void verifyRead(final GridCoverageResource subset, final int 
expansion) throws DataStoreException {
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
index cbe7414..6918ac9 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
@@ -152,7 +152,7 @@ public final strictfp class FeatureQueryTest extends 
TestCase {
      * @throws DataStoreException if an error occurred while executing the 
query.
      */
     @Test
-    public void testFilter() throws DataStoreException {
+    public void testSelection() throws DataStoreException {
         final FilterFactory<Feature,?,?> factory = 
DefaultFilterFactory.forFeatures();
         query.setSelection(factory.equal(factory.property("value1", 
Integer.class),
                                       factory.literal(2), true, 
MatchAction.ALL));
@@ -165,7 +165,7 @@ public final strictfp class FeatureQueryTest extends 
TestCase {
      * @throws DataStoreException if an error occurred while executing the 
query.
      */
     @Test
-    public void testColumns() throws DataStoreException {
+    public void testProjection() throws DataStoreException {
         final FilterFactory<Feature,?,?> factory = 
DefaultFilterFactory.forFeatures();
         query.setProjection(new 
FeatureQuery.NamedExpression(factory.property("value1", Integer.class), 
(String) null),
                             new 
FeatureQuery.NamedExpression(factory.property("value1", Integer.class), 
"renamed1"),
@@ -194,4 +194,19 @@ public final strictfp class FeatureQueryTest extends 
TestCase {
         assertEquals(3, result.getPropertyValue("renamed1"));
         assertEquals("a literal", result.getPropertyValue("computed"));
     }
+
+    /**
+     * Verifies the effect of {@link FeatureQuery#setProjection(String[])}.
+     *
+     * @throws DataStoreException if an error occurred while executing the 
query.
+     */
+    @Test
+    public void testProjectionByNames() throws DataStoreException {
+        query.setProjection("value2");
+        query.setLimit(1);
+        final FeatureSet  fs = query.execute(featureSet);
+        final Feature result = 
TestUtilities.getSingleton(fs.features(false).collect(Collectors.toList()));
+        final PropertyType p = 
TestUtilities.getSingleton(result.getType().getProperties(true));
+        assertEquals("value2", p.getName().toString());
+    }
 }

Reply via email to