This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sis.git
commit 7ea6427d63023545787e8810a526bb7382ff3f1a Merge: e621d10 c6991bf Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sun Jan 2 02:33:49 2022 +0100 Merge branch 'geoapi-3.1' .../sis/gui/coverage/ImagePropertyExplorer.java | 54 +-- .../dataset/{CopyAction.java => PathAction.java} | 38 +- .../org/apache/sis/gui/dataset/ResourceTree.java | 8 +- .../org/apache/sis/gui/metadata/MetadataTree.java | 159 ++++++-- .../sis/gui/metadata/StandardMetadataTree.java | 73 ++-- .../apache/sis/internal/gui/ExceptionReporter.java | 3 +- .../sis/internal/gui/PropertyValueFormatter.java | 85 ++++ .../org/apache/sis/internal/gui/PropertyView.java | 55 ++- .../org/apache/sis/internal/gui/Resources.java | 15 + .../apache/sis/internal/gui/Resources.properties | 3 + .../sis/internal/gui/Resources_fr.properties | 3 + .../org/apache/sis/coverage/SampleDimension.java | 75 ++-- .../org/apache/sis/feature/DefaultFeatureType.java | 14 +- .../sis/feature/builder/FeatureTypeBuilder.java | 13 +- .../org/apache/sis/filter/ArithmeticFunction.java | 8 +- .../org/apache/sis/filter/AssociationValue.java | 239 +++++++++++ .../org/apache/sis/filter/ConvertFunction.java | 26 +- .../apache/sis/filter/DefaultFilterFactory.java | 4 +- .../java/org/apache/sis/filter/LeafExpression.java | 14 +- .../java/org/apache/sis/filter/Optimization.java | 2 +- .../java/org/apache/sis/filter/PropertyValue.java | 208 ++++++---- .../sis/internal/coverage/SampleDimensions.java | 41 -- .../sis/internal/coverage/j2d/Colorizer.java | 20 +- .../sis/internal/feature/FeatureExpression.java | 52 +-- .../apache/sis/internal/feature/package-info.java | 2 +- .../sis/internal/filter/GeometryConverter.java | 8 +- .../sis/internal/filter/sqlmm/SpatialFunction.java | 25 +- .../sis/internal/filter/sqlmm/package-info.java | 2 +- .../org/apache/sis/filter/LogicalFilterTest.java | 4 +- .../java/org/apache/sis/filter/PeriodLiteral.java | 2 +- .../org/apache/sis/util/iso/DefaultLocalName.java | 8 +- .../org/apache/sis/util/iso/DefaultMemberName.java | 2 +- .../apache/sis/util/iso/DefaultNameFactory.java | 4 +- .../org/apache/sis/util/iso/DefaultScopedName.java | 4 +- .../org/apache/sis/util/iso/DefaultTypeName.java | 4 +- .../java/org/apache/sis/util/iso/TypeNames.java | 2 +- .../apache/sis/metadata/TreeTableFormatTest.java | 2 +- .../org/apache/sis/metadata/TreeTableViewTest.java | 2 +- .../extent/DefaultGeographicBoundingBoxTest.java | 5 +- .../DefaultDataIdentificationTest.java | 2 +- .../sis/internal/map/coverage/RenderingData.java | 17 +- .../internal/referencing/WraparoundApplicator.java | 57 +-- .../sis/internal/referencing/package-info.java | 2 +- .../apache/sis/internal/util/PropertyFormat.java | 37 +- .../java/org/apache/sis/internal/util/Strings.java | 28 +- .../java/org/apache/sis/internal/util/XPaths.java | 58 ++- .../org/apache/sis/internal/util/XPointer.java | 113 ++++++ .../org/apache/sis/internal/util/package-info.java | 2 +- .../java/org/apache/sis/measure/UnitFormat.java | 3 +- .../sis/util/collection/TreeTableFormat.java | 2 +- .../java/org/apache/sis/util/resources/Errors.java | 5 + .../apache/sis/util/resources/Errors.properties | 1 + .../apache/sis/util/resources/Errors_fr.properties | 1 + .../org/apache/sis/util/resources/Vocabulary.java | 15 + .../sis/util/resources/Vocabulary.properties | 3 + .../sis/util/resources/Vocabulary_fr.properties | 3 + .../org/apache/sis/internal/util/XPathsTest.java | 16 +- .../util/{XPathsTest.java => XPointerTest.java} | 36 +- .../java/org/apache/sis/measure/UnitsTest.java | 2 +- .../apache/sis/test/suite/UtilityTestSuite.java | 3 +- .../java/org/apache/sis/storage/landsat/Band.java | 2 +- .../sis/internal/geotiff/SchemaModifier.java | 16 +- .../sis/internal/storage/inflater/CCITTRLE.java | 8 +- .../storage/inflater/CompressionChannel.java | 6 +- .../storage/inflater/HorizontalPredictor.java | 443 +++++++++++---------- .../sis/internal/storage/inflater/Inflater.java | 12 +- .../apache/sis/internal/storage/inflater/LZW.java | 8 +- .../sis/internal/storage/inflater/PackBits.java | 8 +- .../internal/storage/inflater/PixelChannel.java | 4 +- .../storage/inflater/PredictorChannel.java | 11 +- .../apache/sis/internal/storage/inflater/ZIP.java | 8 +- .../internal/storage/inflater/package-info.java | 2 +- .../apache/sis/storage/geotiff/GeoTiffStore.java | 2 +- .../sis/storage/geotiff/ImageFileDirectory.java | 9 +- .../apache/sis/storage/geotiff/NativeMetadata.java | 36 +- .../java/org/apache/sis/storage/geotiff/Tags.java | 14 +- .../apache/sis/storage/geotiff/XMLMetadata.java | 289 ++++++++++++++ .../internal/storage/inflater/CCITTRLETest.java | 2 +- .../org/apache/sis/internal/netcdf/CRSBuilder.java | 47 ++- .../sis/internal/sql/postgis/RasterReader.java | 6 +- .../org/apache/sis/storage/sql/SQLStoreTest.java | 4 +- .../sis/internal/storage/TiledGridCoverage.java | 27 +- .../sis/internal/storage/TiledGridResource.java | 1 + .../sis/internal/storage/io/IOUtilities.java | 23 +- .../java/org/apache/sis/storage/FeatureQuery.java | 129 ++++-- .../org/apache/sis/storage/FeatureQueryTest.java | 177 ++++++-- 86 files changed, 2165 insertions(+), 823 deletions(-) diff --cc core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java index 54c1ef3,3d0af60..4dc9e91 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java @@@ -77,7 -77,10 +77,7 @@@ import org.apache.sis.util.Debug * * @author Martin Desruisseaux (IRD, Geomatys) * @author Alexis Manin (Geomatys) - * @version 1.1 + * @version 1.2 - * - * @see org.opengis.metadata.content.SampleDimension - * * @since 1.0 * @module */ diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java index fcc86ee,e2ebc82..882be37 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java @@@ -293,9 -293,9 +293,9 @@@ public class DefaultFeatureType extend } /* * Before to resolve cyclic associations, verify that operations depend only on existing properties. - * Note: the 'allProperties' collection has been created by computeTransientFields(…) above. + * Note: the `allProperties` collection has been created by computeTransientFields(…) above. */ - for (final PropertyType property : allProperties) { + for (final AbstractIdentifiedType property : allProperties) { if (property instanceof AbstractOperation) { for (final String dependency : ((AbstractOperation) property).getDependencies()) { if (!byName.containsKey(dependency)) { @@@ -392,10 -392,10 +392,10 @@@ * name for convenience, provided that it does not create ambiguity. If a short alias could map to two or * more properties, then that alias is not added. * - * In the 'aliases' map below, null values will be assigned to ambiguous short names. + * In the `aliases` map below, null values will be assigned to ambiguous short names. */ - final Map<String, PropertyType> aliases = new LinkedHashMap<>(); - for (final PropertyType property : allProperties) { + final Map<String, AbstractIdentifiedType> aliases = new LinkedHashMap<>(); + for (final AbstractIdentifiedType property : allProperties) { GenericName name = property.getName(); while (name instanceof ScopedName) { if (name == (name = ((ScopedName) name).tail())) break; // Safety against broken implementations. @@@ -527,12 -521,19 +527,14 @@@ * @param previous previous results, for avoiding never ending loop. * @return {@code true} if all names have been resolved. */ - private boolean resolve(final FeatureType feature, final Map<FeatureType,Boolean> previous) { + private boolean resolve(final DefaultFeatureType feature, final Map<FeatureType,Boolean> previous) { /* * The isResolved field is used only as a cache for skipping completely the DefaultFeatureType instance if - * we have determined that there is no unresolved name. + * we have determined that there is no unresolved name. If the given argument is not a DefaultFeatureType + * instance, conservatively assumes `isSimple`. It may cause more calculation than needed, but should not + * change the result. */ - if (feature instanceof DefaultFeatureType) { - final DefaultFeatureType dt = (DefaultFeatureType) feature; - return dt.isResolved = resolve(dt, dt.properties, previous, dt.isResolved); - } else { - return resolve(feature, feature.getProperties(false), previous, feature.isSimple()); - } + return feature.isResolved = resolve(feature, feature.properties, previous, feature.isResolved); } /** diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java index de92b37,3ab4bdc..41bc198 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java @@@ -697,11 -692,9 +697,13 @@@ public class FeatureTypeBuilder extend /** * Creates a new {@code AttributeType} builder initialized to the same characteristics than the given template. + * If the new attribute duplicates an existing one (for example if the same template is used many times), + * caller should use the returned builder for modifying some attributes. * + * <div class="warning"><b>Warning:</b> + * The {@code template} argument type will be changed to {@code AttributeType} if and when such interface + * will be defined in GeoAPI.</div> + * * @param <V> the compile-time type of values in the {@code template} argument. * @param template an existing attribute type to use as a template. * @return a builder for an {@code AttributeType}, initialized with the values of the given template. @@@ -808,12 -797,10 +810,14 @@@ /** * Creates a new {@code FeatureAssociationRole} builder initialized to the same characteristics - * than the given template. + * than the given template. If the new association duplicates an existing one (for example if the + * same template is used many times), caller should use the returned builder for modifying some + * associations. * + * <div class="warning"><b>Warning:</b> + * The {@code template} argument type will be changed to {@code FeatureAssociationRole} if and when such interface + * will be defined in GeoAPI.</div> + * * @param template an existing feature association to use as a template. * @return a builder for an {@code FeatureAssociationRole}, initialized with the values of the given template. * @@@ -830,18 -817,20 +834,23 @@@ /** * Adds the given property in the feature type properties. * The given property shall be an instance of one of the following types: + * * <ul> - * <li>{@link AttributeType}, in which case this method delegate to {@link #addAttribute(AttributeType)}.</li> - * <li>{@link FeatureAssociationRole}, in which case this method delegate to {@link #addAssociation(FeatureAssociationRole)}.</li> - * <li>{@link Operation}, in which case the given operation object will be added verbatim in the {@code FeatureType}; + * <li>{@code AttributeType}, in which case this method delegate to {@code addAttribute(AttributeType)}.</li> + * <li>{@code FeatureAssociationRole}, in which case this method delegate to {@code addAssociation(FeatureAssociationRole)}.</li> + * <li>{@code Operation}, in which case the given operation object will be added verbatim in the {@code FeatureType}; * this builder does not create new operations.</li> * </ul> * + * This method does not verify if the given property duplicates an existing property. + * If the same template is used many times, then the caller should use the returned builder + * for modifying some properties. + * + * <div class="warning"><b>Warning:</b> In a future SIS version, the argument type may be changed to the + * {@code org.opengis.feature.PropertyType} interface. This change is pending GeoAPI revision.</div> + * * @param template the property to add to the feature type. - * @return a builder initialized to the given builder. + * @return a builder initialized to the given template. * In the {@code Operation} case, the builder is a read-only accessor on the operation properties. * * @see #properties() diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java index 0000000,a46b3bb..30c1a7a mode 000000,100644..100644 --- a/core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java @@@ -1,0 -1,234 +1,239 @@@ + /* + * 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.filter; + + import java.util.Arrays; + import java.util.List; + import java.util.Collection; + import java.util.Collections; + import java.util.Optional; + import java.util.StringJoiner; + import org.apache.sis.feature.Features; + import org.apache.sis.feature.builder.FeatureTypeBuilder; + import org.apache.sis.feature.builder.PropertyTypeBuilder; + + // Branch-dependent imports -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; ++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.internal.geoapi.filter.Name; ++import org.apache.sis.internal.geoapi.filter.ValueReference; + + + /** + * Expression whose value is computed by retrieving the value indicated by the provided path. + * This is used for value reference given by x-path such as "a/b/c". The last element of the path + * (the tip) is evaluated by a {@link PropertyValue}. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * + * @param <V> the type of value computed by the expression. + * + * @see PropertyValue + * + * @since 1.2 + * @module + */ -final class AssociationValue<V> extends LeafExpression<Feature, V> - implements ValueReference<Feature, V>, Optimization.OnExpression<Feature, V> ++final class AssociationValue<V> extends LeafExpression<AbstractFeature, V> ++ implements ValueReference<AbstractFeature, V>, Optimization.OnExpression<AbstractFeature, V> + { + /** + * Path to the property from which to retrieve the value. - * Each element in the array is an argument to give in a call to {@link Feature#getProperty(String)}. ++ * Each element in the array is an argument to give in a call to {@code Feature.getProperty(String)}. + * This array should be considered read-only because it may be shared. + */ + private final String[] path; + + /** + * Expression to use for evaluating the property value after the last element of the path. + */ + private final PropertyValue<V> accessor; + + /** + * Creates a new expression retrieving values from a property at the given path. + * + * @param path components of the path before the property evaluated by {@code accessor}. + * @param accessor expression to use for evaluating the property value after the last element of the path. + */ + AssociationValue(final List<String> path, final PropertyValue<V> accessor) { + this.path = path.toArray(new String[path.size()]); + this.accessor = accessor; + } + + /** + * Creates a new expression retrieving values from a property at the given path. + * This constructor is used for creating new expression with the same path than + * a previous expression but a different accessor. + * + * @param path components of the path, not cloned (we share arrays). + * @param accessor expression to use for evaluating the property value after the last element of the path. + */ + private AssociationValue(final String[] path, final PropertyValue<V> accessor) { + this.path = path; + this.accessor = accessor; + } + ++ @Override ++ public final ScopedName getFunctionName() { ++ return Name.VALUE_REFERENCE; ++ } ++ + /** + * For {@link #toString()} implementation. + */ + @Override + protected final Collection<?> getChildren() { + return Collections.singleton(getXPath()); + } + + /** + * Returns the name of the property whose value will be returned by the {@link #apply(Object)} method. + */ + @Override + public final String getXPath() { + final StringJoiner sb = new StringJoiner("/", accessor.isVirtual ? PropertyValue.VIRTUAL_PREFIX : "", ""); + for (final String p : path) sb.add(p); + return sb.add(accessor.name).toString(); + } + + /** + * Returns the value of the property at the path given at construction time. + * Path components should be feature associations. If this is not the case, + * this method silently returns {@code null}. + * + * @param feature the feature from which to get a value, or {@code null}. + * @return value for the property identified by the XPath (may be {@code null}). + */ + @Override - public V apply(Feature instance) { ++ public V apply(AbstractFeature instance) { + walk: if (instance != null) { + for (final String p : path) { + final Object value = instance.getPropertyValue(p); - if (!(value instanceof Feature)) break walk; - instance = (Feature) value; ++ if (!(value instanceof AbstractFeature)) break walk; ++ instance = (AbstractFeature) value; + } + return accessor.apply(instance); + } + return null; + } + + /** + * If at least one evaluated property is a link, replaces this expression by more direct references + * to the target properties. This is needed for better SQL WHERE clause in database queries. + */ + @Override - public Expression<Feature, V> optimize(final Optimization optimization) { - final FeatureType specifiedType = optimization.getFeatureType(); ++ public Expression<AbstractFeature, V> optimize(final Optimization optimization) { ++ final DefaultFeatureType specifiedType = optimization.getFeatureType(); + walk: if (specifiedType != null) try { - FeatureType type = specifiedType; ++ DefaultFeatureType type = specifiedType; + String[] direct = path; // To be cloned before any modification. + for (int i=0; i<path.length; i++) { - PropertyType property = type.getProperty(path[i]); ++ AbstractIdentifiedType property = type.getProperty(path[i]); + Optional<String> link = Features.getLinkTarget(property); + if (link.isPresent()) { + if (direct == path) direct = direct.clone(); + property = type.getProperty(direct[i] = link.get()); + } - if (!(property instanceof FeatureAssociationRole)) break walk; - type = ((FeatureAssociationRole) property).getValueType(); ++ if (!(property instanceof DefaultAssociationRole)) break walk; ++ type = ((DefaultAssociationRole) property).getValueType(); + } + /* + * At this point all links have been resolved, up to the final property to evaluate. + * Delegate the final property optimization to `accessor` which may not only resolve + * links but also tune the `ObjectConverter`. + */ + final PropertyValue<V> converted; + optimization.setFeatureType(type); + try { + converted = accessor.optimize(optimization); + } finally { + optimization.setFeatureType(specifiedType); + } + if (converted != accessor || direct != path) { + return new AssociationValue<>(direct, converted); + } - } catch (PropertyNotFoundException e) { ++ } catch (IllegalArgumentException e) { + warning(e, true); + } + return this; + } + + /** + * Returns an expression that provides values as instances of the specified class. + */ + @Override + @SuppressWarnings("unchecked") - public final <N> Expression<Feature,N> toValueType(final Class<N> target) { ++ public final <N> Expression<AbstractFeature,N> toValueType(final Class<N> target) { + final PropertyValue<N> converted = accessor.toValueType(target); + if (converted == accessor) { + return (AssociationValue<N>) this; + } + return new AssociationValue<>(path, converted); + } + + /** + * Provides the expected type of values produced by this expression when a feature of the given type is evaluated. + * + * @param valueType the type of features to be evaluated by the given expression. + * @param addTo where to add the type of properties evaluated by the given expression. + * @return builder of the added property, or {@code null} if this method can not add a property. + * @throws IllegalArgumentException if this method can not determine the property type for the given feature type. + */ + @Override - public PropertyTypeBuilder expectedType(FeatureType valueType, final FeatureTypeBuilder addTo) { ++ public PropertyTypeBuilder expectedType(DefaultFeatureType valueType, final FeatureTypeBuilder addTo) { + for (final String p : path) { - final PropertyType type; ++ final AbstractIdentifiedType type; + try { + type = valueType.getProperty(p); - } catch (PropertyNotFoundException e) { ++ } catch (IllegalArgumentException e) { + if (accessor.isVirtual) { + // The association does not exist but may be defined on a yet unknown child type. + return accessor.expectedType(addTo); + } + throw e; + } - if (!(type instanceof FeatureAssociationRole)) { ++ if (!(type instanceof DefaultAssociationRole)) { + return null; + } - valueType = ((FeatureAssociationRole) type).getValueType(); ++ valueType = ((DefaultAssociationRole) type).getValueType(); + } + return accessor.expectedType(valueType, addTo); + } + + /** + * Returns a hash code value for this association. + */ + @Override + public int hashCode() { + return Arrays.hashCode(path) + accessor.hashCode(); + } + + /** + * Compares this value reference with the given object for equality. + */ + @Override + public boolean equals(final Object obj) { + if (obj instanceof AssociationValue<?>) { + final AssociationValue<?> other = (AssociationValue<?>) obj; + return Arrays.equals(path, other.path) && accessor.equals(other.accessor); + } + return false; + } + } diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/ConvertFunction.java index bfb8017,90adf3a..948f813 --- a/core/sis-feature/src/main/java/org/apache/sis/filter/ConvertFunction.java +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/ConvertFunction.java @@@ -36,9 -37,9 +36,9 @@@ import org.apache.sis.feature.DefaultFe * Expression whose results are converted to a different type. * * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * - * @param <R> the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs. + * @param <R> the type of resources (e.g. {@code Feature}) used as inputs. * @param <S> the type of value computed by the wrapped exception. This is the type to convert. * @param <V> the type of value computed by this expression. This is the type after conversion. * @@@ -156,12 -157,16 +156,16 @@@ final class ConvertFunction<R,S,V> exte } /** - * Provides the expected type of values produced by this expression - * when a feature of the given type is evaluated. + * Provides the type of values produced by this expression when a feature of the given type is evaluated. + * May return {@code null} if the type can not be determined. */ @Override - public PropertyTypeBuilder expectedType(final FeatureType valueType, final FeatureTypeBuilder addTo) { + public PropertyTypeBuilder expectedType(final DefaultFeatureType valueType, final FeatureTypeBuilder addTo) { - final PropertyTypeBuilder p = FeatureExpression.expectedType(expression, valueType, addTo); + final FeatureExpression<?,?> fex = FeatureExpression.castOrCopy(expression); + if (fex == null) { + return null; + } + final PropertyTypeBuilder p = fex.expectedType(valueType, addTo); if (p instanceof AttributeTypeBuilder<?>) { return ((AttributeTypeBuilder<?>) p).setValueClass(getValueClass()); } diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java index feffd85,158c2f9..7feb03e --- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java @@@ -55,9 -48,9 +55,9 @@@ import org.apache.sis.internal.geoapi.f * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * - * @param <R> the type of resources (e.g. {@link org.opengis.feature.Feature}) to use as inputs. + * @param <R> the type of resources (e.g. {@link AbstractFeature}) to use as inputs. * @param <G> base class of geometry objects. The implementation-neutral type is GeoAPI {@link Geometry}, * but this factory allows the use of other implementations such as JTS * {@link org.locationtech.jts.geom.Geometry} or ESRI {@link com.esri.core.geometry.Geometry}. @@@ -199,7 -226,9 +199,9 @@@ public abstract class DefaultFilterFact * @return an expression evaluating the referenced property value. */ @Override - public <V> ValueReference<Feature,V> property(final String xpath, final Class<V> type) { + public <V> Expression<AbstractFeature,V> property(final String xpath, final Class<V> type) { + ArgumentChecks.ensureNonEmpty("xpath", xpath); + ArgumentChecks.ensureNonNull ("type", type); return PropertyValue.create(xpath, type); } } diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java index 057e829,80e5bc7..4a238a9 --- a/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java @@@ -27,16 -28,18 +28,18 @@@ import org.apache.sis.util.Unconvertibl import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.feature.builder.PropertyTypeBuilder; import org.apache.sis.feature.builder.AttributeTypeBuilder; + import org.apache.sis.util.resources.Errors; + import org.apache.sis.internal.util.XPaths; // Branch-dependent imports -import org.opengis.feature.Feature; -import org.opengis.feature.FeatureType; -import org.opengis.feature.PropertyType; -import org.opengis.feature.AttributeType; -import org.opengis.feature.IdentifiedType; -import org.opengis.feature.Operation; -import org.opengis.feature.PropertyNotFoundException; -import org.opengis.filter.ValueReference; +import org.opengis.util.ScopedName; +import org.apache.sis.feature.AbstractFeature; +import org.apache.sis.feature.AbstractIdentifiedType; +import org.apache.sis.feature.AbstractOperation; +import org.apache.sis.feature.DefaultAttributeType; +import org.apache.sis.feature.DefaultFeatureType; +import org.apache.sis.internal.geoapi.filter.Name; +import org.apache.sis.internal.geoapi.filter.ValueReference; /** @@@ -53,7 -58,9 +58,9 @@@ * @since 1.1 * @module */ - abstract class PropertyValue<V> extends LeafExpression<AbstractFeature,V> implements ValueReference<AbstractFeature,V> { -abstract class PropertyValue<V> extends LeafExpression<Feature,V> - implements ValueReference<Feature,V>, Optimization.OnExpression<Feature,V> ++abstract class PropertyValue<V> extends LeafExpression<AbstractFeature,V> ++ implements ValueReference<AbstractFeature,V>, Optimization.OnExpression<AbstractFeature,V> + { /** * For cross-version compatibility. */ @@@ -65,34 -73,68 +73,73 @@@ protected final String name; /** + * Whether the property to fetch is considered virtual (a property that may be defined only in sub-types). + * If {@code true}, then {@link #expectedType(FeatureType, FeatureTypeBuilder)} will not throw an exception + * if the property is not found. + */ + protected final boolean isVirtual; + + /** + * The prefix in a x-path for considering a property as virual. + */ + static final String VIRTUAL_PREFIX = "/*/"; + + /** * Creates a new expression retrieving values from a property of the given name. */ - protected PropertyValue(final String name) { - ArgumentChecks.ensureNonNull("name", name); + protected PropertyValue(final String name, final boolean isVirtual) { this.name = name; + this.isVirtual = isVirtual; } + @Override + public final ScopedName getFunctionName() { + return Name.VALUE_REFERENCE; + } + /** - * Creates a new expression retrieving values from a property of the given name. + * Creates a new expression retrieving values from a property of the given path. + * Simple path expressions of the form "a/b/c" can be used. * - * @param <V> compile-time value of {@code type}. - * @param name the name of the property to fetch. - * @param type the desired type for the expression result. + * @param <V> compile-time value of {@code type}. + * @param xpath path (usually a single name) of the property to fetch. + * @param type the desired type for the expression result. * @return expression retrieving values from a property of the given name. + * @throws IllegalArgumentException if the given XPath is not supported. */ @SuppressWarnings("unchecked") - static <V> PropertyValue<V> create(final String name, final Class<V> type) { - ArgumentChecks.ensureNonNull("type", type); - if (type == Object.class) { - return (PropertyValue<V>) new AsObject(name); - static <V> ValueReference<Feature,V> create(String xpath, final Class<V> type) { ++ static <V> ValueReference<AbstractFeature,V> create(String xpath, final Class<V> type) { + boolean isVirtual = false; + List<String> path = XPaths.split(xpath); + split: if (path != null) { + /* + * If the XPath is like "/∗/property" where the root "/" is the feature instance, + * we interpret that as meaning "property of a feature of any type", which means + * to relax the restriction about the set of allowed properties. + */ + final String head = path.get(0); // List and items in the list are guaranteed non-empty. + isVirtual = head.equals("/*"); + if (isVirtual || head.charAt(0) != XPaths.SEPARATOR) { + final int offset = isVirtual ? 1 : 0; // Skip the "/*/" component at index 0. + final int last = path.size() - 1; + if (last >= offset) { + xpath = path.get(last); + path = path.subList(offset, last); + break split; // Accept the path as valid. + } + } + throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedXPath_1, xpath)); + } + /* + * At this point, `xpath` is the tip of the path (i.e. prefixes have been removed). + */ + final PropertyValue<V> tip; + if (type != Object.class) { + tip = new Converted<>(type, xpath, isVirtual); } else { - return new Converted<>(type, name); + tip = (PropertyValue<V>) new AsObject(xpath, isVirtual); } + return (path == null || path.isEmpty()) ? tip : new AssociationValue<>(path, tip); } /** @@@ -112,8 -154,10 +159,10 @@@ } /** - * Returns the type of values fetched from {@link Feature} instance. + * Returns the type of values fetched from {@link AbstractFeature} instance. * This is the type before conversion to the {@linkplain #getValueClass() target type}. + * The type is always {@link Object} on newly created expression because the type of feature property + * values is unknown, but may become a specialized type after {@link Optimization} has been applied. */ protected Class<?> getSourceClass() { return Object.class; @@@ -177,11 -237,12 +242,12 @@@ * using or not the database index. */ @Override - public Expression<AbstractFeature,?> optimize(final Optimization optimization) { + public PropertyValue<Object> optimize(final Optimization optimization) { - final FeatureType type = optimization.getFeatureType(); + final DefaultFeatureType type = optimization.getFeatureType(); if (type != null) try { - return Features.getLinkTarget(type.getProperty(name)).map(AsObject::new).orElse(this); + return Features.getLinkTarget(type.getProperty(name)) + .map((rename) -> new AsObject(rename, isVirtual)).orElse(this); - } catch (PropertyNotFoundException e) { + } catch (IllegalArgumentException e) { warning(e, true); } return this; @@@ -252,29 -305,42 +310,42 @@@ * then a specialized expression is returned. Otherwise this method returns {@code this}. */ @Override - public final Expression<AbstractFeature, ? extends V> optimize(final Optimization optimization) { + public final PropertyValue<V> optimize(final Optimization optimization) { - final FeatureType featureType = optimization.getFeatureType(); + final DefaultFeatureType featureType = optimization.getFeatureType(); if (featureType != null) try { - String targetName = name; - AbstractIdentifiedType property = featureType.getProperty(targetName); + /* + * Resolve link (e.g. "sis:identifier" as a reference to the real identifier property). + * This is important for allowing `SQLStore` to use the property in SQL WHERE statements. + * If there is no renaming to apply (which is the usual case), then `rename` is null. + */ + String rename = name; - PropertyType property = featureType.getProperty(rename); ++ AbstractIdentifiedType property = featureType.getProperty(rename); Optional<String> target = Features.getLinkTarget(property); if (target.isPresent()) try { - targetName = target.get(); - property = featureType.getProperty(targetName); + rename = target.get(); + property = featureType.getProperty(rename); - } catch (PropertyNotFoundException e) { + } catch (IllegalArgumentException e) { - targetName = name; warning(e, true); + rename = name; } + /* + * At this point we did our best effort for having the property as an attribute, + * which allows us to get the expected type. If the type is not `Object`, we can + * try to fetch a more specific converter than the default `Converted` one. + */ + Class<?> source = getSourceClass(); + final Class<?> original = source; - if (property instanceof AttributeType<?>) { - source = ((AttributeType<?>) property).getValueClass(); + if (property instanceof DefaultAttributeType<?>) { - final Class<?> source = ((DefaultAttributeType<?>) property).getValueClass(); - if (source != null && source != Object.class && !source.isAssignableFrom(getSourceClass())) { - return new CastedAndConverted<>(source, type, targetName); - } ++ source = ((DefaultAttributeType<?>) property).getValueClass(); } - if (!targetName.equals(name)) { - return rename(targetName); + if (!(rename.equals(name) && source.equals(original))) { + if (source == Object.class) { + return new Converted<>(type, rename, isVirtual); + } else { + return new CastedAndConverted<>(source, type, rename, isVirtual); + } } - } catch (PropertyNotFoundException e) { + } catch (IllegalArgumentException e) { warning(e, true); } return this; @@@ -306,14 -372,23 +377,23 @@@ * @throws IllegalArgumentException if this method can not determine the property type for the given feature type. */ @Override - public PropertyTypeBuilder expectedType(final FeatureType valueType, final FeatureTypeBuilder addTo) { - PropertyType type; + public PropertyTypeBuilder expectedType(final DefaultFeatureType valueType, final FeatureTypeBuilder addTo) { - AbstractIdentifiedType type = valueType.getProperty(name); // May throw IllegalArgumentException. ++ AbstractIdentifiedType type; + try { + type = valueType.getProperty(name); - } catch (PropertyNotFoundException e) { ++ } catch (IllegalArgumentException e) { + if (isVirtual) { + // The property does not exist but may be defined on a yet unknown child type. + return expectedType(addTo); + } + throw e; + } - while (type instanceof Operation) { - final IdentifiedType result = ((Operation) type).getResult(); - if (result != type && result instanceof PropertyType) { - type = (PropertyType) result; - } else if (result instanceof FeatureType) { - return addTo.addAssociation((FeatureType) result).setName(name); + while (type instanceof AbstractOperation) { + final AbstractIdentifiedType result = ((AbstractOperation) type).getResult(); - if (result != type) { - type = result; ++ if (result != type && result instanceof AbstractIdentifiedType) { ++ type = (AbstractIdentifiedType) result; + } else if (result instanceof DefaultFeatureType) { + return addTo.addAssociation((DefaultFeatureType) result).setName(name); } else { return null; } @@@ -346,24 -421,8 +426,8 @@@ converter = ObjectConverters.find(source, type); } - /** Creates a new expression derived from an existing one except for the target name. */ - private CastedAndConverted(final CastedAndConverted<S,V> other, final String name) { - super(other.type, name); - source = other.source; - converter = other.converter; - } - - /** - * Creates a new {@code CastedAndConverted} fetching values for a property of different name. - * The given name should be the target of a link that the caller has resolved. - */ - @Override - protected Converted<V> rename(final String target) { - return new CastedAndConverted<>(this, target); - } - /** - * Returns the type of values fetched from {@link Feature} instance. + * Returns the type of values fetched from {@link AbstractFeature} instance. */ @Override protected Class<S> getSourceClass() { diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java index 7633ee4,f1572b8..6a1ba54 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java @@@ -16,13 -16,15 +16,14 @@@ */ package org.apache.sis.internal.feature; - import org.apache.sis.internal.util.CollectionsExt; -import org.opengis.feature.FeatureType; -import org.opengis.feature.AttributeType; -import org.opengis.filter.Literal; -import org.opengis.filter.Expression; -import org.opengis.filter.ValueReference; ++import org.apache.sis.filter.Expression; + import org.apache.sis.filter.Optimization; + import org.apache.sis.filter.DefaultFilterFactory; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.feature.builder.PropertyTypeBuilder; - +import org.apache.sis.feature.DefaultFeatureType; - import org.apache.sis.feature.AbstractIdentifiedType; - import org.apache.sis.filter.Expression; ++import org.apache.sis.internal.geoapi.filter.Literal; ++import org.apache.sis.internal.geoapi.filter.ValueReference; /** @@@ -34,9 -36,9 +35,9 @@@ * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * - * @param <R> the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs. + * @param <R> the type of resources (e.g. {@code Feature}) used as inputs. * @param <V> the type of values computed by the expression. * * @since 1.0 @@@ -64,20 -66,19 +65,19 @@@ public interface FeatureExpression<R,V * @throws IllegalArgumentException if this method can operate only on some feature types * and the given type is not one of them. */ - PropertyTypeBuilder expectedType(FeatureType valueType, FeatureTypeBuilder addTo); + PropertyTypeBuilder expectedType(DefaultFeatureType valueType, FeatureTypeBuilder addTo); /** - * Provides the type of results computed by the given expression. - * This method executes the first of the following choices that apply: + * Tries to cast or convert the given expression to a {@link FeatureExpression}. + * If the given expression can not be casted, then this method creates a copy + * provided that the expression is one of the following type: * * <ol> - * <li>If the expression implements {@link FeatureExpression}, delegate to {@link #expectedType(FeatureType, - * FeatureTypeBuilder)}. Note that the invoked method may throw an {@link IllegalArgumentException}.</li> - * <li>Otherwise if the given feature type contains exactly one property (including inherited properties), - * adds that property to the given builder.</li> - * <li>Otherwise returns {@code null}.</li> + * <li>{@link Literal}.</li> + * <li>{@link ValueReference}, assuming that the expression expects feature instances.</li> * </ol> * + * Otherwise this method returns {@code null}. * It is caller's responsibility to verify if this method returns {@code null} and to throw an exception in such case. * We leave that responsibility to the caller because (s)he may be able to provide better error messages. * diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/SpatialFunction.java index 582a18f,d250e5c..b9af211 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/SpatialFunction.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/SpatialFunction.java @@@ -41,9 -42,9 +41,9 @@@ import org.apache.sis.filter.Expression * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.2 * - * @param <R> the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs. + * @param <R> the type of resources (e.g. {@code Feature}) used as inputs. * * @since 1.1 * @module @@@ -206,19 -207,22 +206,22 @@@ abstract class SpatialFunction<R> exten * It may be because that expression is backed by an unsupported implementation. */ @Override - public PropertyTypeBuilder expectedType(final FeatureType valueType, final FeatureTypeBuilder addTo) { + public PropertyTypeBuilder expectedType(final DefaultFeatureType valueType, final FeatureTypeBuilder addTo) { AttributeTypeBuilder<?> att; cases: if (operation.isGeometryInOut()) { - final PropertyTypeBuilder type = FeatureExpression.expectedType(getParameters().get(0), valueType, addTo); - if (type instanceof AttributeTypeBuilder<?>) { - att = (AttributeTypeBuilder<?>) type; - final Geometries<?> library = Geometries.implementation(att.getValueClass()); - if (library != null) { - att = att.setValueClass(operation.getReturnType(library)); - break cases; + final FeatureExpression<?,?> fex = FeatureExpression.castOrCopy(getParameters().get(0)); + if (fex != null) { + final PropertyTypeBuilder type = fex.expectedType(valueType, addTo); + if (type instanceof AttributeTypeBuilder<?>) { + att = (AttributeTypeBuilder<?>) type; + final Geometries<?> library = Geometries.implementation(att.getValueClass()); + if (library != null) { + att = att.setValueClass(operation.getReturnType(library)); + break cases; + } } } - throw new InvalidFilterValueException(Resources.format(Resources.Keys.NotAGeometryAtFirstExpression)); + throw new IllegalArgumentException(Resources.format(Resources.Keys.NotAGeometryAtFirstExpression)); } else { att = addTo.addAttribute(getValueClass()); } diff --cc core/sis-feature/src/test/java/org/apache/sis/filter/PeriodLiteral.java index d1f8e71,0a9dd86..d2213c1 --- a/core/sis-feature/src/test/java/org/apache/sis/filter/PeriodLiteral.java +++ b/core/sis-feature/src/test/java/org/apache/sis/filter/PeriodLiteral.java @@@ -61,7 -74,11 +61,7 @@@ final strictfp class PeriodLiteral impl } /** Not needed for the tests. */ - @Override public <N> Expression<AbstractFeature,N> toValueType(Class<N> type) {throw new UnsupportedOperationException();} - @Override public ReferenceIdentifier getName() {throw new UnsupportedOperationException();} - @Override public RelativePosition relativePosition(TemporalPrimitive o) {throw new UnsupportedOperationException();} - @Override public Duration distance(TemporalGeometricPrimitive o) {throw new UnsupportedOperationException();} - @Override public Duration length() {throw new UnsupportedOperationException();} - @Override public <N> Expression<Feature,N> toValueType(Class<N> target) {throw new UnsupportedOperationException();} ++ @Override public <N> Expression<AbstractFeature,N> toValueType(Class<N> target) {throw new UnsupportedOperationException();} /** * Hash code value. Used by the tests for checking the results of deserialization. diff --cc core/sis-metadata/src/test/java/org/apache/sis/metadata/TreeTableFormatTest.java index eaf3bae,9571ad9..67c4165 --- a/core/sis-metadata/src/test/java/org/apache/sis/metadata/TreeTableFormatTest.java +++ b/core/sis-metadata/src/test/java/org/apache/sis/metadata/TreeTableFormatTest.java @@@ -97,8 -96,9 +97,8 @@@ public final strictfp class TreeTableFo " │ │ ├─East bound longitude…… 180°E\n" + " │ │ ├─South bound latitude…… 90°S\n" + " │ │ ├─North bound latitude…… 90°N\n" + - " │ │ └─Extent type code……………… true\n" + + " │ │ └─Extent type code……………… True\n" + - " │ ├─Organisation……………………………………………… Kōdansha\n" + - " │ └─Role…………………………………………………………………… Editor\n" + + " │ └─Party………………………………………………………………… Kōdansha\n" + " ├─Presentation form (1 of 2)…………………… Document digital\n" + " ├─Presentation form (2 of 2)…………………… Document hardcopy\n" + " └─ISBN……………………………………………………………………………… 9782505004509\n", text); diff --cc core/sis-utility/src/main/java/org/apache/sis/internal/util/PropertyFormat.java index a30a890,3fbcecf..47f7224 --- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/PropertyFormat.java +++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/PropertyFormat.java @@@ -92,13 -97,15 +97,15 @@@ public abstract class PropertyFormat ex } text = columnFormat.format(value); } else if (value instanceof InternationalString) { - text = ((InternationalString) value).toString(getLocale()); + text = freeText(((InternationalString) value).toString(getLocale())); } else if (value instanceof CharSequence) { - text = value.toString(); + text = freeText(value.toString()); - } else if (value instanceof ControlledVocabulary) { - text = MetadataServices.getInstance().getCodeTitle((ControlledVocabulary) value, getLocale()); - } else if (value instanceof Boolean) { - text = Vocabulary.getResources(getLocale()).getString((Boolean) value ? Vocabulary.Keys.True : Vocabulary.Keys.False); + } else if (value instanceof CodeList<?>) { + text = MetadataServices.getInstance().getCodeTitle((CodeList<?>) value, getLocale()); } else if (value instanceof Enum<?>) { text = CharSequences.upperCaseToSentence(((Enum<?>) value).name()); ++ } else if (value instanceof Boolean) { ++ text = Vocabulary.getResources(getLocale()).getString((Boolean) value ? Vocabulary.Keys.True : Vocabulary.Keys.False); } else if (value instanceof Type) { appendName(((Type) value).getTypeName()); return; diff --cc storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java index e1bfe7d,ee2f90b..77fe210 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java @@@ -973,10 -975,11 +979,11 @@@ previous: for (int i=components.size( */ @Override void createCS(CSFactory factory, Map<String,?> properties, CoordinateSystemAxis[] axes) throws FactoryException { try { - if (axes.length > 2) { - coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1], axes[2]); - } else { - coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1]); + switch (axes.length) { + case 0: break; // Should never happen but we are paranoiac. - case 1: coordinateSystem = factory.createParametricCS(properties, axes[0]); return; ++ case 1: coordinateSystem = new org.apache.sis.referencing.cs.DefaultParametricCS(properties, axes[0]); return; + case 2: coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1]); return; + default: coordinateSystem = factory.createAffineCS(properties, axes[0], axes[1], axes[2]); return; } } catch (InvalidGeodeticParameterException e) { /* diff --cc storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java index d5d274e,5756e44..c6603b3 --- a/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java +++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java @@@ -378,15 -380,17 +378,15 @@@ public final strictfp class SQLStoreTes */ final FeatureSet parks = dataset.findResource("Parks"); final FeatureQuery query = new FeatureQuery(); - query.setProjection(new FeatureQuery.NamedExpression(FF.property(desiredProperty))); + query.setProjection(FF.property(desiredProperty)); - query.setSortBy(FF.sort(FF.property("country"), SortOrder.DESCENDING), - FF.sort(FF.property(desiredProperty), SortOrder.ASCENDING)); final FeatureSet subset = parks.subset(query); /* - * Verify that all features have the expected property, then verify the sorted values. + * Verify that all features have the expected property. */ final Object[] values; - try (Stream<Feature> features = subset.features(false)) { + try (Stream<AbstractFeature> features = subset.features(false)) { values = features.map(f -> { - final PropertyType p = TestUtilities.getSingleton(f.getType().getProperties(true)); + final AbstractIdentifiedType p = TestUtilities.getSingleton(f.getType().getProperties(true)); assertEquals("Feature has wrong property.", desiredProperty, p.getName().toString()); return f.getPropertyValue(desiredProperty); }).toArray(); @@@ -535,8 -539,9 +535,8 @@@ * Add a filter for parks in France. */ final FeatureQuery query = new FeatureQuery(); - query.setSortBy(FF.sort(FF.property("native_name"), SortOrder.DESCENDING)); query.setSelection(FF.equal(FF.property("country"), FF.literal("FRA"))); - query.setProjection(new FeatureQuery.NamedExpression(FF.property("native_name"))); + query.setProjection(FF.property("native_name")); final FeatureSet frenchParks = parks.subset(query); /* * Verify the feature type. diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java index 11d8896,799c346..f9a9160 --- 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 @@@ -35,19 -37,22 +37,20 @@@ import org.apache.sis.internal.storage. import org.apache.sis.filter.DefaultFilterFactory; import org.apache.sis.filter.Optimization; import org.apache.sis.util.ArgumentChecks; - import org.apache.sis.util.Classes; + import org.apache.sis.util.CharSequences; import org.apache.sis.util.collection.Containers; import org.apache.sis.util.iso.Names; + import org.apache.sis.util.resources.Vocabulary; // 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; -import org.opengis.filter.ValueReference; -import org.opengis.filter.SortBy; -import org.opengis.filter.SortProperty; -import org.opengis.filter.InvalidFilterValueException; +import org.apache.sis.feature.AbstractFeature; +import org.apache.sis.feature.DefaultFeatureType; +import org.apache.sis.filter.Filter; +import org.apache.sis.filter.Expression; +import org.apache.sis.internal.geoapi.filter.Literal; +import org.apache.sis.internal.geoapi.filter.ValueReference; +import org.apache.sis.internal.geoapi.filter.SortBy; +import org.apache.sis.internal.geoapi.filter.SortProperty; /** @@@ -581,9 -586,65 +590,65 @@@ public class FeatureQuery extends Quer if (projection == null) { return valueType; // All columns included: result is of the same type. } + int unnamedNumber = 0; // Sequential number for unnamed expressions. + Set<String> names = null; // Names already used, for avoiding collisions. final FeatureTypeBuilder ftb = new FeatureTypeBuilder().setName(valueType.getName()); - for (int i=0; i<projection.length; i++) { - projection[i].expectedType(i, valueType, ftb); + for (int column = 0; column < projection.length; column++) { + /* + * For each property, get the expected type (mandatory) and its name (optional). + * A default name will be computed if no alias were explicitly given by user. + */ + GenericName name = projection[column].alias; + final Expression<?,?> expression = projection[column].expression; + final FeatureExpression<?,?> fex = FeatureExpression.castOrCopy(expression); + final PropertyTypeBuilder resultType; + if (fex == null || (resultType = fex.expectedType(valueType, ftb)) == null) { - throw new InvalidFilterValueException(Resources.format(Resources.Keys.InvalidExpression_2, ++ throw new IllegalArgumentException(Resources.format(Resources.Keys.InvalidExpression_2, + expression.getFunctionName().toInternationalString(), column)); + } + if (name == null) { + /* + * Build a list of aliases declared by the user, for making sure that we do not collide with them. + * No check for `GenericName` collision here because it was already verified by `setProjection(…)`. + * We may have collision of their `String` representations however, which is okay. + */ + if (names == null) { + names = new HashSet<>(Containers.hashMapCapacity(projection.length)); + for (final NamedExpression p : projection) { + if (p.alias != null) { + names.add(p.alias.toString()); + } + } + } + /* + * If the expression is a `ValueReference`, the `PropertyType` instance can be taken directly + * from the source feature (the Apache SIS implementation does just that). If the name is set, + * then we assume that it is correct. Otherwise we take the tip of the XPath. + */ + CharSequence text = null; + if (expression instanceof ValueReference<?,?>) { + final GenericName current = resultType.getName(); + if (current != null && names.add(current.toString())) { + continue; + } + String xpath = ((ValueReference<?,?>) expression).getXPath().trim(); + xpath = xpath.substring(xpath.lastIndexOf('/') + 1); // Works also if '/' is not found. + if (!(xpath.isEmpty() || names.contains(xpath))) { + text = xpath; + } + } + /* + * If we still have no name at this point, create a name like "Unnamed #1". + * Note that despite the use of `Vocabulary` resources, the name will be unlocalized + * (for easier programmatic use) because `GenericName` implementation is designed for + * providing localized names only if explicitly requested. + */ + if (text == null) do { + text = Vocabulary.formatInternational(Vocabulary.Keys.Unnamed_1, ++unnamedNumber); + } while (!names.add(text.toString())); + name = Names.createLocalName(null, null, text); + } + resultType.setName(name); } return ftb.build(); } diff --cc storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java index b3af6a8,a4d157d..0f79898 --- 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 @@@ -60,33 -68,61 +63,61 @@@ public final strictfp class FeatureQuer private final FeatureQuery query; /** - * Creates a new test. + * Creates a new test with a feature type composed of two attributes and one association. */ public FeatureQueryTest() { - final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); - ftb.setName("Test"); + FeatureTypeBuilder ftb; + + // A dependency of the test feature type. + ftb = new FeatureTypeBuilder().setName("Dependency"); + ftb.addAttribute(Integer.class).setName("value3"); - final FeatureType dependency = ftb.build(); ++ final DefaultFeatureType dependency = ftb.build(); + + // Test feature type with attributes and association. + ftb = new FeatureTypeBuilder().setName("Test"); ftb.addAttribute(Integer.class).setName("value1"); ftb.addAttribute(Integer.class).setName("value2"); + ftb.addAssociation(dependency).setName("dependency"); - final FeatureType type = ftb.build(); - features = new Feature[] { + final DefaultFeatureType type = ftb.build(); + features = new AbstractFeature[] { - feature(type, 3, 1), - feature(type, 2, 2), - feature(type, 2, 1), - feature(type, 1, 1), - feature(type, 4, 1) + feature(type, null, 3, 1, 0), + feature(type, null, 2, 2, 0), + feature(type, dependency, 2, 1, 25), + feature(type, dependency, 1, 1, 18), + feature(type, null, 4, 1, 0) }; featureSet = new MemoryFeatureSet(null, type, Arrays.asList(features)); query = new FeatureQuery(); } - private static AbstractFeature feature(final DefaultFeatureType type, final int value1, final int value2) { + /** + * Creates an instance of the test feature type with the given values. + * The {@code value3} is stored only if {@code dependency} is non-null. + */ - private static Feature feature(final FeatureType type, final FeatureType dependency, ++ private static AbstractFeature feature(final DefaultFeatureType type, final DefaultFeatureType dependency, + final int value1, final int value2, final int value3) + { - final Feature f = type.newInstance(); + final AbstractFeature f = type.newInstance(); f.setPropertyValue("value1", value1); f.setPropertyValue("value2", value2); + if (dependency != null) { - final Feature d = dependency.newInstance(); ++ final AbstractFeature d = dependency.newInstance(); + d.setPropertyValue("value3", value3); + f.setPropertyValue("dependency", d); + } return f; } /** + * Configures the query for returning a single instance and returns that instance. + */ - private Feature executeAndGetFirst() throws DataStoreException { ++ private AbstractFeature executeAndGetFirst() throws DataStoreException { + query.setLimit(1); + final FeatureSet subset = query.execute(featureSet); + return TestUtilities.getSingleton(subset.features(false).collect(Collectors.toList())); + } + + /** * Executes the query and verify that the result is equal to the features at the given indices. * * @param indices indices of expected features. @@@ -130,39 -166,75 +161,62 @@@ } /** - * Verifies the effect of {@link FeatureQuery#setSortBy(SortProperty[])}. - * - * @throws DataStoreException if an error occurred while executing the query. - */ - @Test - public void testSortBy() throws DataStoreException { - final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); - query.setSortBy(ff.sort(ff.property("value1", Integer.class), SortOrder.ASCENDING), - ff.sort(ff.property("value2", Integer.class), SortOrder.DESCENDING)); - verifyQueryResult(3, 1, 2, 0, 4); - } - - /** + * Verifies the effect of {@link FeatureQuery#setSelection(Filter)}. + * + * @throws DataStoreException if an error occurred while executing the query. + */ + @Test + public void testSelection() throws DataStoreException { - final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); ++ final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures(); + query.setSelection(ff.equal(ff.property("value1", Integer.class), - ff.literal(2), true, MatchAction.ALL)); ++ ff.literal(2))); + verifyQueryResult(1, 2); + } + + /** + * Tests {@link FeatureQuery#setSelection(Filter)} on complex features + * with a filter that follows associations. + * + * @throws DataStoreException if an error occurred while executing the query. + */ + @Test + public void testSelectionThroughAssociation() throws DataStoreException { - final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); ++ final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures(); + query.setSelection(ff.equal(ff.property("dependency/value3"), ff.literal(18))); + verifyQueryResult(3); + } + + /** * Verifies the effect of {@link FeatureQuery#setProjection(FeatureQuery.Column[])}. * * @throws DataStoreException if an error occurred while executing the query. */ @Test public void testProjection() throws DataStoreException { - final DefaultFilterFactory<AbstractFeature,?,?> factory = DefaultFilterFactory.forFeatures(); - query.setProjection(new FeatureQuery.NamedExpression(factory.property("value1", Integer.class), (String) null), - new FeatureQuery.NamedExpression(factory.property("value1", Integer.class), "renamed1"), - new FeatureQuery.NamedExpression(factory.literal("a literal"), "computed")); - query.setLimit(1); - - final FeatureSet fs = query.execute(featureSet); - final AbstractFeature result = TestUtilities.getSingleton(fs.features(false).collect(Collectors.toList())); - final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); ++ final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures(); + query.setProjection(new FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) null), + new FeatureQuery.NamedExpression(ff.property("value1", Integer.class), "renamed1"), + new FeatureQuery.NamedExpression(ff.literal("a literal"), "computed")); // Check result type. - final DefaultFeatureType resultType = result.getType(); - final Feature instance = executeAndGetFirst(); - final FeatureType resultType = instance.getType(); ++ final AbstractFeature instance = executeAndGetFirst(); ++ final DefaultFeatureType resultType = instance.getType(); assertEquals("Test", resultType.getName().toString()); assertEquals(3, resultType.getProperties(true).size()); - final PropertyType pt1 = resultType.getProperty("value1"); - final PropertyType pt2 = resultType.getProperty("renamed1"); - final PropertyType pt3 = resultType.getProperty("computed"); - assertTrue(pt1 instanceof AttributeType); - assertTrue(pt2 instanceof AttributeType); - assertTrue(pt3 instanceof AttributeType); - assertEquals(Integer.class, ((AttributeType) pt1).getValueClass()); - assertEquals(Integer.class, ((AttributeType) pt2).getValueClass()); - assertEquals(String.class, ((AttributeType) pt3).getValueClass()); + final AbstractIdentifiedType pt1 = resultType.getProperty("value1"); + final AbstractIdentifiedType pt2 = resultType.getProperty("renamed1"); + final AbstractIdentifiedType pt3 = resultType.getProperty("computed"); + assertTrue(pt1 instanceof DefaultAttributeType); + assertTrue(pt2 instanceof DefaultAttributeType); + assertTrue(pt3 instanceof DefaultAttributeType); + assertEquals(Integer.class, ((DefaultAttributeType) pt1).getValueClass()); + assertEquals(Integer.class, ((DefaultAttributeType) pt2).getValueClass()); + assertEquals(String.class, ((DefaultAttributeType) pt3).getValueClass()); - // Check feature. - assertEquals(3, result.getPropertyValue("value1")); - assertEquals(3, result.getPropertyValue("renamed1")); - assertEquals("a literal", result.getPropertyValue("computed")); + // Check feature instance. + assertEquals(3, instance.getPropertyValue("value1")); + assertEquals(3, instance.getPropertyValue("renamed1")); + assertEquals("a literal", instance.getPropertyValue("computed")); } /** @@@ -173,10 -245,79 +227,79 @@@ @Test public void testProjectionByNames() throws DataStoreException { query.setProjection("value2"); - query.setLimit(1); - final FeatureSet fs = query.execute(featureSet); - final AbstractFeature result = TestUtilities.getSingleton(fs.features(false).collect(Collectors.toList())); - final AbstractIdentifiedType p = TestUtilities.getSingleton(result.getType().getProperties(true)); - final Feature instance = executeAndGetFirst(); - final PropertyType p = TestUtilities.getSingleton(instance.getType().getProperties(true)); ++ final AbstractFeature instance = executeAndGetFirst(); ++ final AbstractIdentifiedType p = TestUtilities.getSingleton(instance.getType().getProperties(true)); assertEquals("value2", p.getName().toString()); } + + /** + * Tests the creation of default column names when no alias where explicitly specified. + * Note that the string representations of default names shall be unlocalized. + * + * @throws DataStoreException if an error occurred while executing the query. + */ + @Test + public void testDefaultColumnName() throws DataStoreException { - final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); ++ final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures(); + query.setLimit(1); + query.setProjection( + ff.add(ff.property("value1", Number.class), ff.literal(1)), + ff.add(ff.property("value2", Number.class), ff.literal(1))); + final FeatureSet subset = featureSet.subset(query); - final FeatureType type = subset.getType(); - final Iterator<? extends PropertyType> properties = type.getProperties(true).iterator(); ++ final DefaultFeatureType type = subset.getType(); ++ final Iterator<? extends AbstractIdentifiedType> properties = type.getProperties(true).iterator(); + assertEquals("Unnamed #1", properties.next().getName().toString()); + assertEquals("Unnamed #2", properties.next().getName().toString()); + assertFalse(properties.hasNext()); + - final Feature instance = TestUtilities.getSingleton(subset.features(false).collect(Collectors.toList())); ++ final AbstractFeature instance = TestUtilities.getSingleton(subset.features(false).collect(Collectors.toList())); + assertSame(type, instance.getType()); + } + + /** + * Tests {@link FeatureQuery#setProjection(FeatureQuery.NamedExpression...)} on an abstract feature type. + * We expect the column to be defined even if the property name is undefined on the feature type. + * This case happens when the {@link FeatureSet} contains features with inherited types. + * + * @throws DataStoreException if an error occurred while executing the query. + */ + @Test + public void testProjectionOfAbstractType() throws DataStoreException { - final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); ++ final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures(); + query.setProjection(new FeatureQuery.NamedExpression(ff.property("value1"), (String) null), + new FeatureQuery.NamedExpression(ff.property("/*/unknown"), "unexpected")); + + // Check result type. - final Feature instance = executeAndGetFirst(); - final FeatureType resultType = instance.getType(); ++ final AbstractFeature instance = executeAndGetFirst(); ++ final DefaultFeatureType resultType = instance.getType(); + assertEquals("Test", resultType.getName().toString()); + assertEquals(2, resultType.getProperties(true).size()); - final PropertyType pt1 = resultType.getProperty("value1"); - final PropertyType pt2 = resultType.getProperty("unexpected"); - assertTrue(pt1 instanceof AttributeType); - assertTrue(pt2 instanceof AttributeType); - assertEquals(Integer.class, ((AttributeType) pt1).getValueClass()); - assertEquals(Object.class, ((AttributeType) pt2).getValueClass()); ++ final AbstractIdentifiedType pt1 = resultType.getProperty("value1"); ++ final AbstractIdentifiedType pt2 = resultType.getProperty("unexpected"); ++ assertTrue(pt1 instanceof DefaultAttributeType); ++ assertTrue(pt2 instanceof DefaultAttributeType); ++ assertEquals(Integer.class, ((DefaultAttributeType) pt1).getValueClass()); ++ assertEquals(Object.class, ((DefaultAttributeType) pt2).getValueClass()); + + // Check feature property values. + assertEquals(3, instance.getPropertyValue("value1")); + assertEquals(null, instance.getPropertyValue("unexpected")); + } + + /** + * Tests {@link FeatureQuery#setProjection(FeatureQuery.NamedExpression...)} on complex features + * with a filter that follows associations. + * + * @throws DataStoreException if an error occurred while executing the query. + */ + @Test + public void testProjectionThroughAssociation() throws DataStoreException { - final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); ++ final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures(); + query.setProjection(new FeatureQuery.NamedExpression(ff.property("value1"), (String) null), + new FeatureQuery.NamedExpression(ff.property("dependency/value3"), "value3")); + query.setOffset(2); - final Feature instance = executeAndGetFirst(); ++ final AbstractFeature instance = executeAndGetFirst(); + assertEquals("value1", 2, instance.getPropertyValue("value1")); + assertEquals("value3", 25, instance.getPropertyValue("value3")); + } }