sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1751821 - in /sis/branches/JDK8: core/sis-feature/src/main/java/org/apache/sis/internal/feature/ core/sis-feature/src/test/java/org/apache/sis/internal/feature/ storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/
Date Thu, 07 Jul 2016 17:50:09 GMT
Author: desruisseaux
Date: Thu Jul  7 17:50:09 2016
New Revision: 1751821

URL: http://svn.apache.org/viewvc?rev=1751821&view=rev
Log:
Review FeatureTypeBuilder API:
- separate the Association and Attribute cases
- replace addDefaultGeometry and addIdentifier by a setRole(AttributeRole) method
- provide a default property name based on the type

Modified:
    sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Builder.java
    sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureTypeBuilder.java
    sis/branches/JDK8/core/sis-feature/src/test/java/org/apache/sis/internal/feature/FeatureTypeBuilderTest.java
    sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXConstants.java

Modified: sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Builder.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Builder.java?rev=1751821&r1=1751820&r2=1751821&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Builder.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Builder.java [UTF-8] Thu Jul  7 17:50:09 2016
@@ -16,12 +16,17 @@
  */
 package org.apache.sis.internal.feature;
 
-import java.util.HashMap;
 import java.util.Map;
+import java.util.HashMap;
+import java.util.Locale;
 import org.opengis.util.GenericName;
 import org.apache.sis.feature.AbstractIdentifiedType;
 import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.NullArgumentException;
+import org.apache.sis.util.Localized;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.Debug;
 
 
 /**
@@ -34,20 +39,39 @@ import org.apache.sis.util.ArgumentCheck
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.7
- * @version 0.7
+ * @version 0.8
  * @module
  */
-abstract class Builder<T extends Builder<T>> {
+abstract class Builder<T extends Builder<T>> implements Localized {
     /**
      * The feature name, definition, designation and description.
      * The name is mandatory; all other information are optional.
      */
-    final Map<String,Object> identification = new HashMap<>(4);
+    private final Map<String,Object> identification = new HashMap<>(4);
+
+    /**
+     * Creates a new builder instance which will format error message using the given locale.
+     */
+    Builder(final Locale locale) {
+        setLocale(locale);
+    }
 
     /**
      * Creates a new builder instance.
      */
-    Builder() {
+    Builder(final Builder<?> parent) {
+        setLocale(parent.identification.get(Errors.LOCALE_KEY));
+    }
+
+    /**
+     * Sets the locale if non-null. This method should be invoked only when the {@link #identification} map is empty.
+     *
+     * @see #getLocale()
+     */
+    private void setLocale(final Object locale) {
+        if (locale != null) {
+            identification.put(Errors.LOCALE_KEY, locale);
+        }
     }
 
     /**
@@ -58,7 +82,9 @@ abstract class Builder<T extends Builder
      */
     @SuppressWarnings("unchecked")
     public T clear() {
+        final Object locale = identification.get(Errors.LOCALE_KEY);
         identification.clear();
+        setLocale(locale);
         return (T) this;
     }
 
@@ -73,7 +99,42 @@ abstract class Builder<T extends Builder
     abstract GenericName name(String scope, String localPart);
 
     /**
-     * Sets the feature type name as a simple string with the default scope.
+     * Returns a default name to use if the user did not specified a name. The first letter will be changed to
+     * lower case (unless the name looks like an acronym) for compliance with Java convention on property names.
+     */
+    String getDefaultName() {
+        return null;
+    }
+
+    /**
+     * Returns the map of properties to give to the {@code FeatureType} or {@code PropertyType} constructor.
+     * If the map does not contains a name, a default name may be generated.
+     */
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    final Map<String,Object> identification() {
+        if (identification.get(AbstractIdentifiedType.NAME_KEY) == null) {
+            String name = getDefaultName();
+            if (name != null) {
+                final int length = name.length();
+                if (length != 0) {
+                    final int c  = name.codePointAt(0);
+                    final int lc = Character.toLowerCase(c);
+                    if (c != lc) {
+                        final int n = Character.charCount(c);
+                        if (n >= length || Character.isLowerCase(name.codePointAt(n))) {
+                            final StringBuilder buffer = new StringBuilder(length);
+                            name = buffer.appendCodePoint(lc).append(name, n, length).toString();
+                        }
+                    }
+                    identification.put(AbstractIdentifiedType.NAME_KEY, name(null, name));
+                }
+            }
+        }
+        return identification;
+    }
+
+    /**
+     * Sets the name as a simple string with the default scope.
      * The default scope is the value specified by the last call to
      * {@link FeatureTypeBuilder#setDefaultScope(String)}.
      *
@@ -87,12 +148,12 @@ abstract class Builder<T extends Builder
      * @return {@code this} for allowing method calls chaining.
      */
     public T setName(String localPart) {
-        ArgumentChecks.ensureNonEmpty("localPart", localPart);
+        ensureNonEmpty("localPart", localPart);
         return setName(name(null, localPart));
     }
 
     /**
-     * Sets the feature type name as a string in the given scope.
+     * Sets the name as a string in the given scope.
      * If a {@linkplain FeatureTypeBuilder#setDefaultScope(String) default scope} was specified,
      * this method override it.
      *
@@ -107,7 +168,7 @@ abstract class Builder<T extends Builder
      * @return {@code this} for allowing method calls chaining.
      */
     public T setName(String scope, String localPart) {
-        ArgumentChecks.ensureNonEmpty("localPart", localPart);
+        ensureNonEmpty("localPart", localPart);
         if (scope == null) {
             scope = "";                                 // For preventing the use of default scope.
         }
@@ -115,7 +176,7 @@ abstract class Builder<T extends Builder
     }
 
     /**
-     * Sets the feature type name as a generic name.
+     * Sets the name as a generic name.
      * If another name was defined before this method call, that previous value will be discarded.
      *
      * <div class="note"><b>Note for subclasses:</b>
@@ -129,19 +190,20 @@ abstract class Builder<T extends Builder
      */
     @SuppressWarnings("unchecked")
     public T setName(GenericName name) {
-        ArgumentChecks.ensureNonNull("name", name);
+        ensureNonNull("name", name);
         identification.put(AbstractIdentifiedType.NAME_KEY, name);
         return (T) this;
     }
 
     /**
-     * Returns the current {@code FeatureType} name, or {@code null} if undefined.
-     * This method returns the value built from the last call to a {@code setName(…)} method.
+     * Returns the current name, or {@code null} if undefined.
+     * This method returns the value built from the last call to a {@code setName(…)} method,
+     * or a default name or {@code null} if no name has been explicitely specified.
      *
-     * @return the current {@code FeatureType} name, or {@code null} if the name has not yet been specified.
+     * @return the current name (may be a default name or {@code null}).
      */
     public GenericName getName() {
-        return (GenericName) identification.get(AbstractIdentifiedType.NAME_KEY);
+        return (GenericName) identification().get(AbstractIdentifiedType.NAME_KEY);
     }
 
     /**
@@ -195,4 +257,74 @@ abstract class Builder<T extends Builder
         identification.put(AbstractIdentifiedType.DESCRIPTION_KEY, description);
         return (T) this;
     }
+
+    /**
+     * Returns the locale used for formatting error messages, or {@code null} if unspecified.
+     * If unspecified, the system default locale will be used.
+     *
+     * @return the locale used for formatting error messages, or {@code null} if unspecified.
+     */
+    @Override
+    public Locale getLocale() {
+        return (Locale) identification.get(Errors.LOCALE_KEY);
+    }
+
+    /**
+     * Returns a string representation of this object.
+     * The returned string is for debugging purpose only and may change in any future SIS version.
+     *
+     * @return a string representation of this object for debugging purpose.
+     */
+    @Debug
+    @Override
+    public String toString() {
+        final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this));
+        toStringInternal(buffer.append("[“").append(getDisplayName()).append('”'));
+        return buffer.append(']').toString();
+    }
+
+    /**
+     * Appends a text inside the value returned by {@link #toString()}, before the closing bracket.
+     */
+    void toStringInternal(StringBuilder buffer) {
+    }
+
+    /**
+     * Returns the resources for error messages.
+     */
+    final Errors errors() {
+        return Errors.getResources(identification);
+    }
+
+    /**
+     * Same as {@link org.apache.sis.util.ArgumentChecks#ensureNonNull(String, Object)},
+     * but uses the current locale in case of error.
+     *
+     * @param  name the name of the argument to be checked. Used only if an exception is thrown.
+     * @param  object the user argument to check against null value.
+     * @throws NullArgumentException if {@code object} is null.
+     */
+    final void ensureNonNull(final String name, final Object value) {
+        if (value == null) {
+            throw new NullArgumentException(errors().getString(Errors.Keys.NullArgument_1, name));
+        }
+    }
+
+    /**
+     * Same as {@link org.apache.sis.util.ArgumentChecks#ensureNonEmpty(String, CharSequence)},
+     * but uses the current locale in case of error.
+     *
+     * @param  name the name of the argument to be checked. Used only if an exception is thrown.
+     * @param  text the user argument to check against null value and empty sequences.
+     * @throws NullArgumentException if {@code text} is null.
+     * @throws IllegalArgumentException if {@code text} is empty.
+     */
+    final void ensureNonEmpty(final String name, final String text) {
+        if (text == null) {
+            throw new NullArgumentException(errors().getString(Errors.Keys.NullArgument_1, name));
+        }
+        if (text.length() == 0) {
+            throw new IllegalArgumentException(errors().getString(Errors.Keys.EmptyArgument_1, name));
+        }
+    }
 }

Modified: sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureTypeBuilder.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureTypeBuilder.java?rev=1751821&r1=1751820&r2=1751821&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureTypeBuilder.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureTypeBuilder.java [UTF-8] Thu Jul  7 17:50:09 2016
@@ -22,6 +22,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.Locale;
 import org.opengis.util.GenericName;
 import org.opengis.util.NameFactory;
 import org.opengis.util.FactoryException;
@@ -33,10 +34,9 @@ import org.apache.sis.feature.DefaultFea
 import org.apache.sis.feature.FeatureOperations;
 import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.internal.util.CollectionsExt;
-import org.apache.sis.util.NullArgumentException;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Classes;
 
 // Branch-dependent imports
 import org.opengis.feature.AttributeType;
@@ -47,8 +47,9 @@ import org.opengis.feature.PropertyType;
 
 /**
  * Helper class for the creation of {@link FeatureType} instances.
- * This builder can create the parameters to be given to the {@linkplain DefaultFeatureType#DefaultFeatureType
- * feature type constructor} from simpler parameters given to this builder.
+ * This builder can create the arguments to be given to the
+ * {@linkplain DefaultFeatureType#DefaultFeatureType feature type constructor}
+ * from simpler parameters given to this builder.
  *
  * <p>{@code FeatureTypeBuilder} should be short lived.
  * After the {@code FeatureType} has been created, the builder should be discarded.</p>
@@ -56,19 +57,36 @@ import org.opengis.feature.PropertyType;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.7
- * @version 0.7
+ * @version 0.8
  * @module
  *
  * @see org.apache.sis.parameter.ParameterBuilder
  */
 public class FeatureTypeBuilder extends Builder<FeatureTypeBuilder> {
     /**
+     * Index of the property where to store the identifier generated by {@link #build()}.
+     */
+    private static final int IDENTIFIER_INDEX = 0;
+
+    /**
+     * Index of the property where to store the envelope generated by {@link #build()}.
+     * Must follow {@link #IDENTIFIER_INDEX}.
+     */
+    private static final int ENVELOPE_INDEX = 1;
+
+    /**
+     * Index of the property where to store the geometry generated by {@link #build()}.
+     * Must follow {@link #ENVELOPE_INDEX}.
+     */
+    private static final int GEOMETRY_INDEX = 2;
+
+    /**
      * The factory to use for creating names.
      */
     private final NameFactory nameFactory;
 
     /**
-     * Builders for the properties of this feature.
+     * Builders for the properties (attributes, associations or operations) of this feature.
      */
     private final List<Property<?>> properties;
 
@@ -104,48 +122,42 @@ public class FeatureTypeBuilder extends
     private int defaultMaximumOccurs;
 
     /**
-     * If {@link #idAttributes} is non-null, an optional prefix or suffix to insert before
-     * or after the {@linkplain FeatureOperations#compound compound key} named {@code "@identifier"}.
+     * An optional prefix or suffix to insert before or after the {@linkplain FeatureOperations#compound compound key}
+     * named {@code "@identifier"}.
      */
     private String idPrefix, idSuffix;
 
     /**
-     * If {@link #idAttributes} is non-null and contains more than one value, the separator to insert between
-     * each single component in a {@linkplain FeatureOperations#compound compound key} named {@code "@identifier"}.
+     * The separator to insert between each single component in a {@linkplain FeatureOperations#compound compound key}
+     * named {@code "@identifier"}.
      */
     private String idDelimiter;
 
     /**
-     * The attributes to use in a {@linkplain FeatureOperations#compound compound key} named {@code "@identifier"},
-     * or {@code null} if none. If this array contains only one property and {@link #idPrefix} is null,
-     * then {@code "@identifier"} will be a {@linkplain FeatureOperations#link link} to {@code idAttributes[0]}.
-     */
-    private final List<Property<?>> idAttributes;
-
-    /**
      * The default geometry attribute, or {@code null} if none.
      *
      * @see AttributeConvention#GEOMETRY_PROPERTY
      */
-    private Property<?> defaultGeometry;
+    private Attribute<?> defaultGeometry;
 
     /**
      * Creates a new builder instance using the default name factory.
      */
     public FeatureTypeBuilder() {
-        this(DefaultFactories.forBuildin(NameFactory.class));
+        this(DefaultFactories.forBuildin(NameFactory.class), null);
     }
 
     /**
      * Creates a new builder instance using the given name factory.
      *
      * @param factory  the factory to use for creating names.
+     * @param locale   the locale to use for formatting error messages, or {@code null} for the default locale.
      */
-    public FeatureTypeBuilder(final NameFactory factory) {
+    public FeatureTypeBuilder(final NameFactory factory, final Locale locale) {
+        super(locale);
         nameFactory  = factory;
         properties   = new ArrayList<>();
         superTypes   = new ArrayList<>();
-        idAttributes = new ArrayList<>();
         idDelimiter  = ":";
         defaultMinimumOccurs = 1;
         defaultMaximumOccurs = 1;
@@ -160,9 +172,8 @@ public class FeatureTypeBuilder extends
     @Override
     public FeatureTypeBuilder clear() {
         super.clear();
-        properties  .clear();
-        superTypes  .clear();
-        idAttributes.clear();
+        properties.clear();
+        superTypes.clear();
         idDelimiter     = ":";
         idPrefix        = null;
         idSuffix        = null;
@@ -193,7 +204,7 @@ public class FeatureTypeBuilder extends
      * @return {@code this} for allowing method calls chaining.
      */
     public FeatureTypeBuilder setSuperTypes(final FeatureType... parents) {
-        ArgumentChecks.ensureNonNull("parents", parents);
+        ensureNonNull("parents", parents);
         superTypes.clear();
         superTypes.addAll(Arrays.asList(parents));
         return this;
@@ -221,7 +232,7 @@ public class FeatureTypeBuilder extends
      * @param  maximumOccurs  new default maximum number of property values.
      * @return {@code this} for allowing method calls chaining.
      *
-     * @see Property#setCardinality(int, int)
+     * @see Attribute#setCardinality(int, int)
      */
     public FeatureTypeBuilder setDefaultCardinality(final int minimumOccurs, final int maximumOccurs) {
         if (minimumOccurs < 0 || maximumOccurs < minimumOccurs) {
@@ -234,7 +245,8 @@ public class FeatureTypeBuilder extends
 
     /**
      * Sets the prefix, suffix and delimiter to use when formatting a compound identifier made of two or more attributes.
-     * The strings specified to this method will be used only if {@link #addIdentifier(Class)} is invoked more than once.
+     * The delimiter will be used only if at least two attributes have the {@linkplain AttributeRole#IDENTIFIER_COMPONENT
+     * identifier component role}.
      *
      * <p>If this method is not invoked, then the default values are the {@code ":"} delimiter and no prefix or suffix.</p>
      *
@@ -243,11 +255,11 @@ public class FeatureTypeBuilder extends
      * @param  suffix     characters to use at the end of the concatenated string, or {@code null} if none.
      * @return {@code this} for allowing method calls chaining.
      *
-     * @see java.util.StringJoiner
+     * @see AttributeRole#IDENTIFIER_COMPONENT
      * @see FeatureOperations#compound(Map, String, String, String, PropertyType...)
      */
     public FeatureTypeBuilder setIdentifierDelimiters(final String delimiter, final String prefix, final String suffix) {
-        ArgumentChecks.ensureNonEmpty("delimiter", delimiter);
+        ensureNonEmpty("delimiter", delimiter);
         idDelimiter = delimiter;
         idPrefix    = prefix;
         idSuffix    = suffix;
@@ -255,106 +267,44 @@ public class FeatureTypeBuilder extends
     }
 
     /**
-     * Creates a new {@code AttributeType} builder for values of the given class which will be used as identifiers.
-     * An arbitrary amount of attributes can be specified as identifiers:
-     *
-     * <ul>
-     *   <li>If this method is never invoked, no attribute is marked as feature identifier.</li>
-     *   <li>If this method is invoked exactly once, then a new attribute is created in the same way than
-     *       {@link #addAttribute(Class)} and a synthetic attribute named {@code "@identifier"}
-     *       will be created as a {@linkplain FeatureOperations#link link} to the new attribute.</li>
-     *   <li>If this method is invoked more than once, then new attributes are created in the same way than
-     *       {@link #addAttribute(Class)} and a synthetic attribute named {@code "@identifier"} will be created
-     *       as a {@linkplain FeatureOperations#compound compound key} made of all identifiers.</li>
-     * </ul>
-     *
-     * Callers shall invoke at least one of the {@code Property.setName(…)} methods on the returned instance.
-     * All other methods are optional.
-     *
-     * @param  <V>  the compile-time value of {@code valueClass} argument.
-     * @param  valueClass  the class of attribute values.
-     * @return a builder for an {@code AttributeType}.
-     */
-    public <V> Property<V> addIdentifier(final Class<V> valueClass) {
-        ensureAttributeType(valueClass);
-        final Property<V> property = new Property<>(valueClass);
-        idAttributes.add(property);
-        properties.add(property);
-        return property;
-    }
-
-    /**
-     * Creates a new {@code AttributeType} builder for a geometry which will be flagged as the default geometry.
-     * Callers shall invoke at least one of the {@code Property.setName(…)} methods on the returned instance.
-     * All other methods are optional.
-     *
-     * @param  <V>  the compile-time value of {@code valueClass} argument.
-     * @param  valueClass  the geometry class of attribute values.
-     * @return a builder for an {@code AttributeType} which will contain the default geometry.
-     * @throws IllegalArgumentException if the given type is not a supported geometry type.
-     * @throws IllegalStateException if a default geometry has already been specified to this builder.
-     */
-    public <V> Property<V> addDefaultGeometry(final Class<V> valueClass) {
-        ensureAttributeType(valueClass);
-        if (!Geometries.isKnownType(valueClass)) {
-            throw new IllegalArgumentException(errors().getString(Errors.Keys.UnsupportedImplementation_1, valueClass));
-        }
-        if (defaultGeometry != null) {
-            throw new IllegalStateException(errors().getString(Errors.Keys.PropertyAlreadyExists_2,
-                    getDisplayName(), AttributeConvention.GEOMETRY_PROPERTY));
-        }
-        final Property<V> property = new Property<>(valueClass);
-        defaultGeometry = property;
-        properties.add(property);
-        return property;
-    }
-
-    /**
      * Creates a new {@code AttributeType} builder for values of the given class.
-     * Callers shall invoke at least one of the {@code Property.setName(…)} methods on the returned instance.
-     * All other methods are optional.
+     * The default attribute name is the name of the given type, but callers should invoke one
+     * of the {@code Attribute.setName(…)} methods on the returned instance with a better name.
      *
      * <p>Usage example:</p>
      * {@preformat java
-     *     builder.addAttribute(String.class).setName("City");
+     *     builder.addAttribute(String.class).setName("City").setDefaultValue("Metropolis");
      * }
      *
+     * The value class can not be {@code Feature.class} since features shall be handled
+     * as {@linkplain #addAssociation(FeatureType) associations} instead than attributes.
+     *
      * @param  <V>  the compile-time value of {@code valueClass} argument.
-     * @param  valueClass  the class of attribute values.
+     * @param  valueClass  the class of attribute values (can not be {@code Feature.class}).
      * @return a builder for an {@code AttributeType}.
      */
-    public <V> Property<V> addAttribute(final Class<V> valueClass) {
-        ensureAttributeType(valueClass);
-        final Property<V> property = new Property<>(valueClass);
-        properties.add(property);
-        return property;
-    }
-
-    /**
-     * Ensures that the given value class is not null and not assignable to {@code Feature}.
-     * We disallow {@code Feature.class} because those type shall be handled as associations
-     * instead than attributes.
-     */
-    private void ensureAttributeType(final Class<?> valueClass) {
-        if (valueClass == null) {
-            throw new NullArgumentException(errors().getString(Errors.Keys.NullArgument_1, "valueClass"));
-        }
+    public <V> Attribute<V> addAttribute(final Class<V> valueClass) {
+        ensureNonNull("valueClass", valueClass);
         if (Feature.class.isAssignableFrom(valueClass)) {
+            // We disallow Feature.class because that type shall be handled as association instead than attribute.
             throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalArgumentValue_2, "valueClass", valueClass));
         }
+        final Attribute<V> property = new Attribute<>(valueClass);
+        properties.add(property);
+        return property;
     }
 
     /**
      * Creates a new {@code FeatureAssociationRole} builder for features of the given type.
-     * Callers shall invoke at least one of the {@code Property.setName(…)} methods on the returned instance.
-     * All other methods are optional.
+     * The default association name is the name of the given type, but callers should invoke one
+     * of the {@code Association.setName(…)} methods on the returned instance with a better name.
      *
      * @param  type  the type of feature values.
      * @return a builder for a {@code FeatureAssociationRole}.
      */
-    public Property<Feature> addAssociation(final FeatureType type) {
-        ArgumentChecks.ensureNonNull("type", type);
-        final Property<Feature> property = new Property<>(Feature.class, FeatureType.class, type);
+    public Association addAssociation(final FeatureType type) {
+        ensureNonNull("type", type);
+        final Association property = new Association(type, type.getName());
         properties.add(property);
         return property;
     }
@@ -367,113 +317,266 @@ public class FeatureTypeBuilder extends
      * @param  type  the name of the type of feature values.
      * @return a builder for a {@code FeatureAssociationRole}.
      */
-    public Property<Feature> addAssociation(final GenericName type) {
-        ArgumentChecks.ensureNonNull("type", type);
-        final Property<Feature> property = new Property<>(Feature.class, GenericName.class, type);
+    public Association addAssociation(final GenericName type) {
+        ensureNonNull("type", type);
+        final Association property = new Association(null, type);
         properties.add(property);
         return property;
     }
 
+
+
+
     /**
      * Describes one property of the {@code FeatureType} to be built by the enclosing {@code FeatureTypeBuilder}.
      * A different instance of {@code Property} exists for each property to describe. Those instances are created by:
      *
      * <ul>
-     *   <li>{@link FeatureTypeBuilder#addIdentifier(Class)}</li>
-     *   <li>{@link FeatureTypeBuilder#addDefaultGeometry(Class)}</li>
      *   <li>{@link FeatureTypeBuilder#addAttribute(Class)}</li>
      *   <li>{@link FeatureTypeBuilder#addAssociation(FeatureType)}</li>
      *   <li>{@link FeatureTypeBuilder#addAssociation(GenericName)}</li>
      * </ul>
      *
-     * @param <V> the class of property values.
+     * @param <T> the property subclass. It is subclass responsibility to ensure that {@code this}
+     *            is assignable to {@code <T>}; this {@code Property} class can not verify that.
      */
-    public final class Property<V> extends Builder<Property<V>> {
-        /**
-         * The class of property values. Can not be changed after construction
-         * because this value determines the parameterized type {@code <V>}.
-         */
-        private final Class<V> valueClass;
-
-        /**
-         * The default value for the property, or {@code null} if none.
-         */
-        private V defaultValue;
-
+    abstract class Property<T extends Property<T>> extends Builder<T> {
         /**
          * The minimum number of property values.
          * The default value is 1, unless otherwise specified by {@link #setDefaultCardinality(int, int)}.
          *
          * @see #setCardinality(int, int)
          */
-        private int minimumOccurs;
+        int minimumOccurs;
 
         /**
-         * The maximum number of property values. The default value is 1.
+         * The maximum number of property values.
          * The default value is 1, unless otherwise specified by {@link #setDefaultCardinality(int, int)}.
          *
          * @see #setCardinality(int, int)
          */
-        private int maximumOccurs;
+        int maximumOccurs;
 
         /**
-         * Builders for the characteristics associated to the attribute.
+         * Creates a new property initialized to the default cardinality.
          */
-        private final List<Characteristic<?>> characteristics;
+        Property() {
+            super(FeatureTypeBuilder.this);
+            minimumOccurs = defaultMinimumOccurs;
+            maximumOccurs = defaultMaximumOccurs;
+        }
 
         /**
-         * Creates a new {@code AttributeType} or {@code Operation} builder for values of the given class.
+         * Sets the minimum and maximum number of property values. Those numbers must be equal or greater than zero.
          *
-         * @param valueClass the class of property values.
+         * <p>If this method is not invoked, then the default values are the cardinality specified by the last call
+         * to {@link FeatureTypeBuilder#setDefaultCardinality(int, int)} at the time this instance has been created.
+         * If the later method has not been invoked, then the default cardinality is [1 … 1].</p>
+         *
+         * @param  minimumOccurs  new minimum number of property values.
+         * @param  maximumOccurs  new maximum number of property values.
+         * @return {@code this} for allowing method calls chaining.
          */
-        Property(final Class<V> valueClass) {
-            this.valueClass = valueClass;
-            minimumOccurs   = defaultMinimumOccurs;
-            maximumOccurs   = defaultMaximumOccurs;
-            characteristics = new ArrayList<>();
+        @SuppressWarnings("unchecked")
+        public T setCardinality(final int minimumOccurs, final int maximumOccurs) {
+            if (minimumOccurs < 0 || maximumOccurs < minimumOccurs) {
+                throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalRange_2, minimumOccurs, maximumOccurs));
+            }
+            this.minimumOccurs = minimumOccurs;
+            this.maximumOccurs = maximumOccurs;
+            return (T) this;
         }
 
         /**
+         * Delegates the creation of a new name to the enclosing builder.
+         */
+        @Override
+        final GenericName name(final String scope, final String localPart) {
+            return FeatureTypeBuilder.this.name(scope, localPart);
+        }
+
+        /**
+         * Returns {@code true} if {@link AttributeRole#IDENTIFIER_COMPONENT} has been associated to this property.
+         */
+        boolean isIdentifier() {
+            return false;
+        }
+
+        /**
+         * Creates a new property type from the current setting.
+         */
+        abstract PropertyType build();
+    }
+
+
+
+
+    /**
+     * Describes one association from the {@code FeatureType} to be built by the enclosing {@code FeatureTypeBuilder}
+     * to another {@code FeatureType}. A different instance of {@code Association} exists for each feature association
+     * to describe. Those instances are created preferably by {@link FeatureTypeBuilder#addAssociation(FeatureType)},
+     * or in case of cyclic reference by {@link FeatureTypeBuilder#addAssociation(GenericName)}.
+     *
+     * @see FeatureTypeBuilder#addAssociation(FeatureType)
+     * @see FeatureTypeBuilder#addAssociation(GenericName)
+     */
+    public final class Association extends Property<Association> {
+        /**
+         * The target feature type, or {@code null} if unknown.
+         */
+        private final FeatureType type;
+
+        /**
+         * Name of the target feature type (never null).
+         */
+        private final GenericName typeName;
+
+        /**
          * Creates a new {@code AssociationRole} builder for values of the given type.
-         * This constructor arbitrarily stores the feature type as an unnamed characteristic of this property.
-         *
-         * @param valueClass shall be {@code Feature.class}.
-         * @param typeClass  shall be either {@code FeatureType.class} or {@code GenericName.class}.
-         * @param type       the type of associated features.
+         * The {@code type} argument can be null if unknown, but {@code typeName} is mandatory.
          */
-        <C> Property(final Class<V> valueClass, final Class<C> typeClass, final C type) {
-            this.valueClass = valueClass;
-            minimumOccurs   = defaultMinimumOccurs;
-            maximumOccurs   = defaultMaximumOccurs;
-            characteristics = Collections.singletonList(new Characteristic<>(typeClass).setDefaultValue(type));
+        Association(final FeatureType type, final GenericName typeName) {
+            this.type     = type;
+            this.typeName = typeName;
         }
 
         /**
-         * Delegates the creation of a new name to the enclosing builder.
+         * Returns a default name to use if the user did not specified a name. The first letter will be changed to
+         * lower case (unless the name looks like an acronym) for compliance with Java convention on property names.
          */
         @Override
-        final GenericName name(final String scope, final String localPart) {
-            return FeatureTypeBuilder.this.name(scope, localPart);
+        final String getDefaultName() {
+            return typeName.tip().toString();
         }
 
         /**
-         * Sets the minimum and maximum number of property values. Those numbers must be equal or greater than zero.
+         * Creates a new property type from the current setting.
+         */
+        @Override
+        final PropertyType build() {
+            final PropertyType property;
+            if (type != null) {
+                property = new DefaultAssociationRole(identification(), type, minimumOccurs, maximumOccurs);
+            } else {
+                property = new DefaultAssociationRole(identification(), typeName, minimumOccurs, maximumOccurs);
+            }
+            return property;
+        }
+
+        /**
+         * Appends a text inside the value returned by {@link #toString()}, before the closing bracket.
+         */
+        @Override
+        final void toStringInternal(final StringBuilder buffer) {
+            buffer.append(" → ").append(typeName);
+        }
+    }
+
+
+
+
+    /**
+     * Roles that can be associated to some attributes for instructing {@code FeatureTypeBuilder}
+     * how to generate pre-defined operations. Those pre-defined operations are:
+     *
+     * <ul>
+     *   <li>A {@linkplain FeatureOperations#compound compound operation} for generating a unique identifier
+     *       from an arbitrary amount of attribute values.</li>
+     *   <li>A {@linkplain FeatureOperations#link link operation} for referencing a geometry to be used as the
+     *       <em>default</em> geometry.</li>
+     *   <li>An {@linkplain FeatureOperations#envelope operation} for computing the bounding box of all geometries
+     *       found in the feature. This operation is automatically added if the feature contains a default geometry.</li>
+     * </ul>
+     *
+     * This enumeration allows user code to specify which feature attribute to use for creating those operations.
+     *
+     * @see Attribute#addRole(AttributeRole)
+     */
+    public static enum AttributeRole {
+        /**
+         * Attribute value will be part of a unique identifier for the feature instance.
+         * An arbitrary amount of attributes can be flagged as identifier components:
          *
-         * <p>If this method is not invoked, then the default values are the cardinality specified by the last call
-         * to {@link #setDefaultCardinality(int, int)} at the time this {@code Property} instance has been created.
-         * If the later method has not invoked neither, then the default cardinality is [1 … 1].</p>
+         * <ul>
+         *   <li>If no attribute has this role, then no attribute is marked as feature identifier.</li>
+         *   <li>If exactly one attribute has this role, then a synthetic attribute named {@code "@identifier"}
+         *       will be created as a {@linkplain FeatureOperations#link link} to the flagged attribute.</li>
+         *   <li>If more than one attribute have this role, then a synthetic attribute named {@code "@identifier"}
+         *       will be created as a {@linkplain FeatureOperations#compound compound key} made of all flagged
+         *       attributes. The separator character can be modified by a call to
+         *       {@link FeatureTypeBuilder#setIdentifierDelimiters(String, String, String)}</li>
+         * </ul>
          *
-         * @param  minimumOccurs  new minimum number of property values.
-         * @param  maximumOccurs  new maximum number of property values.
-         * @return {@code this} for allowing method calls chaining.
+         * @see FeatureTypeBuilder#setIdentifierDelimiters(String, String, String)
          */
-        public Property<V> setCardinality(final int minimumOccurs, final int maximumOccurs) {
-            if (minimumOccurs < 0 || maximumOccurs < minimumOccurs) {
-                throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalRange_2, minimumOccurs, maximumOccurs));
-            }
-            this.minimumOccurs = minimumOccurs;
-            this.maximumOccurs = maximumOccurs;
-            return this;
+        IDENTIFIER_COMPONENT,
+
+        /**
+         * Attribute value will be flagged as the <em>default</em> geometry.
+         * Feature can have an arbitrary amount of geometry attributes,
+         * but only one can be flagged as the default geometry.
+         */
+        DEFAULT_GEOMETRY
+    }
+
+
+
+
+    /**
+     * Describes one attribute of the {@code FeatureType} to be built by the enclosing {@code FeatureTypeBuilder}.
+     * A different instance of {@code Attribute} exists for each feature attribute to describe.
+     * Those instances are created by {@link FeatureTypeBuilder#addAttribute(Class)}.
+     *
+     * @param <V> the class of property values.
+     *
+     * @see FeatureTypeBuilder#addAttribute(Class)
+     */
+    public final class Attribute<V> extends Property<Attribute<V>> {
+        /**
+         * The class of property values. Can not be changed after construction
+         * because this value determines the parameterized type {@code <V>}.
+         */
+        private final Class<V> valueClass;
+
+        /**
+         * The default value for the property, or {@code null} if none.
+         */
+        private V defaultValue;
+
+        /**
+         * Whether this attribute will be used in a {@linkplain FeatureOperations#compound compound key} named
+         * {@code "@identifier"}. If only one attribute has this flag and {@link FeatureTypeBuilder#idPrefix} and
+         * {@code isSuffix} are null, then {@code "@identifier"} will be a {@linkplain FeatureOperations#link link}
+         * to {@code idAttributes[0]}.
+         *
+         * @see #addRole(AttributeRole)
+         */
+        private boolean isIdentifier;
+
+        /**
+         * Builders for the characteristics associated to the attribute.
+         */
+        private final List<Characteristic<?>> characteristics;
+
+        /**
+         * Creates a new {@code AttributeType} or {@code Operation} builder for values of the given class.
+         *
+         * @param valueClass the class of property values.
+         */
+        Attribute(final Class<V> valueClass) {
+            this.valueClass = valueClass;
+            minimumOccurs   = defaultMinimumOccurs;
+            maximumOccurs   = defaultMaximumOccurs;
+            characteristics = new ArrayList<>();
+        }
+
+        /**
+         * Returns a default name to use if the user did not specified a name. The first letter will be changed to
+         * lower case (unless the name looks like an acronym) for compliance with Java convention on property names.
+         */
+        @Override
+        final String getDefaultName() {
+            return Classes.getShortName(valueClass);
         }
 
         /**
@@ -482,7 +585,7 @@ public class FeatureTypeBuilder extends
          * @param  defaultValue  default property value, or {@code null} if none.
          * @return {@code this} for allowing method calls chaining.
          */
-        public Property<V> setDefaultValue(final V defaultValue) {
+        public Attribute<V> setDefaultValue(final V defaultValue) {
             this.defaultValue = defaultValue;
             return this;
         }
@@ -500,7 +603,7 @@ public class FeatureTypeBuilder extends
          * @see AttributeConvention#VALID_VALUES_CHARACTERISTIC
          */
         @SafeVarargs
-        public final Property<V> setValidValues(final V... values) {
+        public final Attribute<V> setValidValues(final V... values) {
             return setCharacteristic(AttributeConvention.VALID_VALUES_CHARACTERISTIC,
                     Set.class, CollectionsExt.immutableSet(false, values));
         }
@@ -519,7 +622,7 @@ public class FeatureTypeBuilder extends
          *
          * @see AttributeConvention#MAXIMAL_LENGTH_CHARACTERISTIC
          */
-        public Property<V> setMaximalLengthCharacteristic(final Integer length) {
+        public Attribute<V> setMaximalLengthCharacteristic(final Integer length) {
             return setCharacteristic(AttributeConvention.MAXIMAL_LENGTH_CHARACTERISTIC, Integer.class, length);
         }
 
@@ -537,7 +640,7 @@ public class FeatureTypeBuilder extends
          *
          * @see AttributeConvention#CRS_CHARACTERISTIC
          */
-        public Property<V> setCRSCharacteristic(final CoordinateReferenceSystem crs) {
+        public Attribute<V> setCRSCharacteristic(final CoordinateReferenceSystem crs) {
             return setCharacteristic(AttributeConvention.CRS_CHARACTERISTIC, CoordinateReferenceSystem.class, crs);
         }
 
@@ -546,9 +649,9 @@ public class FeatureTypeBuilder extends
          *
          * @throws UnsupportedOperationException if this property does not support characteristics.
          */
-        private <C> Property<V> setCharacteristic(final GenericName name, final Class<C> type, final C value) {
+        private <C> Attribute<V> setCharacteristic(final GenericName name, final Class<C> type, final C value) {
             for (final Characteristic<?> characteristic : characteristics) {
-                if (name.equals(characteristic.identification.get(DefaultAttributeType.NAME_KEY))) {
+                if (name.equals(characteristic.getName())) {
                     characteristic.set(value);
                     return this;
                 }
@@ -566,8 +669,8 @@ public class FeatureTypeBuilder extends
          *     attribute.addCharacteristic(Unit.class).setName("Unit of measurement").setDefaultValue(SI.CELSIUS);
          * }
          *
-         * Callers shall invoke at least one of the {@code Characteristic.setName(…)} methods on the returned instance.
-         * All other methods are optional.
+         * The default characteristic name is the name of the given type, but callers should invoke one
+         * of the {@code Characteristic.setName(…)} methods on the returned instance with a better name.
          *
          * @param  <C>   the compile-time type of {@code type} argument.
          * @param  type  the class of characteristic values.
@@ -578,42 +681,77 @@ public class FeatureTypeBuilder extends
             if (valueClass == Feature.class) {
                 throw new UnsupportedOperationException(errors().getString(Errors.Keys.IllegalOperationForValueClass_1, valueClass));
             }
-            ArgumentChecks.ensureNonNull("type", type);
+            ensureNonNull("type", type);
             final Characteristic<C> characteristic = new Characteristic<>(type);
             characteristics.add(characteristic);
             return characteristic;
         }
 
         /**
-         * Creates a new property type from the current setting.
+         * Flags this attribute as an input of one of the pre-defined operations managed by {@code FeatureTypeBuilder}.
+         *
+         * @param role the role to add to this attribute (shall not be null).
          */
-        final PropertyType build() {
-            final PropertyType property;
-            if (valueClass == Feature.class) {
-                final Object type = CollectionsExt.first(characteristics).defaultValue;
-                if (type instanceof FeatureType) {
-                    property = new DefaultAssociationRole(identification, (FeatureType) type, minimumOccurs, maximumOccurs);
-                } else {
-                    property = new DefaultAssociationRole(identification, (GenericName) type, minimumOccurs, maximumOccurs);
+        public void addRole(final AttributeRole role) {
+            ensureNonNull("role", role);
+            switch (role) {
+                case IDENTIFIER_COMPONENT: {
+                    isIdentifier = true;
+                    break;
                 }
-            } else {
-                final AttributeType<?>[] chrts = new AttributeType<?>[characteristics.size()];
-                for (int i=0; i<chrts.length; i++) {
-                    chrts[i] = characteristics.get(i).build();
+                case DEFAULT_GEOMETRY: {
+                    if (!Geometries.isKnownType(valueClass)) {
+                        throw new IllegalStateException(errors().getString(Errors.Keys.UnsupportedImplementation_1, valueClass));
+                    }
+                    if (defaultGeometry != null) {
+                        throw new IllegalStateException(errors().getString(Errors.Keys.PropertyAlreadyExists_2,
+                                FeatureTypeBuilder.this.getDisplayName(), AttributeConvention.GEOMETRY_PROPERTY));
+                    }
+                    defaultGeometry = this;
+                    break;
                 }
-                property = new DefaultAttributeType<>(identification, valueClass, minimumOccurs, maximumOccurs, defaultValue, chrts);
             }
-            return property;
+        }
+
+        /**
+         * Returns {@code true} if {@link AttributeRole#IDENTIFIER_COMPONENT} has been associated to this attribute.
+         */
+        @Override
+        boolean isIdentifier() {
+            return isIdentifier;
+        }
+
+        /**
+         * Appends a text inside the value returned by {@link #toString()}, before the closing bracket.
+         */
+        @Override
+        final void toStringInternal(final StringBuilder buffer) {
+            buffer.append(" : ").append(Classes.getShortName(valueClass));
+        }
+
+        /**
+         * Creates a new property type from the current setting.
+         */
+        @Override
+        final PropertyType build() {
+            final AttributeType<?>[] chrts = new AttributeType<?>[characteristics.size()];
+            for (int i=0; i<chrts.length; i++) {
+                chrts[i] = characteristics.get(i).build();
+            }
+            return new DefaultAttributeType<>(identification(), valueClass, minimumOccurs, maximumOccurs, defaultValue, chrts);
         }
     }
 
+
+
+
     /**
      * Describes one characteristic of the {@code AttributeType} to be built by the enclosing {@code FeatureTypeBuilder}.
      * A different instance of {@code Characteristic} exists for each characteristic to describe.
      * Those instances are created by:
      *
      * <ul>
-     *   <li>{@link Property#addCharacteristic(Class)}</li>
+     *   <li>{@link Attribute#addCharacteristic(Class)}</li>
      * </ul>
      *
      * @param <V> the class of characteristic values.
@@ -628,7 +766,7 @@ public class FeatureTypeBuilder extends
         /**
          * The default value for the attribute, or {@code null} if none.
          */
-        V defaultValue;
+        private V defaultValue;
 
         /**
          * Creates a new characteristic builder for values of the given class.
@@ -636,10 +774,20 @@ public class FeatureTypeBuilder extends
          * @param valueClass the class of characteristic values.
          */
         Characteristic(final Class<V> valueClass) {
+            super(FeatureTypeBuilder.this);
             this.valueClass = valueClass;
         }
 
         /**
+         * Returns a default name to use if the user did not specified a name. The first letter will be changed to
+         * lower case (unless the name looks like an acronym) for compliance with Java convention on property names.
+         */
+        @Override
+        final String getDefaultName() {
+            return Classes.getShortName(valueClass);
+        }
+
+        /**
          * Delegates the creation of a new name to the enclosing builder.
          */
         @Override
@@ -669,10 +817,13 @@ public class FeatureTypeBuilder extends
          * Creates a new characteristic from the current setting.
          */
         final AttributeType<V> build() {
-            return new DefaultAttributeType<>(identification, valueClass, 0, 1, defaultValue);
+            return new DefaultAttributeType<>(identification(), valueClass, 0, 1, defaultValue);
         }
     }
 
+
+
+
     /**
      * Builds the feature type from the information and properties specified to this builder.
      * One of the {@code setName(…)} methods must have been invoked before this {@code build()} method (mandatory).
@@ -680,61 +831,67 @@ public class FeatureTypeBuilder extends
      *
      * @return the new feature type.
      * @throws IllegalStateException if the feature type contains incompatible
-     *         {@linkplain Property#setCRSCharacteristic CRS characteristics}.
+     *         {@linkplain Attribute#setCRSCharacteristic CRS characteristics}.
      */
     public FeatureType build() throws IllegalStateException {
-        int numSynthetic;                                   // Number of synthetic properties to be generated.
-        int numSpecified = properties.size();               // Number of explicitely specified properties.
-        final PropertyType[] identifierTypes;
-        if (idAttributes.isEmpty()) {
-            identifierTypes = null;
-            numSynthetic = 0;
-        } else {
-            identifierTypes = new PropertyType[idAttributes.size()];
-            numSynthetic = 1;                               // Reserve one slot for the synthetic property "@identifier".
-        }
-        if (defaultGeometry != null) {
-            numSynthetic += 2;                              // One slot for "@defaultGeometry" and one for "@envelope".
-        }
-        PropertyType[] propertyTypes = new PropertyType[numSynthetic + numSpecified];
+        /*
+         * Creates an initial array of property types with up to 3 slots reserved for @identifier, @geometry
+         * and @envelope operations. At first we presume that there is always an identifier.  The identifier
+         * slot will be removed later if there is none.
+         */
+        final int numSpecified = properties.size();             // Number of explicitely specified properties.
+        final int numSynthetic = (defaultGeometry != null)      // Number of synthetic properties to be generated.
+                           ? GEOMETRY_INDEX + 1
+                           : IDENTIFIER_INDEX + 1;
+        PropertyType[] propertyTypes = new PropertyType[numSpecified + numSynthetic];
+        PropertyType[] identifierTypes = new PropertyType[4];   // There is rarely more than 4 identifier components.
+        int idIndex = 0;
         for (int i=0,j=numSynthetic; i<numSpecified; i++, j++) {
             final Property<?>  builder  = properties.get(i);
             final PropertyType instance = builder.build();
             propertyTypes[j] = instance;
-            final int id = idAttributes.indexOf(builder);
-            if (id >= 0) {
-                identifierTypes[id] = instance;
+            if (builder.isIdentifier()) {
+                if (idIndex >= identifierTypes.length) {
+                    identifierTypes = Arrays.copyOf(identifierTypes, idIndex*2);
+                }
+                identifierTypes[idIndex++] = instance;
             }
             /*
-             * If there is a default geometry, add a link named "@geometry" to that geometry. It may happen
-             * that the property created by the user is already named "@geometry", in which case will will
-             * avoid to duplicate the property by removing the second occurrence.
+             * If there is a default geometry, add a link named "@geometry" to that geometry.
+             * It may happen that the property created by the user is already named "@geometry",
+             * in which case we will avoid to duplicate the property.
              */
             if (builder == defaultGeometry) {
-                final PropertyType geom;
                 if (AttributeConvention.GEOMETRY_PROPERTY.equals(instance.getName())) {
-                    propertyTypes = ArraysExt.remove(propertyTypes, j--, 1);
-                    geom = instance;
+                    System.arraycopy(propertyTypes, GEOMETRY_INDEX, propertyTypes, ENVELOPE_INDEX, i);
+                    j--;
                 } else {
-                    geom = FeatureOperations.link(name(AttributeConvention.GEOMETRY_PROPERTY), instance);
+                    propertyTypes[GEOMETRY_INDEX] = FeatureOperations.link(name(AttributeConvention.GEOMETRY_PROPERTY), instance);
                 }
-                propertyTypes[numSynthetic - 1] = geom;
             }
         }
         /*
-         * Create the "envelope" operation only after we created all other properties, except "@identifier" which is not
-         * needed for envelope. It is okay if the 'propertyTypes' array still contains null elements like "@identifier",
-         * since FeatureOperations.envelope(…) constructor will ignore any property which is not for a value.
+         * Create the "envelope" operation only after we created all other properties.
+         * Actually it is okay if the 'propertyTypes' array still contains null elements not needed for envelope calculation
+         * like "@identifier", since FeatureOperations.envelope(…) constructor ignores any property which is not for a value.
          */
         if (defaultGeometry != null) try {
-            propertyTypes[numSynthetic - 2] = FeatureOperations.envelope(name(AttributeConvention.ENVELOPE_PROPERTY), null, propertyTypes);
+            propertyTypes[ENVELOPE_INDEX] = FeatureOperations.envelope(name(AttributeConvention.ENVELOPE_PROPERTY), null, propertyTypes);
         } catch (FactoryException e) {
             throw new IllegalStateException(e);
         }
-        if (identifierTypes != null) {
-            propertyTypes[0] = FeatureOperations.compound(name(AttributeConvention.IDENTIFIER_PROPERTY), idDelimiter, idPrefix, idSuffix, identifierTypes);
+        /*
+         * If a synthetic identifier need to be created, create it now as the first property.
+         * It may happen that the user provided a single identifier component already named
+         * "@identifier", in which case we avoid to duplicate the property.
+         */
+        if (idIndex != 0 && (idIndex != 1 || !AttributeConvention.IDENTIFIER_PROPERTY.equals(identifierTypes[0].getName()))) {
+            propertyTypes[IDENTIFIER_INDEX] = FeatureOperations.compound(name(AttributeConvention.IDENTIFIER_PROPERTY),
+                    idDelimiter, idPrefix, idSuffix, ArraysExt.resize(identifierTypes, idIndex));
+        } else {
+            propertyTypes = ArraysExt.remove(propertyTypes, IDENTIFIER_INDEX, 1);
         }
-        return new DefaultFeatureType(identification, isAbstract, superTypes.toArray(new FeatureType[superTypes.size()]), propertyTypes);
+        return new DefaultFeatureType(identification(), isAbstract, superTypes.toArray(new FeatureType[superTypes.size()]), propertyTypes);
     }
 
     /**
@@ -758,11 +915,4 @@ public class FeatureTypeBuilder extends
             return nameFactory.createGenericName(null, scope, localPart);
         }
     }
-
-    /**
-     * Returns the resources for error messages.
-     */
-    final Errors errors() {
-        return Errors.getResources(identification);
-    }
 }

Modified: sis/branches/JDK8/core/sis-feature/src/test/java/org/apache/sis/internal/feature/FeatureTypeBuilderTest.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-feature/src/test/java/org/apache/sis/internal/feature/FeatureTypeBuilderTest.java?rev=1751821&r1=1751820&r2=1751821&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-feature/src/test/java/org/apache/sis/internal/feature/FeatureTypeBuilderTest.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-feature/src/test/java/org/apache/sis/internal/feature/FeatureTypeBuilderTest.java [UTF-8] Thu Jul  7 17:50:09 2016
@@ -47,14 +47,9 @@ public final strictfp class FeatureTypeB
      */
     @Test
     public void testEmptyProperty() {
-        final FeatureTypeBuilder.Property<String> builder = new FeatureTypeBuilder().addAttribute(String.class);
-        try {
-            builder.build();
-            fail("Builder should have failed if there is not at least a name set.");
-        } catch (IllegalArgumentException ex) {
-            final String message = ex.getMessage();
-            assertTrue(message, message.contains("name"));
-        }
+        final FeatureTypeBuilder.Attribute<String> builder = new FeatureTypeBuilder().addAttribute(String.class);
+        assertEquals("default name", "string", builder.getName().toString());
+
         builder.setName("myScope", "myName");
         final AttributeType<?> att = (AttributeType<?>) builder.build();
 
@@ -104,7 +99,7 @@ public final strictfp class FeatureTypeB
     @Test
     @DependsOnMethod("testEmptyProperty")
     public void testPropertyBuild() {
-        final FeatureTypeBuilder.Property<String> builder = new FeatureTypeBuilder().addAttribute(String.class);
+        final FeatureTypeBuilder.Attribute<String> builder = new FeatureTypeBuilder().addAttribute(String.class);
         builder.setName        ("myScope", "myName");
         builder.setDefinition  ("test definition");
         builder.setDesignation ("test designation");
@@ -216,8 +211,11 @@ public final strictfp class FeatureTypeB
         final FeatureTypeBuilder builder = new FeatureTypeBuilder();
         builder.setName("scope", "test");
         builder.setIdentifierDelimiters("-", "pref.", null);
-        builder.addIdentifier(String.class).setName("name");
-        builder.addDefaultGeometry(Geometry.class).setName("shape").setCRSCharacteristic(HardCodedCRS.WGS84);
+        builder.addAttribute(String.class).setName("name")
+                .addRole(FeatureTypeBuilder.AttributeRole.IDENTIFIER_COMPONENT);
+        builder.addAttribute(Geometry.class).setName("shape")
+                .setCRSCharacteristic(HardCodedCRS.WGS84)
+                .addRole(FeatureTypeBuilder.AttributeRole.DEFAULT_GEOMETRY);
 
         final FeatureType type = builder.build();
         assertEquals("name", "scope:test", type.getName().toString());

Modified: sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXConstants.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXConstants.java?rev=1751821&r1=1751820&r2=1751821&view=diff
==============================================================================
--- sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXConstants.java [UTF-8] (original)
+++ sis/branches/JDK8/storage/sis-xmlstore/src/main/java/org/apache/sis/internal/gpx/GPXConstants.java [UTF-8] Thu Jul  7 17:50:09 2016
@@ -232,7 +232,7 @@ public final class GPXConstants extends
         final Map<String,?> geomInfo = Collections.singletonMap(AbstractIdentifiedType.NAME_KEY, geomName);
 
         //-------------------- GENERIC GPX ENTITY ------------------------------
-        final FeatureTypeBuilder builder = new FeatureTypeBuilder(factory);
+        final FeatureTypeBuilder builder = new FeatureTypeBuilder(factory, null);
         builder.setDefaultScope(GPX_NAMESPACE).setName("GPXEntity").setAbstract(true);
         builder.addAttribute(Integer.class).setName("index");
         TYPE_GPX_ENTITY = builder.build();
@@ -262,7 +262,9 @@ public final class GPXConstants extends
          * <extensions> extensionsType </extensions> [0..1] ?
          */
         builder.clear().setDefaultScope(GPX_NAMESPACE).setName("WayPoint").setSuperTypes(TYPE_GPX_ENTITY);
-        builder.addDefaultGeometry(Point.class).setName(geomName).setCRSCharacteristic(CommonCRS.defaultGeographic());
+        builder.addAttribute(Point.class).setName(geomName)
+                .setCRSCharacteristic(CommonCRS.defaultGeographic())
+                .addRole(FeatureTypeBuilder.AttributeRole.DEFAULT_GEOMETRY);
         builder.setDefaultCardinality(0, 1);
         builder.addAttribute(Double  .class).setName(TAG_WPT_ELE);
         builder.addAttribute(Temporal.class).setName(TAG_WPT_TIME);



Mime
View raw message