You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2019/06/21 07:54:44 UTC

[sis] branch master updated (1d47498 -> 5ed134f)

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

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


    from 1d47498  Merge branch 'geoapi-3.1'
     add 35bb3c4  Post-merge fix: some pending API were not recognized (if an interface existed in GeoAPI 3.0 but a new parent has been added in GeoAPI 3.1).
     add 693a3d1  fix(netcdf): Fix border behavior to ensure enclosing when reading a subgrid.
     add 4f4ef95  Minor adjustment of imports order.
     add 2f67744  feat(Storage): add a feature set decoration, allowing to view a sequence of feature sets as a single one.
     add ea26103  Refactor pull request #18: - Move code common between ConcatenatedFeatureSet and JoinFeatureSet in a new common parent: AggregatedFeatureSet. - Populate the `Metadata.lineage` node in ISO 19115 metadata with information about the source FeatureSets. - Implement ConcatenatedFeatureSet.subset(Query) by delegating the query to source feature sets. - Modify the algorithm finding common parent type: instead of counting occurrences (those numbers may be wrong in context of mul [...]
     add d44bfad  Merge branch 'feat/featureset-concatenation' into geoapi-4.0 (pull request #18).
     add 6e82922  Use OptionalLong for AbstractFeatureSet.getFeatureCount().
     add 38b1593  AggregatedFeatureSet tuning: - If a source can not compute the envelope, aggregated envelope is null. - Cache the envelope and add a `clearCache()` method (to be used later). - Add metadata tests.
     add 2b788ad  Adjustment in default implementation of Resources: - AbstractFeatureSet implements ResourceOnFileSystem - FeatureSet provides a default implementation of subset(Query).
     add abde658  DataSet.getEnvelope() should return Optional<Envelope>. https://issues.apache.org/jira/browse/SIS-464
     add 4e2add0  Resources.getIdentifier() should be Optional<GenericName> since the identifier may be absent.
     add 59b8d22  Merge branch 'geoapi-4.0' into geoapi-3.1
     new 5ed134f  Merge branch 'geoapi-3.1'

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/sis/feature/CommonParentFinder.java | 174 +++++++++++++++++++++
 .../org/apache/sis/feature/DefaultFeatureType.java |   6 +
 .../main/java/org/apache/sis/feature/Features.java |  19 ++-
 .../org/apache/sis/metadata/MetadataStandard.java  |   2 +-
 .../iso/maintenance/DefaultScopeDescription.java   |  80 +++++++++-
 .../iso/maintenance/LegacyFeatureType.java         |  21 +++
 .../java/org/apache/sis/test/MetadataAssert.java   |  53 ++++++-
 .../apache/sis/internal/util/CollectionsExt.java   |   2 +-
 .../org/apache/sis/internal/util/Numerics.java     |   6 +-
 .../sis/storage/earthobservation/LandsatStore.java |   7 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  12 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |   5 +-
 .../apache/sis/internal/netcdf/RasterResource.java |  11 +-
 .../sis/internal/netcdf/impl/FeaturesInfo.java     |   5 +-
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |   7 +-
 .../apache/sis/internal/sql/feature/Database.java  |   3 +-
 .../org/apache/sis/internal/sql/feature/Table.java |   5 +-
 .../java/org/apache/sis/storage/sql/SQLStore.java  |   7 +-
 .../sis/internal/storage/AbstractFeatureSet.java   |  49 ++----
 .../sis/internal/storage/AbstractGridResource.java |  12 +-
 .../sis/internal/storage/AbstractResource.java     |  48 ++++--
 .../internal/storage/FeatureCatalogBuilder.java    |   6 +-
 .../sis/internal/storage/MemoryFeatureSet.java     |   5 +-
 .../sis/internal/storage/MetadataBuilder.java      | 103 ++++++++++--
 .../sis/internal/storage/StoreUtilities.java       |   7 +-
 .../apache/sis/internal/storage/URIDataStore.java  |  31 +++-
 .../org/apache/sis/internal/storage/csv/Store.java |  13 +-
 .../apache/sis/internal/storage/folder/Store.java  |   5 +-
 .../sis/internal/storage/folder/WritableStore.java |   2 +-
 .../sis/internal/storage/query/FeatureSubset.java  |   7 +-
 .../main/java/org/apache/sis/storage/DataSet.java  |   7 +-
 .../java/org/apache/sis/storage/DataStore.java     |  23 +--
 .../java/org/apache/sis/storage/FeatureSet.java    |  11 +-
 .../main/java/org/apache/sis/storage/Resource.java |   7 +-
 .../internal/storage/AbstractGridResourceTest.java |   3 +-
 .../org/apache/sis/internal/storage/gpx/Store.java |   5 +-
 36 files changed, 617 insertions(+), 152 deletions(-)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/feature/CommonParentFinder.java


[sis] 01/01: Merge branch 'geoapi-3.1'

Posted by de...@apache.org.
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 5ed134f538e9409b9628bc1f65e9f6cfd5bd49bc
Merge: 1d47498 59b8d22
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Fri Jun 21 09:53:38 2019 +0200

    Merge branch 'geoapi-3.1'

 .../org/apache/sis/feature/CommonParentFinder.java | 174 +++++++++++++++++++++
 .../org/apache/sis/feature/DefaultFeatureType.java |   6 +
 .../main/java/org/apache/sis/feature/Features.java |  19 ++-
 .../org/apache/sis/metadata/MetadataStandard.java  |   2 +-
 .../iso/maintenance/DefaultScopeDescription.java   |  80 +++++++++-
 .../iso/maintenance/LegacyFeatureType.java         |  21 +++
 .../java/org/apache/sis/test/MetadataAssert.java   |  53 ++++++-
 .../apache/sis/internal/util/CollectionsExt.java   |   2 +-
 .../org/apache/sis/internal/util/Numerics.java     |   6 +-
 .../sis/storage/earthobservation/LandsatStore.java |   7 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |  12 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |   5 +-
 .../apache/sis/internal/netcdf/RasterResource.java |  11 +-
 .../sis/internal/netcdf/impl/FeaturesInfo.java     |   5 +-
 .../org/apache/sis/storage/netcdf/NetcdfStore.java |   7 +-
 .../apache/sis/internal/sql/feature/Database.java  |   3 +-
 .../org/apache/sis/internal/sql/feature/Table.java |   5 +-
 .../java/org/apache/sis/storage/sql/SQLStore.java  |   7 +-
 .../sis/internal/storage/AbstractFeatureSet.java   |  49 ++----
 .../sis/internal/storage/AbstractGridResource.java |  12 +-
 .../sis/internal/storage/AbstractResource.java     |  48 ++++--
 .../internal/storage/FeatureCatalogBuilder.java    |   6 +-
 .../sis/internal/storage/MemoryFeatureSet.java     |   5 +-
 .../sis/internal/storage/MetadataBuilder.java      | 103 ++++++++++--
 .../sis/internal/storage/StoreUtilities.java       |   7 +-
 .../apache/sis/internal/storage/URIDataStore.java  |  31 +++-
 .../org/apache/sis/internal/storage/csv/Store.java |  13 +-
 .../apache/sis/internal/storage/folder/Store.java  |   5 +-
 .../sis/internal/storage/folder/WritableStore.java |   2 +-
 .../sis/internal/storage/query/FeatureSubset.java  |   7 +-
 .../main/java/org/apache/sis/storage/DataSet.java  |   7 +-
 .../java/org/apache/sis/storage/DataStore.java     |  23 +--
 .../java/org/apache/sis/storage/FeatureSet.java    |  11 +-
 .../main/java/org/apache/sis/storage/Resource.java |   7 +-
 .../internal/storage/AbstractGridResourceTest.java |   3 +-
 .../org/apache/sis/internal/storage/gpx/Store.java |   5 +-
 36 files changed, 617 insertions(+), 152 deletions(-)

diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/CommonParentFinder.java
index 0000000,18c30b0..6e224e9
mode 000000,100644..100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/CommonParentFinder.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/CommonParentFinder.java
@@@ -1,0 -1,177 +1,174 @@@
+ /*
+  * 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.feature;
+ 
+ import java.util.Map;
+ import java.util.LinkedHashMap;
+ import java.util.function.Predicate;
+ 
 -// Branch-dependent imports
 -import org.opengis.feature.FeatureType;
 -
+ 
+ /**
+  * Finds a feature type common to all given types. This is either one of the given types, or a parent common to all types.
 - * A feature <var>F</var> is considered a common parent if <code>F.{@link DefaultFeatureType#isAssignableFrom(FeatureType)
++ * A feature <var>F</var> is considered a common parent if <code>F.{@link DefaultFeatureType#isAssignableFrom(DefaultFeatureType)
+  * isAssignableFrom}(type)</code> returns {@code true} for all elements <var>type</var> in the given array.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.0
+  * @since   1.0
+  * @module
+  */
+ final class CommonParentFinder {
+     /**
+      * Finds a feature type common to all given types. See class javadoc.
+      *
+      * @param  types  types for which to find a common type.
+      * @return a feature type which is assignable from all given types, or {@code null} if none.
+      */
 -    static FeatureType select(final Iterable<? extends FeatureType> types) {
++    static DefaultFeatureType select(final Iterable<? extends DefaultFeatureType> types) {
+         /*
+          * Get a set of unique feature types. Since the process done in this method may be relatively costly,
+          * we want to avoid doing the same work twice. The purpose of Boolean value will be explained later.
+          */
 -        final Map<FeatureType,Boolean> allTypes = new LinkedHashMap<>();
++        final Map<DefaultFeatureType,Boolean> allTypes = new LinkedHashMap<>();
+         types.forEach((type) -> allTypes.putIfAbsent(type, Boolean.FALSE));
+         allTypes.remove(null);
+         int count = allTypes.size();
 -        final FeatureType[] required = allTypes.keySet().toArray(new FeatureType[count]);
++        final DefaultFeatureType[] required = allTypes.keySet().toArray(new DefaultFeatureType[count]);
+         /*
+          * We are going to iterate over the `required` array many times. For performance reason, this array should be as
+          * short as possible. We can make it shorter by removing any element A which is assignable to another element B,
+          * since "B assignable from A" implies that a common parent assignable from B will also be assignable from A.
+          * If this simplification results in an array of only one element, we are done.
+          */
+         for (int i=0; i<count; i++) {
 -            final FeatureType parent = required[i];
++            final DefaultFeatureType parent = required[i];
+             for (int j=count; --j >= 0;) {                 // Reverse order so that removed elements do not impact index j.
+                 if (j != i && parent.isAssignableFrom(required[j])) {
+                     System.arraycopy(required, j+1, required, j, --count - j);      // If A is assignable from B, remove B.
+                     required[count] = null;
+                     if (j < i) i--;                         // If removed element was before i, then i indices are shifted.
+                 }
+             }
+         }
+         switch (count) {
+             case 0: return null;
+             case 1: return required[0];
+         }
+         /*
+          * If we reach this point, no element in the given types is a parent of all other types.
+          * We need to build a set of all parent types.
+          */
+         return new CommonParentFinder(allTypes, required, count).select();
+     }
+ 
+     /**
+      * All feature types examined by this class. Values are whether a feature type is retained
+      * as a candidate for common parent. Those values are initially {@link Boolean#FALSE} then
+      * updated after we find that a type {@linkplain #isAssignableFromAll is assignable from
+      * all required types}.
+      */
 -    private final Map<FeatureType,Boolean> allTypes;
++    private final Map<DefaultFeatureType,Boolean> allTypes;
+ 
+     /**
+      * The features types which must be assignable to the common parent.
+      * This array may not contain all feature types given by user, since we try to remove redundant elements.
+      */
 -    private final FeatureType[] required;
++    private final DefaultFeatureType[] required;
+ 
+     /**
+      * Number of valid elements in the {@link #required} array.
+      */
+     private final int count;
+ 
+     /**
+      * Creates a finder for a common parent of the given types.
+      * Invokes {@link #select()} after construction time for getting the parent.
+      */
 -    private CommonParentFinder(final Map<FeatureType,Boolean> allTypes, final FeatureType[] required, final int count) {
++    private CommonParentFinder(final Map<DefaultFeatureType,Boolean> allTypes, final DefaultFeatureType[] required, final int count) {
+         this.allTypes = allTypes;
+         this.required = required;
+         this.count    = count;
+         for (int i=0; i<count; i++) {
+             scanParents(required[i]);
+         }
+     }
+ 
+     /**
+      * Returns {@code true} if the given parent candidate is assignable from all required types.
+      * The feature type to be returned by {@link #select()} must met that condition.
+      */
 -    private boolean isAssignableFromAll(final FeatureType parent) {
++    private boolean isAssignableFromAll(final DefaultFeatureType parent) {
+         for (int i=0; i<count; i++) {
 -            final FeatureType type = required[i];
++            final DefaultFeatureType type = required[i];
+             if (type != parent && !parent.isAssignableFrom(type)) {
+                 return false;
+             }
+         }
+         return true;
+     }
+ 
+     /**
+      * Verifies if the given type has a parent which is assignable from all required types.
+      * This method verifies recursively parents of parents, skipping types that have already
+      * been examined in a previous invocation of this method.
+      */
 -    private void scanParents(final FeatureType type) {
 -        for (final FeatureType parent : type.getSuperTypes()) {
++    private void scanParents(final DefaultFeatureType type) {
++        for (final DefaultFeatureType parent : type.getSuperTypes()) {
+             if (allTypes.putIfAbsent(parent, Boolean.FALSE) == null) {
+                 if (isAssignableFromAll(parent)) {
+                     allTypes.put(parent, Boolean.TRUE);     // Found a candidate.
+                     skipParents(parent);                    // All parents of this candidate can be skipped.
+                 } else {
+                     scanParents(parent);                    // Verify if a parent is assignable.
+                 }
+             }
+         }
+     }
+ 
+     /**
+      * Invoked when the given feature type is assignable from all required types.
+      * There is no need to verify the parents since they are not going to be a better match.
+      */
 -    private void skipParents(final FeatureType type) {
++    private void skipParents(final DefaultFeatureType type) {
+         assert isAssignableFromAll(type);
 -        for (final FeatureType parent : type.getSuperTypes()) {
++        for (final DefaultFeatureType parent : type.getSuperTypes()) {
+             if (Boolean.TRUE.equals(allTypes.put(parent, Boolean.FALSE))) {
+                 // If `parent` was previously a candidate, its parents have already been set to `FALSE`.
+             } else {
+                 skipParents(parent);
+             }
+         }
+     }
+ 
+     /**
+      * Invoked after all feature types have been examined. This method removes all features types that
+      * are not parent of required types, then select the one having the greatest number of properties.
+      */
 -    FeatureType select() {
++    DefaultFeatureType select() {
+         allTypes.values().removeIf(Predicate.isEqual(Boolean.FALSE));
 -        FeatureType best = null;
++        DefaultFeatureType best = null;
+         int numProperties = 0;
 -        for (final FeatureType type : allTypes.keySet()) {
++        for (final DefaultFeatureType type : allTypes.keySet()) {
+             final int n = type.getProperties(true).size();
+             if (best == null || n > numProperties) {
+                 best = type;
+                 numProperties = n;
+             }
+         }
+         return best;
+     }
+ }
diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/Features.java
index d7dd0a0,4852a3f..672e8be
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/Features.java
@@@ -108,8 -120,25 +108,25 @@@ public final class Features extends Sta
      }
  
      /**
+      * Finds a feature type common to all given types, or returns {@code null} if none is found.
+      * The return value is either one of the given types, or a parent common to all types.
+      * A feature <var>F</var> is considered a common parent if <code>F.{@link DefaultFeatureType#isAssignableFrom
+      * isAssignableFrom}(type)</code> returns {@code true} for all elements <var>type</var> in the given array.
+      *
+      * @param  types  types for which to find a common type, or {@code null}.
+      * @return a feature type which is assignable from all given types, or {@code null} if none.
+      *
 -     * @see FeatureType#isAssignableFrom(FeatureType)
++     * @see DefaultFeatureType#isAssignableFrom(DefaultFeatureType)
+      *
+      * @since 1.0
+      */
 -    public static FeatureType findCommonParent(final Iterable<? extends FeatureType> types) {
++    public static DefaultFeatureType findCommonParent(final Iterable<? extends DefaultFeatureType> types) {
+         return (types != null) ? CommonParentFinder.select(types) : null;
+     }
+ 
+     /**
       * Returns the name of the type of values that the given property can take.
 -     * The type of value can be a {@link Class}, a {@link org.opengis.feature.FeatureType}
 +     * The type of value can be a {@link Class}, a {@code FeatureType}
       * or another {@code PropertyType} depending on given argument:
       *
       * <ul>
diff --cc core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java
index dbf49a0,8f22c56..c9a9f70
--- a/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/test/MetadataAssert.java
@@@ -22,6 -22,11 +22,9 @@@ import org.xml.sax.SAXException
  import javax.xml.parsers.ParserConfigurationException;
  import org.opengis.util.InternationalString;
  import org.opengis.metadata.citation.Citation;
+ import org.opengis.metadata.lineage.Source;
 -import org.opengis.metadata.maintenance.Scope;
+ import org.opengis.metadata.maintenance.ScopeCode;
 -import org.opengis.metadata.content.FeatureTypeInfo;
+ import org.opengis.metadata.content.FeatureCatalogueDescription;
  import org.opengis.referencing.IdentifiedObject;
  import org.apache.sis.io.wkt.Symbols;
  import org.apache.sis.io.wkt.WKTFormat;
@@@ -29,10 -34,12 +32,18 @@@ import org.apache.sis.io.wkt.Convention
  import org.apache.sis.xml.Namespaces;
  import org.apache.sis.test.xml.DocumentComparator;
  import org.apache.sis.internal.xml.LegacyNamespaces;
+ import org.apache.sis.internal.util.CollectionsExt;
+ 
+ import static org.apache.sis.test.TestUtilities.getSingleton;
  
  // Branch-specific imports
 -import org.opengis.metadata.citation.Responsibility;
++import org.opengis.feature.type.FeatureType;
 +import org.apache.sis.metadata.iso.citation.DefaultCitation;
 +import org.apache.sis.metadata.iso.citation.DefaultResponsibility;
++import org.apache.sis.metadata.iso.content.DefaultFeatureCatalogueDescription;
++import org.apache.sis.metadata.iso.content.DefaultFeatureTypeInfo;
++import org.apache.sis.metadata.iso.lineage.DefaultSource;
++import org.apache.sis.metadata.iso.quality.DefaultScope;
  
  
  /**
@@@ -91,15 -98,53 +102,53 @@@ public strictfp class MetadataAssert ex
       *
       * @since 0.8
       */
 -    public static void assertPartyNameEquals(final String message, final String expected, final Citation citation) {
 +    public static void assertPartyNameEquals(final String message, final String expected, final DefaultCitation citation) {
          assertNotNull(message, citation);
-         final DefaultResponsibility r = (DefaultResponsibility) TestUtilities.getSingleton(citation.getCitedResponsibleParties());
-         final InternationalString name = TestUtilities.getSingleton(r.getParties()).getName();
 -        final Responsibility r = getSingleton(citation.getCitedResponsibleParties());
++        final DefaultResponsibility r = (DefaultResponsibility) getSingleton(citation.getCitedResponsibleParties());
+         final InternationalString name = getSingleton(r.getParties()).getName();
          assertNotNull(message, name);
          assertEquals(message, expected, name.toString(Locale.US));
      }
  
      /**
+      * Verifies that the given {@code ContentInfo} describes the given feature.
+      * This method expects that the given catalog contains exactly one feature info.
+      *
+      * @param  name     expected feature type name (possibly null).
+      * @param  count    expected feature instance count (possibly null).
+      * @param  catalog  the content info to validate.
+      *
+      * @since 1.0
+      */
+     public static void assertContentInfoEquals(final String name, final Integer count, final FeatureCatalogueDescription catalog) {
 -        final FeatureTypeInfo info = getSingleton(catalog.getFeatureTypeInfo());
++        final DefaultFeatureTypeInfo info = getSingleton(((DefaultFeatureCatalogueDescription) catalog).getFeatureTypeInfo());
+         assertEquals("metadata.contentInfo.featureType", name, String.valueOf(info.getFeatureTypeName()));
+         assertEquals("metadata.contentInfo.featureInstanceCount", count, info.getFeatureInstanceCount());
+     }
+ 
+     /**
+      * Verifies that the source contains the given feature type. This method expects that the given source contains
+      * exactly one scope description and that the hierarchical level is {@link ScopeCode#FEATURE_TYPE}.
+      *
+      * @param  name      expected source identifier.
+      * @param  features  expected names of feature type.
+      * @param  source    the source to validate.
+      *
+      * @since 1.0
+      */
+     public static void assertFeatureSourceEquals(final String name, final String[] features, final Source source) {
+         assertEquals("metadata.lineage.source.sourceCitation.title", name, String.valueOf(source.getSourceCitation().getTitle()));
 -        final Scope scope = source.getScope();
++        final DefaultScope scope = (DefaultScope) ((DefaultSource) source).getScope();
+         assertNotNull("metadata.lineage.source.scope", scope);
+         assertEquals("metadata.lineage.source.scope.level", ScopeCode.FEATURE_TYPE, scope.getLevel());
 -        final CharSequence[] actual = CollectionsExt.toArray(getSingleton(scope.getLevelDescription()).getFeatures(), CharSequence.class);
++        final Object[] actual = CollectionsExt.toArray(getSingleton(scope.getLevelDescription()).getFeatures(), FeatureType.class);
+         for (int i=0; i<actual.length; i++) {
+             actual[i] = actual[i].toString();
+         }
+         assertArrayEquals("metadata.lineage.source.scope.levelDescription.feature", features, actual);
+     }
+ 
+     /**
       * Asserts that the WKT 2 of the given object is equal to the expected one.
       * This method expected the {@code “…”} quotation marks instead of {@code "…"}
       * for easier readability of {@link String} constants in Java code.
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
index e548f0f,d3da090..30a4767
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/AbstractFeatureSet.java
@@@ -80,12 -80,9 +80,9 @@@ public abstract class AbstractFeatureSe
       * @see DataStore#getIdentifier()
       */
      @Override
-     public GenericName getIdentifier() throws DataStoreException {
+     public Optional<GenericName> getIdentifier() throws DataStoreException {
 -        final FeatureType type = getType();
 +        final DefaultFeatureType type = getType();
-         if (type != null) {
-             return type.getName();
-         }
-         return null;
+         return (type != null) ? Optional.of(type.getName()) : Optional.empty();
      }
  
      /**
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/FeatureCatalogBuilder.java
index ff362aa,5b38bf3..bf883d1
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/FeatureCatalogBuilder.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/FeatureCatalogBuilder.java
@@@ -28,7 -28,7 +28,7 @@@ import org.apache.sis.feature.DefaultFe
  
  /**
   * Helper methods for the feature metadata created by {@code DataStore} implementations.
-  * This is a convenience class for chaining {@code addFeatureType(FeatureType, Integer)}
 - * This is a convenience class for chaining {@link #addFeatureType(FeatureType, long)}
++ * This is a convenience class for chaining {@code addFeatureType(FeatureType, long)}
   * method calls with {@link FeatureNaming#add(DataStore, GenericName, Object)}.
   *
   * @author  Martin Desruisseaux (Geomatys)
@@@ -74,10 -74,10 +74,10 @@@ public final class FeatureCatalogBuilde
       * @param  type  the feature type to add, or {@code null}.
       * @throws IllegalNameException if a feature of the same name has already been added.
       *
-      * @see #addFeatureType(DefaultFeatureType, Integer)
 -     * @see #addFeatureType(FeatureType, long)
++     * @see #addFeatureType(DefaultFeatureType, long)
       */
 -    public final void define(final FeatureType type) throws IllegalNameException {
 +    public final void define(final DefaultFeatureType type) throws IllegalNameException {
-         final GenericName name = addFeatureType(type, null);
+         final GenericName name = addFeatureType(type, -1);
          if (name != null) {
              features.add(store, name, type);
          }
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
index 7d67376,7f3c2cd..c6a31a7
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
@@@ -65,6 -69,8 +69,7 @@@ import org.opengis.referencing.cs.Coord
  import org.opengis.referencing.crs.VerticalCRS;
  import org.opengis.referencing.crs.CoordinateReferenceSystem;
  import org.opengis.referencing.operation.TransformException;
+ import org.apache.sis.internal.util.CollectionsExt;
 -import org.apache.sis.internal.simple.SimpleDuration;
  import org.apache.sis.geometry.AbstractEnvelope;
  import org.apache.sis.metadata.iso.DefaultMetadata;
  import org.apache.sis.metadata.iso.DefaultIdentifier;
@@@ -97,6 -103,6 +102,7 @@@ import org.apache.sis.metadata.iso.cita
  import org.apache.sis.metadata.iso.citation.DefaultOrganisation;
  import org.apache.sis.metadata.iso.citation.DefaultOnlineResource;
  import org.apache.sis.metadata.iso.constraint.DefaultLegalConstraints;
++import org.apache.sis.metadata.iso.identification.AbstractIdentification;
  import org.apache.sis.metadata.iso.identification.DefaultKeywords;
  import org.apache.sis.metadata.iso.identification.DefaultResolution;
  import org.apache.sis.metadata.iso.identification.DefaultDataIdentification;
@@@ -1874,12 -1876,12 +1881,12 @@@ parse:      for (int i = 0; i < length;
       * Note that the {@link FeatureCatalogBuilder} subclasses can also be used for that chaining.
       *
       * @param  type         the feature type to add, or {@code null} for no-operation.
-      * @param  occurrences  number of instances of the given feature type, or {@code null} if unknown.
+      * @param  occurrences  number of instances of the given feature type, or a negative value if unknown.
       * @return the name of the added feature, or {@code null} if none.
       *
 -     * @see FeatureCatalogBuilder#define(FeatureType)
 +     * @see FeatureCatalogBuilder#define(DefaultFeatureType)
       */
-     public final GenericName addFeatureType(final DefaultFeatureType type, final Integer occurrences) {
 -    public final GenericName addFeatureType(final FeatureType type, final long occurrences) {
++    public final GenericName addFeatureType(final DefaultFeatureType type, final long occurrences) {
          if (type != null) {
              final GenericName name = type.getName();
              if (name != null) {
@@@ -2733,6 -2751,55 +2740,58 @@@
      }
  
      /**
+      * Adds information about a source of data used for producing the resource.
+      * Storage location is:
+      *
+      * <ul>
+      *   <li>{@code metadata/resourceLineage/source/scope/level}</li>
+      *   <li>{@code metadata/resourceLineage/source/scope/extent}</li>
+      *   <li>{@code metadata/resourceLineage/source/scope/levelDescription/*}</li>
+      *   <li>{@code metadata/resourceLineage/source/citation}</li>
+      *   <li>{@code metadata/resourceLineage/source/sourceReferenceSystem}</li>
+      *   <li>{@code metadata/resourceLineage/source/sourceSpatialResolution}</li>
+      * </ul>
+      *
+      * <div class="note"><b>Example:</b>
+      * if a {@code FeatureSet} is the aggregation of two other {@code FeatureSet} resources,
+      * then this method can be invoked twice with the metadata of each source {@code FeatureSet}.
+      * If the aggregated data are features, then {@code level} should be {@link ScopeCode#FEATURE}.</div>
+      *
+      * @param  metadata  the metadata of the source, or {@code null} if none.
+      * @param  level     hierarchical level of the source (e.g. feature). Should not be null.
+      * @param  features  names of dataset, features or attributes used in the source.
+      */
+     public final void addSource(final Metadata metadata, final ScopeCode level, final CharSequence... features) {
+         if (metadata != null) {
+             final DefaultSource source = new DefaultSource();
+             final DefaultScope scope = new DefaultScope(level);
+             source.setSourceReferenceSystem(CollectionsExt.first(metadata.getReferenceSystemInfo()));
+             for (final Identification id : metadata.getIdentificationInfo()) {
+                 source.setSourceCitation(id.getCitation());
 -                source.setSourceSpatialResolution(CollectionsExt.first(id.getSpatialResolutions()));
 -                scope.setExtents(id.getExtents());
++                if (id instanceof AbstractIdentification) {
++                    final AbstractIdentification aid = (AbstractIdentification) id;
++                    source.setSourceSpatialResolution(CollectionsExt.first(aid.getSpatialResolutions()));
++                    scope.setExtents(aid.getExtents());
++                }
+                 if (features != null && features.length != 0) {
+                     /*
+                      * Note: the same ScopeDescription may be shared by many Source instances
+                      * in the common case where many sources contain features of the same type.
+                      */
+                     final DefaultScopeDescription sd = new DefaultScopeDescription();
+                     sd.setLevelDescription(level, new LinkedHashSet<>(Arrays.asList(features)));
+                     scope.getLevelDescription().add(shared(DefaultScopeDescription.class, sd));
+                 }
+                 source.setScope(scope.isEmpty() ? null : scope);
+                 if (!source.isEmpty()) {
+                     addIfNotPresent(lineage().getSources(), source);
+                     break;
+                 }
+             }
+         }
+     }
+ 
+     /**
       * Adds information about the procedure, process and algorithm applied in a process step.
       * If a processing was already defined with a different identifier, then a new processing
       * instance will be created. Storage location is:
diff --cc storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/FeatureSubset.java
index c327d87,6332215..3afeaf2
--- a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/FeatureSubset.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/query/FeatureSubset.java
@@@ -16,8 -16,11 +16,9 @@@
   */
  package org.apache.sis.internal.storage.query;
  
 -import java.util.List;
+ import java.util.Optional;
  import java.util.stream.Stream;
  import org.opengis.util.GenericName;
 -import org.apache.sis.internal.feature.FeatureUtilities;
  import org.apache.sis.internal.storage.AbstractFeatureSet;
  import org.apache.sis.storage.DataStoreException;
  import org.apache.sis.storage.FeatureSet;
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
index a6b30b3,07d371a..53461b3
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java
@@@ -320,12 -320,12 +323,12 @@@ public abstract class DataStore impleme
                  }
              }
              if (citation != null) {
 -                Identifier first = null;
 +                ReferenceIdentifier first = null;
                  for (final Identifier c : citation.getIdentifiers()) {
                      if (c instanceof GenericName) {
-                         return (GenericName) c;
+                         return Optional.of((GenericName) c);
 -                    } else if (first == null) {
 -                        first = c;
 +                    } else if (first == null && c instanceof ReferenceIdentifier) {
 +                        first = (ReferenceIdentifier) c;
                      }
                  }
                  if (first != null) {
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSet.java
index 0b817ad,105e7f1..69c8f04
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSet.java
@@@ -18,10 -18,11 +18,11 @@@ package org.apache.sis.storage
  
  import java.util.stream.Stream;
  import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.internal.storage.query.SimpleQuery;
  
  // Branch-dependent imports
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.DefaultFeatureType;
  
  
  /**