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 4cba6e649c62b3a8a6a4eb330f8f7f376a12a665 Merge: e13ceef 7b8a6a6 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Dec 31 02:51:25 2021 +0100 Merge remote-tracking branch 'origin/fix/projection_name_clash' into geoapi-4.0 together with correction for the bug demonstrated by the new test. .../sis/feature/builder/FeatureTypeBuilder.java | 13 ++- .../org/apache/sis/util/resources/Vocabulary.java | 5 + .../sis/util/resources/Vocabulary.properties | 1 + .../sis/util/resources/Vocabulary_fr.properties | 1 + .../org/apache/sis/storage/sql/SQLStoreTest.java | 4 +- .../java/org/apache/sis/storage/FeatureQuery.java | 113 +++++++++++++++------ .../org/apache/sis/storage/FeatureQueryTest.java | 26 ++++- 7 files changed, 129 insertions(+), 34 deletions(-) diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java index 55457bc,55457bc..3ab4bdc --- 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 @@@ -692,6 -692,6 +692,8 @@@ 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. * * @param <V> the compile-time type of values in the {@code template} argument. * @param template an existing attribute type to use as a template. @@@ -795,7 -795,7 +797,9 @@@ /** * 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. * * @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. @@@ -813,6 -813,6 +817,7 @@@ /** * 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> @@@ -820,8 -820,8 +825,12 @@@ * 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. ++ * * @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-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java index 97b2e11,a3fa6f8..0f7d994 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.java @@@ -1285,6 -1265,6 +1285,11 @@@ public final class Vocabulary extends I public static final short Unnamed = 208; /** ++ * Unnamed #{0} ++ */ ++ public static final short Unnamed_1 = 266; ++ ++ /** * Unspecified */ public static final short Unspecified = 209; diff --cc core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties index 3737547,23535c1..62e15b9 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary.properties @@@ -259,6 -255,6 +259,7 @@@ TypeOfResource = Type of resou Units = Units Unknown = Unknown Unnamed = Unnamed ++Unnamed_1 = Unnamed #{0} Unspecified = Unspecified Untitled = Untitled UnavailableContent = Unavailable content. diff --cc core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties index 959ad86,873cf3d..8440b29 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Vocabulary_fr.properties @@@ -266,6 -262,6 +266,7 @@@ TypeOfResource = Type de resso Units = Unit\u00e9s Unknown = Inconnu Unnamed = Sans nom ++Unnamed_1 = Sans nom \u2116{0} Unspecified = Non-sp\u00e9cifi\u00e9 Untitled = Sans titre UnavailableContent = Contenu non-disponible. diff --cc storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java index cd2cb95,094ceb4..5756e44 --- 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 @@@ -380,7 -369,7 +380,7 @@@ 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); @@@ -541,7 -501,7 +541,7 @@@ 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 ffee909,ffee909..9368d53 --- 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 @@@ -17,6 -17,6 +17,8 @@@ package org.apache.sis.storage; import java.util.Arrays; ++import java.util.Set; ++import java.util.HashSet; import java.util.Map; import java.util.LinkedHashMap; import java.util.Objects; @@@ -35,9 -35,9 +37,10 @@@ 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; @@@ -218,9 -218,9 +221,12 @@@ public class FeatureQuery extends Quer for (int i=0; i<properties.length; i++) { final NamedExpression c = properties[i]; ArgumentChecks.ensureNonNullElement("properties", i, c); -- final Object key = c.alias != null ? c.alias : c.expression; ++ Object key = (c.alias != null) ? c.alias : c.expression; final Integer p = uniques.putIfAbsent(key, i); if (p != null) { ++ if (key instanceof Expression) { ++ key = label((Expression) key); ++ } throw new IllegalArgumentException(Resources.format(Resources.Keys.DuplicatedQueryProperty_3, key, p, i)); } } @@@ -396,6 -396,6 +402,9 @@@ * An expression to be retrieved by a {@code Query}, together with the name to assign to it. * In relational database terminology, subset of columns is called <cite>projection</cite>. * Columns can be given to the {@link FeatureQuery#setProjection(NamedExpression[])} method. ++ * ++ * @version 1.2 ++ * @since 1.1 */ public static class NamedExpression implements Serializable { /** @@@ -451,30 -451,30 +460,6 @@@ } /** -- * Adds in the given builder the type of results computed by this column. -- * -- * @param column index of this column. Used for error message only. -- * @param valueType the type of features to be evaluated by the expression in this column. -- * @param addTo where to add the type of properties evaluated by expression in this column. -- * @throws IllegalArgumentException if this method can operate only on some feature types -- * and the given type is not one of them. -- * @throws InvalidFilterValueException if this method can not determine the result type of the expression -- * in this column. It may be because that expression is backed by an unsupported implementation. -- * -- * @see FeatureQuery#expectedType(FeatureType) -- */ -- final void expectedType(final int column, final FeatureType valueType, final FeatureTypeBuilder addTo) { -- final PropertyTypeBuilder resultType = FeatureExpression.expectedType(expression, valueType, addTo); -- if (resultType == null) { -- throw new InvalidFilterValueException(Resources.format(Resources.Keys.InvalidExpression_2, -- expression.getFunctionName().toInternationalString(), column)); -- } -- if (alias != null && !alias.equals(resultType.getName())) { -- resultType.setName(alias); -- } -- } -- -- /** * Returns a hash code value for this column. * * @return a hash code value. @@@ -524,7 -524,7 +509,7 @@@ } else if (expression instanceof ValueReference<?,?>) { buffer.append(((ValueReference<?,?>) expression).getXPath()); } else { -- buffer.append(Classes.getShortClassName(expression)); // Class name with enclosing class if any. ++ buffer.append(expression.getFunctionName()); } if (alias != null) { buffer.append(" AS “").append(alias).append('”'); @@@ -533,6 -533,6 +518,23 @@@ } /** ++ * Returns a label for the given expression for reporting to human (e.g. in exception messages). ++ * This method uses the value reference (XPath) or literal value if applicable, truncated to an ++ * arbitrary length. ++ */ ++ private static String label(final Expression<?,?> expression) { ++ final String text; ++ if (expression instanceof Literal<?,?>) { ++ text = String.valueOf(((Literal<?,?>) expression).getValue()); ++ } else if (expression instanceof ValueReference<?,?>) { ++ text = ((ValueReference<?,?>) expression).getXPath(); ++ } else { ++ return expression.getFunctionName().toString(); ++ } ++ return CharSequences.shortSentence(text, 40).toString(); ++ } ++ ++ /** * Applies this query on the given feature set. * This method is invoked by the default implementation of {@link FeatureSet#subset(Query)}. * The default implementation executes the query using the default {@link java.util.stream.Stream} methods. @@@ -565,6 -565,6 +567,13 @@@ /** * Returns the type of values evaluated by this query when executed on features of the given type. ++ * If some expressions have no name, default names are computed as below: ++ * ++ * <ul> ++ * <li>If the expression is an instance of {@link ValueReference}, ++ * the tip of the {@linkplain ValueReference#getXPath() x-path}.</li> ++ * <li>Otherwise the localized string "Unnamed #1" with increasing numbers.</li> ++ * </ul> * * @param valueType the type of features to be evaluated by the expressions in this query. * @return type resulting from expressions evaluation (never null). @@@ -577,9 -577,9 +586,55 @@@ 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 PropertyTypeBuilder resultType = FeatureExpression.expectedType(expression, valueType, ftb); ++ if (resultType == null) { ++ throw new InvalidFilterValueException(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.tip().toString()); ++ } ++ } ++ } ++ /* ++ * The rules for creating a default name are documented in this method Javadoc. ++ * 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. ++ */ ++ CharSequence text = null; ++ if (expression instanceof ValueReference<?,?>) { ++ 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 (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 6918ac9,f675e4f..7f6af73 --- 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 @@@ -18,6 -18,6 +18,7 @@@ package org.apache.sis.storage import java.util.List; import java.util.Arrays; ++import java.util.Iterator; import java.util.stream.Collectors; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.internal.storage.MemoryFeatureSet; @@@ -43,7 -44,7 +45,9 @@@ import org.opengis.filter.SortProperty * Tests {@link FeatureQuery} and (indirectly) {@link FeatureSubset}. * * @author Johann Sorel (Geomatys) -- * @version 1.1 ++ * @author Alexis Manin (Geomatys) ++ * @author Martin Desruisseaux (Geomatys) ++ * @version 1.2 * @since 1.0 * @module */ @@@ -209,4 -210,15 +213,24 @@@ public final strictfp class FeatureQuer final PropertyType p = TestUtilities.getSingleton(result.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 testColumnNameClash() throws DataStoreException { ++ public void testDefaultColumnName() throws DataStoreException { + final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); + 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); // NOTE: I would expect an error here ++ final FeatureSet subset = featureSet.subset(query); + final FeatureType type = subset.getType(); - assertEquals(2, type.getProperties(true).size()); ++ final Iterator<? extends PropertyType> properties = type.getProperties(true).iterator(); ++ assertEquals("Unnamed #1", properties.next().getName().toString()); ++ assertEquals("Unnamed #2", properties.next().getName().toString()); ++ assertFalse(properties.hasNext()); + } }