Modified: 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java?rev=1803070&r1=1803069&r2=1803070&view=diff
==============================================================================
--- 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java
 [UTF-8] (original)
+++ 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java
 [UTF-8] Wed Jul 26 16:14:09 2017
@@ -21,6 +21,7 @@ import java.util.Iterator;
 import java.util.Collection;
 import java.util.Collections;
 import java.io.Serializable;
+import org.opengis.util.ScopedName;
 import org.opengis.util.GenericName;
 import org.opengis.metadata.quality.DataQuality;
 import org.opengis.metadata.maintenance.ScopeCode;
@@ -242,12 +243,14 @@ public abstract class AbstractFeature im
         } else if (pt instanceof FeatureAssociationRole) {
             return ((FeatureAssociationRole) pt).newInstance();
         } else {
-            throw unsupportedPropertyType(pt.getName());
+            throw new 
IllegalArgumentException(unsupportedPropertyType(pt.getName()));
         }
     }
 
     /**
      * Executes the parameterless operation of the given name and returns its 
result.
+     *
+     * @see #getOperationValue(String)
      */
     final Property getOperationResult(final String name) {
         /*
@@ -260,41 +263,6 @@ public abstract class AbstractFeature im
     }
 
     /**
-     * Executes the parameterless operation of the given name and returns the 
value of its result.
-     */
-    final Object getOperationValue(final String name) {
-        final Operation operation = (Operation) type.getProperty(name);
-        if (operation instanceof LinkOperation) {
-            return getPropertyValue(((LinkOperation) operation).referentName);
-        }
-        final Property result = operation.apply(this, null);
-        if (result instanceof Attribute<?>) {
-            return getAttributeValue((Attribute<?>) result);
-        } else if (result instanceof FeatureAssociation) {
-            return getAssociationValue((FeatureAssociation) result);
-        } else {
-            return null;
-        }
-    }
-
-    /**
-     * Executes the parameterless operation of the given name and sets the 
value of its result.
-     */
-    final void setOperationValue(final String name, final Object value) {
-        final Operation operation = (Operation) type.getProperty(name);
-        if (operation instanceof LinkOperation) {
-            setPropertyValue(((LinkOperation) operation).referentName, value);
-        } else {
-            final Property result = operation.apply(this, null);
-            if (result != null) {
-                setPropertyValue(result, value);
-            } else {
-                throw new 
IllegalStateException(Resources.format(Resources.Keys.CanNotSetPropertyValue_1, 
name));
-            }
-        }
-    }
-
-    /**
      * Returns the default value to be returned by {@link 
#getPropertyValue(String)}
      * for the property of the given name.
      *
@@ -310,7 +278,7 @@ public abstract class AbstractFeature im
             final int maximumOccurs = ((FeatureAssociationRole) 
pt).getMaximumOccurs();
             return maximumOccurs > 1 ? Collections.EMPTY_LIST : null;       // 
No default value for associations.
         } else {
-            throw unsupportedPropertyType(pt.getName());
+            throw new 
IllegalArgumentException(unsupportedPropertyType(pt.getName()));
         }
     }
 
@@ -387,6 +355,73 @@ public abstract class AbstractFeature im
     public abstract void setPropertyValue(final String name, final Object 
value) throws IllegalArgumentException;
 
     /**
+     * Executes the parameterless operation of the given name and returns the 
value of its result.
+     * This is a convenience method for sub-classes where some properties may 
be operations that
+     * {@linkplain AbstractOperation#getDependencies() depend} on other 
properties of this {@code Feature} instance
+     * (for example a {@linkplain FeatureOperations#link link} to another 
property value).
+     * Invoking this method is equivalent to performing the following steps:
+     *
+     * {@preformat java
+     *     Operation operation = (Operation) type.getProperty(name);
+     *     Property result = operation.apply(this, null);
+     *     if (result instanceof Attribute<?>) {
+     *         return ...;                                      // the 
attribute value.
+     *     } else if (result instanceof FeatureAssociation) {
+     *         return ...;                                      // the 
associated feature.
+     *     } else {
+     *         return null;
+     *     }
+     * }
+     *
+     * @param  name  the name of the operation to execute. The caller is 
responsible to ensure that the
+     *               property type for that name is an instance of {@link 
Operation}.
+     * @return the result value of the given operation, or {@code null} if 
none.
+     *
+     * @since 0.8
+     */
+    protected Object getOperationValue(final String name) {
+        final Operation operation = (Operation) type.getProperty(name);
+        if (operation instanceof LinkOperation) {
+            return getPropertyValue(((LinkOperation) operation).referentName);
+        }
+        final Property result = operation.apply(this, null);
+        if (result instanceof Attribute<?>) {
+            return getAttributeValue((Attribute<?>) result);
+        } else if (result instanceof FeatureAssociation) {
+            return getAssociationValue((FeatureAssociation) result);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Executes the parameterless operation of the given name and sets the 
value of its result.
+     * This method is the complement of {@link #getOperationValue(String)} for 
subclasses where
+     * some properties may be operations. Not all operations accept 
assignments,
+     * but the {@linkplain FeatureOperations#link link} operation for instance 
does.
+     *
+     * @param  name   the name of the operation to execute. The caller is 
responsible to ensure that the
+     *                property type for that name is an instance of {@link 
Operation}.
+     * @param  value  the value to assign to the result of the named operation.
+     * @throws IllegalStateException if the operation of the given name does 
not accept assignment.
+     *
+     * @since 0.8
+     */
+    protected void setOperationValue(final String name, final Object value) {
+        final Operation operation = (Operation) type.getProperty(name);
+        if (operation instanceof LinkOperation) {
+            setPropertyValue(((LinkOperation) operation).referentName, value);
+        } else {
+            final Property result = operation.apply(this, null);
+            if (result != null) {
+                setPropertyValue(result, value);
+            } else {
+                throw new 
IllegalStateException(Resources.format(Resources.Keys.CanNotSetPropertyValue_1, 
name));
+            }
+        }
+    }
+
+    /**
      * Returns the value of the given attribute, as a singleton or as a 
collection depending
      * on the maximum number of occurrences.
      */
@@ -411,7 +446,7 @@ public abstract class AbstractFeature im
         } else if (property instanceof FeatureAssociation) {
             setAssociationValue((FeatureAssociation) property, value);
         } else {
-            throw unsupportedPropertyType(property.getName());
+            throw new 
IllegalArgumentException(unsupportedPropertyType(property.getName()));
         }
     }
 
@@ -439,7 +474,7 @@ public abstract class AbstractFeature im
                     } while ((element = it.next()) == null || 
base.isInstance(element));
                     // Found an illegal value. Exeption is thrown below.
                 }
-                throw illegalValueClass(pt, base, element);         // 
'element' can not be null here.
+                throw new ClassCastException(illegalValueClass(pt, base, 
element));         // 'element' can not be null here.
             }
         }
         ((Attribute) attribute).setValue(value);
@@ -457,14 +492,14 @@ public abstract class AbstractFeature im
             if (value instanceof Feature) {
                 final FeatureType actual = ((Feature) value).getType();
                 if (base != actual && 
!DefaultFeatureType.maybeAssignableFrom(base, actual)) {
-                    throw illegalFeatureType(role, base, actual);
+                    throw new 
InvalidPropertyValueException(illegalFeatureType(role, base, actual));
                 }
             } else if (value instanceof Collection<?>) {
                 verifyAssociationValues(role, (Collection<?>) value);
                 association.setValues((Collection<? extends Feature>) value);
                 return;                                 // Skip the setter at 
the end of this method.
             } else {
-                throw illegalValueClass(role, Feature.class, value);
+                throw new ClassCastException(illegalValueClass(role, 
Feature.class, value));
             }
         }
         association.setValue((Feature) value);
@@ -531,7 +566,7 @@ public abstract class AbstractFeature im
                 return verifyAssociationValue((FeatureAssociationRole) pt, 
value);
             }
         } else {
-            throw unsupportedPropertyType(pt.getName());
+            throw new 
IllegalArgumentException(unsupportedPropertyType(pt.getName()));
         }
         return value;
     }
@@ -554,7 +589,7 @@ public abstract class AbstractFeature im
         } else if (!isSingleton && value instanceof Collection<?>) {
             return CheckedArrayList.castOrCopy((Collection<?>) value, 
valueClass);
         } else {
-            throw illegalValueClass(type, valueClass, value);
+            throw new ClassCastException(illegalValueClass(type, valueClass, 
value));
         }
     }
 
@@ -580,13 +615,13 @@ public abstract class AbstractFeature im
             if (base == valueType || 
DefaultFeatureType.maybeAssignableFrom(base, valueType)) {
                 return isSingleton ? value : singletonList(Feature.class, 
role.getMinimumOccurs(), value);
             } else {
-                throw illegalFeatureType(role, base, valueType);
+                throw new 
InvalidPropertyValueException(illegalFeatureType(role, base, valueType));
             }
         } else if (!isSingleton && value instanceof Collection<?>) {
             verifyAssociationValues(role, (Collection<?>) value);
             return CheckedArrayList.castOrCopy((Collection<?>) value, 
Feature.class);
         } else {
-            throw illegalValueClass(role, Feature.class, value);
+            throw new ClassCastException(illegalValueClass(role, 
Feature.class, value));
         }
     }
 
@@ -599,11 +634,11 @@ public abstract class AbstractFeature im
         for (final Object value : values) {
             ArgumentChecks.ensureNonNullElement("values", index, value);
             if (!(value instanceof Feature)) {
-                throw illegalValueClass(role, Feature.class, value);
+                throw new ClassCastException(illegalValueClass(role, 
Feature.class, value));
             }
             final FeatureType type = ((Feature) value).getType();
             if (base != type && !DefaultFeatureType.maybeAssignableFrom(base, 
type)) {
-                throw illegalFeatureType(role, base, type);
+                throw new 
InvalidPropertyValueException(illegalFeatureType(role, base, type));
             }
             index++;
         }
@@ -621,34 +656,57 @@ public abstract class AbstractFeature im
     }
 
     /**
-     * Returns the exception for a property type which is neither an attribute 
or an association.
+     * Returns the exception message for a property not found. The message 
will differ depending
+     * on whether the property is not found because ambiguous or because it 
does not exist.
+     *
+     * @param  feature   the name of the feature where a property where 
searched ({@link String} or {@link GenericName}).
+     * @param  property  the name of the property which has not been found.
+     */
+    static String propertyNotFound(final FeatureType type, final Object 
feature, final String property) {
+        GenericName ambiguous = null;
+        for (final IdentifiedType p : type.getProperties(true)) {
+            final GenericName next = p.getName();
+            GenericName name = next;
+            do {
+                if (property.equalsIgnoreCase(name.toString())) {
+                    if (ambiguous == null) {
+                        ambiguous = next;
+                    } else {
+                        return Errors.format(Errors.Keys.AmbiguousName_3, 
ambiguous, next, property);
+                    }
+                }
+            } while (name instanceof ScopedName && (name = ((ScopedName) 
name).tail()) != null);
+        }
+        return Resources.format(Resources.Keys.PropertyNotFound_2, feature, 
property);
+    }
+
+    /**
+     * Returns the exception message for a property type which is neither an 
attribute or an association.
      * This method is invoked after a {@link PropertyType} has been found for 
the user-supplied name,
      * but that property can not be stored in or extracted from a {@link 
Property} instance.
      */
-    static IllegalArgumentException unsupportedPropertyType(final GenericName 
name) {
-        return new 
IllegalArgumentException(Resources.format(Resources.Keys.CanNotInstantiateProperty_1,
 name));
+    static String unsupportedPropertyType(final GenericName name) {
+        return Resources.format(Resources.Keys.CanNotInstantiateProperty_1, 
name);
     }
 
     /**
-     * Returns the exception for a property value of wrong Java class.
+     * Returns the exception message for a property value of wrong Java class.
      *
      * @param  value  the value, which shall be non-null.
      */
-    private static ClassCastException illegalValueClass(
-            final IdentifiedType property, final Class<?> expected, final 
Object value)
-    {
-        return new 
ClassCastException(Resources.format(Resources.Keys.IllegalPropertyValueClass_3,
-                property.getName(), expected, value.getClass()));
+    private static String illegalValueClass(final IdentifiedType property, 
final Class<?> expected, final Object value) {
+        return Resources.format(Resources.Keys.IllegalPropertyValueClass_3,
+                                property.getName(), expected, 
value.getClass());
     }
 
     /**
-     * Returns the exception for an association value of wrong type.
+     * Returns the exception message for an association value of wrong type.
      */
-    private static InvalidPropertyValueException illegalFeatureType(
+    private static String illegalFeatureType(
             final FeatureAssociationRole association, final FeatureType 
expected, final FeatureType actual)
     {
-        return new 
InvalidPropertyValueException(Resources.format(Resources.Keys.IllegalFeatureType_3,
-                association.getName(), expected.getName(), actual.getName()));
+        return Resources.format(Resources.Keys.IllegalFeatureType_3,
+                                association.getName(), expected.getName(), 
actual.getName());
     }
 
     /**

Modified: 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java?rev=1803070&r1=1803069&r2=1803070&view=diff
==============================================================================
--- 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java
 [UTF-8] (original)
+++ 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAssociationRole.java
 [UTF-8] Wed Jul 26 16:14:09 2017
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.IdentityHashMap;
 import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
+import org.opengis.metadata.Identifier;
 import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.util.Debug;
 
@@ -33,6 +34,7 @@ import org.opengis.feature.AttributeType
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.FeatureAssociation;
 import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.PropertyNotFoundException;
 
 
 /**
@@ -378,35 +380,62 @@ public class DefaultAssociationRole exte
 
     /**
      * Returns the name of the property to use as a title for the associated 
feature, or {@code null} if none.
-     * This method searches for the first attribute having a value class 
assignable to {@link CharSequence}.
+     * This method applies the following heuristic rules:
+     *
+     * <ul>
+     *   <li>If associated feature has a property named {@code 
"sis:identifier"}, then this method returns that name.</li>
+     *   <li>Otherwise if the associated feature has a mandatory property of 
type {@link CharSequence}, {@link GenericName}
+     *       or {@link Identifier}, then this method returns the name of that 
property.</li>
+     *   <li>Otherwise if the associated feature has an optional property of 
type {@link CharSequence}, {@link GenericName}
+     *       or {@link Identifier}, then this method returns the name of that 
property.</li>
+     *   <li>Otherwise this method returns {@code null}.</li>
+     * </ul>
+     *
+     * This method should be used only for display purpose, not as a reliable 
or stable way to get the identifier.
+     * The heuristic rules implemented in this method may change in any future 
Apache SIS version.
      */
     static String getTitleProperty(final FeatureAssociationRole role) {
         if (role instanceof DefaultAssociationRole) {
-            String p = ((DefaultAssociationRole) role).titleProperty; // No 
synchronization - not a big deal if computed twice.
+            String p = ((DefaultAssociationRole) role).titleProperty;       // 
No synchronization - not a big deal if computed twice.
             if (p != null) {
                 return p.isEmpty() ? null : p;
             }
-            p = searchTitleProperty(role);
+            p = searchTitleProperty(role.getValueType());
             ((DefaultAssociationRole) role).titleProperty = (p != null) ? p : 
"";
             return p;
         }
-        return searchTitleProperty(role);
+        return searchTitleProperty(role.getValueType());
     }
 
     /**
      * Implementation of {@link #getTitleProperty(FeatureAssociationRole)} for 
first search,
      * or for non-SIS {@code FeatureAssociationRole} implementations.
      */
-    private static String searchTitleProperty(final FeatureAssociationRole 
role) {
-        for (final PropertyType type : 
role.getValueType().getProperties(true)) {
+    private static String searchTitleProperty(final FeatureType ft) {
+        String fallback = null;
+        try {
+            return ft.getProperty("sis:identifier").getName().toString();
+        } catch (PropertyNotFoundException e) {
+            // Ignore.
+        }
+        for (final PropertyType type : ft.getProperties(true)) {
             if (type instanceof AttributeType<?>) {
                 final AttributeType<?> pt = (AttributeType<?>) type;
-                if (pt.getMaximumOccurs() != 0 && 
CharSequence.class.isAssignableFrom(pt.getValueClass())) {
-                    return pt.getName().toString();
+                final Class<?> valueClass = pt.getValueClass();
+                if (CharSequence.class.isAssignableFrom(valueClass) ||
+                    GenericName .class.isAssignableFrom(valueClass) ||
+                    Identifier  .class.isAssignableFrom(valueClass))
+                {
+                    final String name = pt.getName().toString();
+                    if (pt.getMaximumOccurs() != 0) {
+                        return name;
+                    } else if (fallback == null) {
+                        fallback = name;
+                    }
                 }
             }
         }
-        return null;
+        return fallback;
     }
 
     /**

Modified: 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAttributeType.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAttributeType.java?rev=1803070&r1=1803069&r2=1803070&view=diff
==============================================================================
--- 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAttributeType.java
 [UTF-8] (original)
+++ 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultAttributeType.java
 [UTF-8] Wed Jul 26 16:14:09 2017
@@ -186,6 +186,7 @@ public class DefaultAttributeType<V> ext
      *
      * @see org.apache.sis.feature.builder.AttributeTypeBuilder
      */
+    @SuppressWarnings("ThisEscapedInObjectConstruction")    // Okay because 
used only in package-private class.
     public DefaultAttributeType(final Map<String,?> identification, final 
Class<V> valueClass,
             final int minimumOccurs, final int maximumOccurs, final V 
defaultValue,
             final AttributeType<?>... characterizedBy)

Modified: 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java?rev=1803070&r1=1803069&r2=1803070&view=diff
==============================================================================
--- 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
 [UTF-8] (original)
+++ 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
 [UTF-8] Wed Jul 26 16:14:09 2017
@@ -712,27 +712,59 @@ public class DefaultFeatureType extends
      */
     private static boolean isAssignableIgnoreName(final PropertyType base, 
final PropertyType other) {
         if (base != other) {
+            /*
+             * If the base property is an attribute, then the overriding 
property shall be either an attribute
+             * or a parameterless operation producing an attribute.  The 
parameterless operation is considered
+             * has having a [1…1] cardinality.
+             */
             if (base instanceof AttributeType<?>) {
-                if (!(other instanceof AttributeType<?>)) {
+                final AttributeType<?> p0 = (AttributeType<?>) base;
+                final AttributeType<?> p1;
+                if (other instanceof AttributeType<?>) {
+                    p1 = (AttributeType<?>) other;
+                } else if (isParameterlessOperation(other)) {
+                    final IdentifiedType result = ((Operation) 
other).getResult();
+                    if (result instanceof AttributeType<?>) {
+                        p1 = (AttributeType<?>) result;
+                    } else {
+                        return false;
+                    }
+                } else {
                     return false;
                 }
-                final AttributeType<?> p0 = (AttributeType<?>) base;
-                final AttributeType<?> p1 = (AttributeType<?>) other;
-                if (!p0.getValueClass().isAssignableFrom(p1.getValueClass()) ||
-                     p0.getMinimumOccurs() > p1.getMinimumOccurs() ||
-                     p0.getMaximumOccurs() < p1.getMaximumOccurs())
+                final int minOccurs, maxOccurs;
+                if (!p0.getValueClass().isAssignableFrom(p1.getValueClass())   
 ||
+                    (minOccurs = p0.getMinimumOccurs()) > 
p1.getMinimumOccurs() ||
+                    (maxOccurs = p0.getMaximumOccurs()) < 
p1.getMaximumOccurs() ||
+                    (p1 != other && (minOccurs > 1 || maxOccurs < 1)))         
    // [1…1] cardinality for operations.
                 {
                     return false;
                 }
             }
+            /*
+             * Unconditionally test for associations even if we executed the 
previous block for attributes,
+             * because an implementation could implement both AttributeType 
and AssociationRole interfaces.
+             * This is not recommended, but if it happen we want a behavior as 
consistent as possible.
+             */
             if (base instanceof FeatureAssociationRole) {
-                if (!(other instanceof FeatureAssociationRole)) {
+                final FeatureAssociationRole p0 = (FeatureAssociationRole) 
base;
+                final FeatureAssociationRole p1;
+                if (other instanceof FeatureAssociationRole) {
+                    p1 = (FeatureAssociationRole) other;
+                } else if (isParameterlessOperation(other)) {
+                    final IdentifiedType result = ((Operation) 
other).getResult();
+                    if (result instanceof FeatureAssociationRole) {
+                        p1 = (FeatureAssociationRole) result;
+                    } else {
+                        return false;
+                    }
+                } else {
                     return false;
                 }
-                final FeatureAssociationRole p0 = (FeatureAssociationRole) 
base;
-                final FeatureAssociationRole p1 = (FeatureAssociationRole) 
other;
-                if (p0.getMinimumOccurs() > p1.getMinimumOccurs() ||
-                    p0.getMaximumOccurs() < p1.getMaximumOccurs())
+                final int minOccurs, maxOccurs;
+                if ((minOccurs = p0.getMinimumOccurs()) > 
p1.getMinimumOccurs() ||
+                    (maxOccurs = p0.getMaximumOccurs()) < 
p1.getMaximumOccurs() ||
+                    (p1 != other && (minOccurs > 1 || maxOccurs < 1)))         
    // [1…1] cardinality for operations.
                 {
                     return false;
                 }
@@ -742,17 +774,26 @@ public class DefaultFeatureType extends
                     return false;
                 }
             }
+            /*
+             * Operations can be overridden by other operations having the 
same parameters.
+             * In the special case of parameterless operations, can also be 
overridden by
+             * AttributeType or FeatureAssociationRole.
+             */
             if (base instanceof Operation) {
-                if (!(other instanceof Operation)) {
-                    return false;
-                }
                 final Operation p0 = (Operation) base;
-                final Operation p1 = (Operation) other;
-                if (!Objects.equals(p0.getParameters(), p1.getParameters())) {
+                final IdentifiedType r1;
+                if (other instanceof Operation) {
+                    final Operation p1 = (Operation) other;
+                    if (!Objects.equals(p0.getParameters(), 
p1.getParameters())) {
+                        return false;
+                    }
+                    r1 = p1.getResult();
+                } else if (isParameterlessOperation(base)) {
+                    r1 = other;
+                } else {
                     return false;
                 }
                 final IdentifiedType r0 = p0.getResult();
-                final IdentifiedType r1 = p1.getResult();
                 if (r0 != r1) {
                     if (r0 instanceof FeatureType) {
                         if (!(r1 instanceof FeatureType) || !((FeatureType) 
r0).isAssignableFrom((FeatureType) r1)) {
@@ -764,6 +805,7 @@ public class DefaultFeatureType extends
                             return false;
                         }
                     }
+                    // No need for explicit AttributeType or Operation checks 
because they are PropertyType.
                 }
             }
         }
@@ -822,7 +864,7 @@ public class DefaultFeatureType extends
         if (pt != null) {
             return pt;
         }
-        throw new 
PropertyNotFoundException(Resources.format(Resources.Keys.PropertyNotFound_2, 
getName(), name));
+        throw new 
PropertyNotFoundException(AbstractFeature.propertyNotFound(this, getName(), 
name));
     }
 
     /**

Modified: 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java?rev=1803070&r1=1803069&r2=1803070&view=diff
==============================================================================
--- 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java
 [UTF-8] (original)
+++ 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java
 [UTF-8] Wed Jul 26 16:14:09 2017
@@ -20,7 +20,6 @@ import java.util.Map;
 import java.util.Arrays;
 import org.opengis.metadata.maintenance.ScopeCode;
 import org.opengis.metadata.quality.DataQuality;
-import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.util.Cloner;
 import org.apache.sis.util.ArgumentChecks;
 
@@ -72,7 +71,7 @@ final class DenseFeature extends Abstrac
     /**
      * Creates a new feature of the given type.
      *
-     * @param type Information about the feature (name, characteristics, 
<i>etc.</i>).
+     * @param type  information about the feature (name, characteristics, 
<i>etc.</i>).
      */
     public DenseFeature(final DefaultFeatureType type) {
         super(type);
@@ -93,7 +92,7 @@ final class DenseFeature extends Abstrac
         if (index != null) {
             return index;
         }
-        throw new 
PropertyNotFoundException(Resources.format(Resources.Keys.PropertyNotFound_2, 
getName(), name));
+        throw new PropertyNotFoundException(propertyNotFound(type, getName(), 
name));
     }
 
     /**
@@ -196,7 +195,7 @@ final class DenseFeature extends Abstrac
                 } else if (element instanceof FeatureAssociation) {
                     return getAssociationValue((FeatureAssociation) element);
                 } else {
-                    throw unsupportedPropertyType(((Property) 
element).getName());
+                    throw new 
IllegalArgumentException(unsupportedPropertyType(((Property) 
element).getName()));
                 }
             }
         }

Modified: 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureFormat.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureFormat.java?rev=1803070&r1=1803069&r2=1803070&view=diff
==============================================================================
--- 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureFormat.java
 [UTF-8] (original)
+++ 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureFormat.java
 [UTF-8] Wed Jul 26 16:14:09 2017
@@ -18,7 +18,10 @@ package org.apache.sis.feature;
 
 import java.util.List;
 import java.util.ArrayList;
+import java.util.Set;
+import java.util.EnumSet;
 import java.util.Iterator;
+import java.util.Collection;
 import java.util.Locale;
 import java.util.TimeZone;
 import java.io.IOException;
@@ -33,12 +36,14 @@ import org.opengis.util.GenericName;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.io.TabularFormat;
 import org.apache.sis.util.Deprecable;
+import org.apache.sis.util.Characters;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.math.MathFunctions;
 
 // Branch-dependent imports
 import java.io.UncheckedIOException;
@@ -49,9 +54,9 @@ import org.opengis.feature.Attribute;
 import org.opengis.feature.AttributeType;
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
+import org.opengis.feature.FeatureAssociation;
 import org.opengis.feature.FeatureAssociationRole;
 import org.opengis.feature.Operation;
-import org.apache.sis.util.Characters;
 
 
 /**
@@ -88,7 +93,7 @@ public class FeatureFormat extends Tabul
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = 8866440357566645070L;
+    private static final long serialVersionUID = -5792086817264884947L;
 
     /**
      * An instance created when first needed and potentially shared.
@@ -101,6 +106,12 @@ public class FeatureFormat extends Tabul
     private final Locale displayLocale;
 
     /**
+     * The columns to include in the table formatted by this {@code 
FeatureFormat}.
+     * By default, all columns having at least one value are included.
+     */
+    private final EnumSet<Column> columns = EnumSet.allOf(Column.class);
+
+    /**
      * Maximal length of attribute values, in number of characters.
      * If a value is longer than this length, it will be truncated.
      *
@@ -159,6 +170,104 @@ public class FeatureFormat extends Tabul
     }
 
     /**
+     * Returns all columns that may be shown in the tables to format.
+     * The columns included in the set may be shown, but not necessarily;
+     * some columns will still be omitted if they are completely empty.
+     * However columns <em>not</em> included in the set are guaranteed to be 
omitted.
+     *
+     * @return all columns that may be shown in the tables to format.
+     *
+     * @since 0.8
+     */
+    public Set<Column> getAllowedColumns() {
+        return columns.clone();
+    }
+
+    /**
+     * Sets all columns that may be shown in the tables to format.
+     * Note that the columns specified to this method are not guaranteed to be 
shown;
+     * some columns will still be omitted if they are completely empty.
+     *
+     * @param inclusion  all columns that may be shown in the tables to format.
+     *
+     * @since 0.8
+     */
+    public void setAllowedColumns(final Set<Column> inclusion) {
+        ArgumentChecks.ensureNonNull("inclusion", inclusion);
+        columns.clear();
+        columns.addAll(inclusion);
+    }
+
+    /**
+     * Identifies the columns to include in the table formatted by {@code 
FeatureFormat}.
+     * By default, all columns having at least one non-null value are shown. 
But a smaller
+     * set of columns can be specified to the {@link 
FeatureFormat#setAllowedColumns(Set)}
+     * method for formatting narrower tables.
+     *
+     * @see FeatureFormat#setAllowedColumns(Set)
+     *
+     * @since 0.8
+     */
+    public enum Column {
+        /**
+         * Natural language designator for the property.
+         * This is the character sequence returned by {@link 
PropertyType#getDesignation()}.
+         * This column is omitted if no property has a designation.
+         */
+        DESIGNATION(Vocabulary.Keys.Designation),
+
+        /**
+         * Name of the property.
+         * This is the character sequence returned by {@link 
PropertyType#getName()}.
+         */
+        NAME(Vocabulary.Keys.Name),
+
+        /**
+         * Type of property values. This is the type returned by {@link 
AttributeType#getValueClass()} or
+         * {@link FeatureAssociationRole#getValueType()}.
+         */
+        TYPE(Vocabulary.Keys.Type),
+
+        /**
+         * The minimum and maximum occurrences of attribute values. This is 
made from the numbers returned
+         * by {@link AttributeType#getMinimumOccurs()} and {@link 
AttributeType#getMaximumOccurs()}.
+         */
+        CARDINALITY(Vocabulary.Keys.Cardinality),
+
+        /**
+         * Property value (for properties) or default value (for property 
types).
+         * This is the value returned by {@link Attribute#getValue()}, {@link 
FeatureAssociation#getValue()}
+         * or {@link AttributeType#getDefaultValue()}.
+         */
+        VALUE(Vocabulary.Keys.Value),
+
+        /**
+         * Other attributes that describes the attribute.
+         * This is made from the map returned by {@link 
Attribute#characteristics()}.
+         * This column is omitted if no property has characteristics.
+         */
+        CHARACTERISTICS(Vocabulary.Keys.Characteristics),
+
+        /**
+         * Whether a property is deprecated, or other remarks.
+         * This column is omitted if no property has remarks.
+         */
+        REMARKS(Vocabulary.Keys.Remarks);
+
+        /**
+         * The {@link Vocabulary} key to use for formatting the header of this 
column.
+         */
+        final short resourceKey;
+
+        /**
+         * Creates a new column enumeration constant.
+         */
+        private Column(final short key) {
+            resourceKey = key;
+        }
+    }
+
+    /**
      * Invoked when the formatter needs to move to the next column.
      */
     private void nextColumn(final TableAppender table) {
@@ -197,18 +306,30 @@ public class FeatureFormat extends Tabul
                     .getString(Errors.Keys.UnsupportedType_1, 
object.getClass()));
         }
         /*
-         * Check if at least one attribute has at least one characteritic. In 
many cases there is none.
-         * In none we will ommit the "characteristics" column, which is the 
last column.
+         * Computes the columns to show. We start with the set of columns 
specified by setAllowedColumns(Set),
+         * then we check if some of those columns are empty. For example in 
many cases there is no attribute
+         * with characteritic, in which case we will ommit the whole 
"characteristics" column. We perform such
+         * check only for optional information, not for mandatory information 
like property names.
          */
-        boolean hasCharacteristics = false;
-        boolean hasDeprecatedTypes = false;
-        for (final PropertyType propertyType : 
featureType.getProperties(true)) {
-            if (!hasCharacteristics && propertyType instanceof 
AttributeType<?>) {
-                hasCharacteristics = !((AttributeType<?>) 
propertyType).characteristics().isEmpty();
-            }
-            if (!hasDeprecatedTypes && propertyType instanceof Deprecable) {
-                hasDeprecatedTypes = ((Deprecable) 
propertyType).isDeprecated();
+        final EnumSet<Column> visibleColumns = columns.clone();
+        {
+            boolean hasDesignation     = false;
+            boolean hasCharacteristics = false;
+            boolean hasDeprecatedTypes = false;
+            for (final PropertyType propertyType : 
featureType.getProperties(true)) {
+                if (!hasDesignation) {
+                    hasDesignation = propertyType.getDesignation() != null;
+                }
+                if (!hasCharacteristics && propertyType instanceof 
AttributeType<?>) {
+                    hasCharacteristics = !((AttributeType<?>) 
propertyType).characteristics().isEmpty();
+                }
+                if (!hasDeprecatedTypes && propertyType instanceof Deprecable) 
{
+                    hasDeprecatedTypes = ((Deprecable) 
propertyType).isDeprecated();
+                }
             }
+            if (!hasDesignation)     visibleColumns.remove(Column.DESIGNATION);
+            if (!hasCharacteristics) 
visibleColumns.remove(Column.CHARACTERISTICS);
+            if (!hasDeprecatedTypes) visibleColumns.remove(Column.REMARKS);
         }
         /*
          * Format the feature type name. In the case of feature type, format 
also the names of super-type
@@ -218,43 +339,44 @@ public class FeatureFormat extends Tabul
          */
         toAppendTo.append(toString(featureType.getName()));
         if (feature == null) {
-            String separator = " ⇾ ";   // UML symbol for inheritance.
+            String separator = " ⇾ ";                                       // 
UML symbol for inheritance.
             for (final FeatureType parent : featureType.getSuperTypes()) {
                 
toAppendTo.append(separator).append(toString(parent.getName()));
                 separator = ", ";
             }
         }
         toAppendTo.append(getLineSeparator());
+        /*
+         * Create a table and format the header. Columns will be shown in 
Column enumeration order.
+         */
         final Vocabulary resources = Vocabulary.getResources(displayLocale);
         final TableAppender table = new TableAppender(toAppendTo, 
columnSeparator);
         table.setMultiLinesCells(true);
         table.nextLine('─');
-header: for (int i=0; ; i++) {
-            final short key;
-            switch (i) {
-                case 0:                     key = Vocabulary.Keys.Name; break;
-                case 1:  nextColumn(table); key = Vocabulary.Keys.Type; break;
-                case 2:  nextColumn(table); key = Vocabulary.Keys.Cardinality; 
break;
-                case 3:  nextColumn(table); key = (feature != null) ? 
Vocabulary.Keys.Value : Vocabulary.Keys.DefaultValue; break;
-                case 4:  if (!hasCharacteristics) continue;
-                         nextColumn(table); key = 
Vocabulary.Keys.Characteristics; break;
-                case 5:  if (!hasDeprecatedTypes) continue;
-                         nextColumn(table); key = Vocabulary.Keys.Remarks; 
break;
-                default: break header;
+        boolean isFirstColumn = true;
+        for (final Column column : visibleColumns) {
+            short key = column.resourceKey;
+            if (key == Vocabulary.Keys.Value && feature == null) {
+                key = Vocabulary.Keys.DefaultValue;
             }
+            if (!isFirstColumn) nextColumn(table);
             table.append(resources.getString(key));
+            isFirstColumn = false;
         }
         table.nextLine();
         table.nextLine('─');
         /*
-         * Done writing the header. Now write all property rows.
-         * Rows without value will be skipped only if optional.
+         * Done writing the header. Now write all property rows.  For each 
row, the first part in the loop
+         * extracts all information needed without formatting anything yet. If 
we detect in that part that
+         * a row has no value, it will be skipped if and only if that row is 
optional (minimum occurrence
+         * of zero).
          */
         final StringBuffer  buffer  = new StringBuffer();
         final FieldPosition dummyFP = new FieldPosition(-1);
         final List<String>  remarks = new ArrayList<>();
         for (final PropertyType propertyType : 
featureType.getProperties(true)) {
             Object value = null;
+            int cardinality = -1;
             if (feature != null) {
                 if (!(propertyType instanceof AttributeType<?>) &&
                     !(propertyType instanceof FeatureAssociationRole) &&
@@ -264,16 +386,21 @@ header: for (int i=0; ; i++) {
                 }
                 value = 
feature.getPropertyValue(propertyType.getName().toString());
                 if (value == null) {
-                    if (propertyType instanceof AttributeType &&
-                            ((AttributeType) propertyType).getMinimumOccurs() 
== 0)
+                    if (propertyType instanceof AttributeType<?>
+                            && ((AttributeType<?>) 
propertyType).getMinimumOccurs() == 0)
                     {
-                        continue;                                       // If 
no value, skip the full row.
+                        continue;                           // If optional and 
no value, skip the full row.
                     }
-                    if (propertyType instanceof FeatureAssociationRole &&
-                            ((FeatureAssociationRole) 
propertyType).getMinimumOccurs() == 0)
+                    if (propertyType instanceof FeatureAssociationRole
+                            && ((FeatureAssociationRole) 
propertyType).getMinimumOccurs() == 0)
                     {
-                        continue;                                       // If 
no value, skip the full row.
+                        continue;                           // If optional and 
no value, skip the full row.
                     }
+                    cardinality = 0;
+                } else if (value instanceof Collection<?>) {
+                    cardinality = ((Collection<?>) value).size();
+                } else {
+                    cardinality = 1;
                 }
             } else if (propertyType instanceof AttributeType<?>) {
                 value = ((AttributeType<?>) propertyType).getDefaultValue();
@@ -291,14 +418,6 @@ header: for (int i=0; ; i++) {
                 value = CharSequences.trimWhitespaces(buffer).toString();
                 buffer.setLength(0);
             }
-            /*
-             * Column 0 - Name.
-             */
-            table.append(toString(propertyType.getName()));
-            nextColumn(table);
-            /*
-             * Column 1 and 2 - Type and cardinality.
-             */
             final String   valueType;                       // The value to 
write in the type column.
             final Class<?> valueClass;                      // 
AttributeType.getValueClass() if applicable.
             final int minimumOccurs, maximumOccurs;         // Negative values 
mean no cardinality.
@@ -327,82 +446,134 @@ header: for (int i=0; ; i++) {
                 minimumOccurs = -1;
                 maximumOccurs = -1;
             }
-            table.append(valueType);
-            nextColumn(table);
-            if (maximumOccurs >= 0) {
-                final Format format = getFormat(Integer.class);
-                table.append('[').append(format.format(minimumOccurs, buffer, 
dummyFP)).append(" … ");
-                buffer.setLength(0);
-                if (maximumOccurs != Integer.MAX_VALUE) {
-                    table.append(format.format(maximumOccurs, buffer, 
dummyFP));
-                } else {
-                    table.append('∞');
-                }
-                buffer.setLength(0);
-                table.append(']');
-            }
-            nextColumn(table);
             /*
-             * Column 3 - Value or default value.
+             * At this point we determined that the row should not be skipped
+             * and we got all information to format.
              */
-            if (value != null) {
-                final Format format = getFormat(valueClass);                   
         // Null if valueClass is null.
-                final Iterator<?> it = 
CollectionsExt.toCollection(value).iterator();
-                String separator = "";
-                int length = 0;
-                while (it.hasNext()) {
-                    value = it.next();
-                    if (value != null) {
-                        if (format != null && valueClass.isInstance(value)) {
-                            value = format.format(value, buffer, dummyFP);
-                        } else if (value instanceof Feature && propertyType 
instanceof FeatureAssociationRole) {
-                            final String p = 
DefaultAssociationRole.getTitleProperty((FeatureAssociationRole) propertyType);
-                            if (p != null) {
-                                value = ((Feature) value).getPropertyValue(p);
-                                if (value == null) continue;
+            isFirstColumn = true;
+            for (final Column column : visibleColumns) {
+                if (!isFirstColumn) nextColumn(table);
+                isFirstColumn = false;
+                switch (column) {
+                    case DESIGNATION: {
+                        final InternationalString d = 
propertyType.getDesignation();
+                        if (d != null) table.append(d.toString(displayLocale));
+                        break;
+                    }
+                    case NAME: {
+                        table.append(toString(propertyType.getName()));
+                        break;
+                    }
+                    case TYPE: {
+                        table.append(valueType);
+                        break;
+                    }
+                    case CARDINALITY: {
+                        table.setCellAlignment(TableAppender.ALIGN_RIGHT);
+                        if (cardinality >= 0) {
+                            
table.append(getFormat(Integer.class).format(cardinality, buffer, dummyFP));
+                            buffer.setLength(0);
+                        }
+                        if (maximumOccurs >= 0) {
+                            if (cardinality >= 0) {
+                                table.append(' ')
+                                     .append((cardinality >= minimumOccurs && 
cardinality <= maximumOccurs) ? '∈' : '∉')
+                                     .append(' ');
                             }
+                            final Format format = getFormat(Integer.class);
+                            
table.append('[').append(format.format(minimumOccurs, buffer, 
dummyFP)).append(" … ");
+                            buffer.setLength(0);
+                            if (maximumOccurs != Integer.MAX_VALUE) {
+                                table.append(format.format(maximumOccurs, 
buffer, dummyFP));
+                            } else {
+                                table.append('∞');
+                            }
+                            buffer.setLength(0);
+                            table.append(']');
                         }
-                        length = formatValue(value, table.append(separator), 
length);
-                        buffer.setLength(0);
-                        separator = ", ";
-                        if (length < 0) break;      // Value is too long, 
abandon remaining iterations.
+                        break;
                     }
-                }
-            }
-            /*
-             * Column 4 - Characteristics.
-             */
-            if (hasCharacteristics) {
-                nextColumn(table);
-                if (propertyType instanceof AttributeType<?>) {
-                    String separator = "";
-                    for (final AttributeType<?> attribute : 
((AttributeType<?>) propertyType).characteristics().values()) {
-                        
table.append(separator).append(toString(attribute.getName()));
-                        Object c = attribute.getDefaultValue();
-                        if (feature != null) {
-                            final Property p = 
feature.getProperty(propertyType.getName().toString());
-                            if (p instanceof Attribute<?>) {            // 
Should always be true, but we are paranoiac.
-                                c = ((Attribute<?>) 
p).characteristics().get(attribute.getName().toString());
+                    case VALUE: {
+                        table.setCellAlignment(TableAppender.ALIGN_LEFT);
+                        final Format format = getFormat(valueClass);           
                 // Null if valueClass is null.
+                        final Iterator<?> it = 
CollectionsExt.toCollection(value).iterator();
+                        String separator = "";
+                        int length = 0;
+                        while (it.hasNext()) {
+                            value = it.next();
+                            if (value != null) {
+                                if (propertyType instanceof 
FeatureAssociationRole) {
+                                    final String p = 
DefaultAssociationRole.getTitleProperty((FeatureAssociationRole) propertyType);
+                                    if (p != null) {
+                                        value = ((Feature) 
value).getPropertyValue(p);
+                                        if (value == null) continue;
+                                    }
+                                } else if (format != null && 
valueClass.isInstance(value)) {    // Null safe because of 
getFormat(valueClass) contract.
+                                    /*
+                                     * Convert numbers, dates, angles, etc. to 
character sequences before to append them in the table.
+                                     * Note that DecimalFormat writes 
Not-a-Number as "NaN" in some locales and as "�" in other locales
+                                     * (U+FFFD - Unicode replacement 
character). The "�" seems to be used mostly for historical reasons;
+                                     * as of 2017 the Unicode Common Locale 
Data Repository (CLDR) seems to define "NaN" for all locales.
+                                     * We could configure DecimalFormatSymbols 
for using "NaN", but (for now) we rather substitute "�" by
+                                     * "NaN" here for avoiding to change the 
DecimalFormat configuration and for distinguishing the NaNs.
+                                     */
+                                    final StringBuffer t = 
format.format(value, buffer, dummyFP);
+                                    if (value instanceof Number) {
+                                        final float f = ((Number) 
value).floatValue();
+                                        if (Float.isNaN(f)) {
+                                            if ("�".contentEquals(t)) {
+                                                t.setLength(0);
+                                                t.append("NaN");
+                                            }
+                                            final int n = 
MathFunctions.toNanOrdinal(f);
+                                            if (n > 0) buffer.append(" 
#").append(n);
+                                        }
+                                    }
+                                    value = t;
+                                }
+                                /*
+                                 * All values: the numbers, dates, angles, 
etc. formatted above, any other character sequences
+                                 * (e.g. InternationalString), or other kind 
of values - some of them handled in a special way.
+                                 */
+                                length = formatValue(value, 
table.append(separator), length);
+                                buffer.setLength(0);
+                                if (length < 0) break;      // Value is too 
long, abandon remaining iterations.
+                                separator = ", ";
+                                length += 2;
                             }
                         }
-                        if (c != null) {
-                            formatValue(c, table.append(" = "), 0);
+                        break;
+                    }
+                    case CHARACTERISTICS: {
+                        if (propertyType instanceof AttributeType<?>) {
+                            String separator = "";
+                            for (final AttributeType<?> attribute : 
((AttributeType<?>) propertyType).characteristics().values()) {
+                                
table.append(separator).append(toString(attribute.getName()));
+                                Object c = attribute.getDefaultValue();
+                                if (feature != null) {
+                                    final Property p = 
feature.getProperty(propertyType.getName().toString());
+                                    if (p instanceof Attribute<?>) {           
 // Should always be true, but we are paranoiac.
+                                        c = ((Attribute<?>) 
p).characteristics().get(attribute.getName().toString());
+                                    }
+                                }
+                                if (c != null) {
+                                    formatValue(c, table.append(" = "), 0);
+                                }
+                                separator = ", ";
+                            }
                         }
-                        separator = ", ";
+                        break;
                     }
-                }
-            }
-            /*
-             * Column 5 - Deprecation
-             */
-            if (hasDeprecatedTypes) {
-                nextColumn(table);
-                if (org.apache.sis.feature.Field.isDeprecated(propertyType)) {
-                    
table.append(resources.getString(Vocabulary.Keys.Deprecated));
-                    final InternationalString r = ((Deprecable) 
propertyType).getRemarks();
-                    if (r != null) {
-                        remarks.add(r.toString(displayLocale));
-                        appendSuperscript(remarks.size(), table);
+                    case REMARKS: {
+                        if 
(org.apache.sis.feature.Field.isDeprecated(propertyType)) {
+                            
table.append(resources.getString(Vocabulary.Keys.Deprecated));
+                            final InternationalString r = ((Deprecable) 
propertyType).getRemarks();
+                            if (r != null) {
+                                remarks.add(r.toString(displayLocale));
+                                appendSuperscript(remarks.size(), table);
+                            }
+                        }
+                        break;
                     }
                 }
             }

Modified: 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java?rev=1803070&r1=1803069&r2=1803070&view=diff
==============================================================================
--- 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java
 [UTF-8] (original)
+++ 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/LinkOperation.java
 [UTF-8] Wed Jul 26 16:14:09 2017
@@ -24,6 +24,7 @@ import org.opengis.parameter.ParameterVa
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.apache.sis.internal.feature.FeatureUtilities;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
 
 // Branch-dependent imports
 import org.opengis.feature.Feature;
@@ -48,11 +49,6 @@ final class LinkOperation extends Abstra
     private static final long serialVersionUID = 765096861589501215L;
 
     /**
-     * The parameter descriptor for the "Link" operation, which does not take 
any parameter.
-     */
-    private static final ParameterDescriptorGroup EMPTY_PARAMS = 
FeatureUtilities.parameters("Link");
-
-    /**
      * The type of the result.
      */
     private final PropertyType result;
@@ -70,10 +66,17 @@ final class LinkOperation extends Abstra
      *
      * @see FeatureOperations#link(Map, PropertyType)
      */
-    LinkOperation(final Map<String,?> identification, final PropertyType 
referent) {
+    LinkOperation(final Map<String,?> identification, PropertyType referent) {
         super(identification);
+        if (referent instanceof LinkOperation) {
+            referent = ((LinkOperation) referent).result;
+            // Avoiding links to links may help performance and reduce the 
risk of circular references.
+        }
         result = referent;
         referentName = referent.getName().toString();
+        if (referentName.equals(getName().toString())) {
+            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.CircularReference));
+        }
     }
 
     /**
@@ -81,7 +84,7 @@ final class LinkOperation extends Abstra
      */
     @Override
     public ParameterDescriptorGroup getParameters() {
-        return EMPTY_PARAMS;
+        return FeatureUtilities.LINK_PARAMS;
     }
 
     /**

Modified: 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java?rev=1803070&r1=1803069&r2=1803070&view=diff
==============================================================================
--- 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java
 [UTF-8] (original)
+++ 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java
 [UTF-8] Wed Jul 26 16:14:09 2017
@@ -22,7 +22,6 @@ import java.util.Objects;
 import java.util.ConcurrentModificationException;
 import org.opengis.metadata.maintenance.ScopeCode;
 import org.opengis.metadata.quality.DataQuality;
-import org.apache.sis.internal.feature.Resources;
 import org.apache.sis.internal.util.Cloner;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.CorruptedObjectException;
@@ -59,7 +58,7 @@ final class SparseFeature extends Abstra
     /**
      * A {@link #valuesKind} flag meaning that the {@link #properties} map 
contains raw values.
      */
-    private static final byte VALUES = 0; // Must be zero, because we want it 
to be 'valuesKind' default value.
+    private static final byte VALUES = 0;   // Must be zero, because we want 
it to be 'valuesKind' default value.
 
     /**
      * A {@link #valuesKind} flag meaning that the {@link #properties} map 
contains {@link Property} instances.
@@ -128,7 +127,7 @@ final class SparseFeature extends Abstra
         if (index != null) {
             return index;
         }
-        throw new 
PropertyNotFoundException(Resources.format(Resources.Keys.PropertyNotFound_2, 
getName(), name));
+        throw new PropertyNotFoundException(propertyNotFound(type, getName(), 
name));
     }
 
     /**
@@ -243,7 +242,7 @@ final class SparseFeature extends Abstra
             } else if (element instanceof FeatureAssociation) {
                 return getAssociationValue((FeatureAssociation) element);
             } else if (valuesKind == PROPERTIES) {
-                throw unsupportedPropertyType(((Property) element).getName());
+                throw new 
IllegalArgumentException(unsupportedPropertyType(((Property) 
element).getName()));
             } else {
                 throw new CorruptedObjectException(getName());
             }
@@ -278,7 +277,7 @@ final class SparseFeature extends Abstra
              * a new value or a value of a different type, then we need to 
check the name and type validity.
              */
             if (!canSkipVerification(previous, value)) {
-                Object toStore = previous; // This initial value will restore 
the previous value if the check fail.
+                Object toStore = previous;  // This initial value will restore 
the previous value if the check fail.
                 try {
                     toStore = verifyPropertyValue(name, value);
                 } finally {

Modified: 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java?rev=1803070&r1=1803069&r2=1803070&view=diff
==============================================================================
--- 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java
 [UTF-8] (original)
+++ 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AssociationRoleBuilder.java
 [UTF-8] Wed Jul 26 16:14:09 2017
@@ -66,7 +66,7 @@ public final class AssociationRoleBuilde
      * @param owner  the builder of the {@code FeatureType} for which to add 
this property.
      */
     AssociationRoleBuilder(final FeatureTypeBuilder owner, final FeatureType 
type, final GenericName typeName) {
-        super(owner, null);
+        super(owner);
         this.type     = type;
         this.typeName = typeName;
     }
@@ -77,7 +77,7 @@ public final class AssociationRoleBuilde
      * @param owner  the builder of the {@code FeatureType} for which to add 
this property.
      */
     AssociationRoleBuilder(final FeatureTypeBuilder owner, final 
FeatureAssociationRole template) {
-        super(owner, template);
+        super(owner);
         property      = template;
         minimumOccurs = template.getMinimumOccurs();
         maximumOccurs = template.getMaximumOccurs();
@@ -88,6 +88,7 @@ public final class AssociationRoleBuilde
             type     = template.getValueType();
             typeName = type.getName();
         }
+        initialize(template);
     }
 
     /**
@@ -130,32 +131,52 @@ public final class AssociationRoleBuilde
     }
 
     /**
-     * Sets the {@code FeatureAssociationRole} 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)}.
-     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName 
local name} if no default scope
-     * has been specified, or a {@linkplain 
org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     * Sets the {@code FeatureAssociationRole} name as a simple string (local 
name).
+     * The namespace will be the value specified by the last call to {@link 
FeatureTypeBuilder#setNameSpace(CharSequence)},
+     * but that namespace will not be visible in the {@linkplain 
org.apache.sis.util.iso.DefaultLocalName#toString()
+     * string representation} unless the {@linkplain 
org.apache.sis.util.iso.DefaultLocalName#toFullyQualifiedName()
+     * fully qualified name} is requested.
+     *
+     * <p>This convenience method creates a {@link org.opengis.util.LocalName} 
instance from
+     * the given {@code CharSequence}, then delegates to {@link 
#setName(GenericName)}.</p>
      *
      * @return {@code this} for allowing method calls chaining.
      */
     @Override
-    public AssociationRoleBuilder setName(final String localPart) {
+    public AssociationRoleBuilder setName(final CharSequence localPart) {
         super.setName(localPart);
         return this;
     }
 
     /**
      * Sets the {@code FeatureAssociationRole} name as a string in the given 
scope.
-     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName 
local name} if the given scope is
-     * {@code null} or empty, or a {@linkplain 
org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
-     * If a {@linkplain FeatureTypeBuilder#setDefaultScope(String) default 
scope} has been specified, then the
-     * {@code scope} argument overrides it.
+     * The {@code components} array must contain at least one element.
+     * The last component (the {@linkplain 
org.apache.sis.util.iso.DefaultScopedName#tip() tip}) will be sufficient
+     * in many cases for calls to the {@link 
org.apache.sis.feature.AbstractFeature#getProperty(String)} method.
+     * The other elements before the last one are optional and can be used for 
resolving ambiguity.
+     * They will be visible as the name {@linkplain 
org.apache.sis.util.iso.DefaultScopedName#path() path}.
+     *
+     * <div class="note"><b>Example:</b>
+     * a call to {@code setName("A", "B", "C")} will create a "A:B:C" name.
+     * An association built with this name can be obtained from a feature by a 
call to {@code feature.getProperty("C")}
+     * if there is no ambiguity, or otherwise by a call to {@code 
feature.getProperty("B:C")} (if non-ambiguous) or
+     * {@code feature.getProperty("A:B:C")}.</div>
+     *
+     * In addition to the path specified by the {@code components} array, the 
name may also contain
+     * a namespace specified by the last call to {@link 
FeatureTypeBuilder#setNameSpace(CharSequence)}.
+     * But contrarily to the specified components, the namespace will not be 
visible in the name
+     * {@linkplain org.apache.sis.util.iso.DefaultScopedName#toString() string 
representation} unless the
+     * {@linkplain 
org.apache.sis.util.iso.DefaultScopedName#toFullyQualifiedName() fully 
qualified name} is requested.
+     *
+     * <p>This convenience method creates a {@link org.opengis.util.LocalName} 
or {@link org.opengis.util.ScopedName}
+     * instance depending on whether the {@code names} array contains exactly 
1 element or more than 1 element, then
+     * delegates to {@link #setName(GenericName)}.</p>
      *
      * @return {@code this} for allowing method calls chaining.
      */
     @Override
-    public AssociationRoleBuilder setName(final String scope, final String 
localPart) {
-        super.setName(scope, localPart);
+    public AssociationRoleBuilder setName(final CharSequence... components) {
+        super.setName(components);
         return this;
     }
 

Modified: 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java?rev=1803070&r1=1803069&r2=1803070&view=diff
==============================================================================
--- 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
 [UTF-8] (original)
+++ 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/builder/AttributeTypeBuilder.java
 [UTF-8] Wed Jul 26 16:14:09 2017
@@ -129,7 +129,7 @@ public final class AttributeTypeBuilder<
      * @param valueClass  the class of attribute values.
      */
     AttributeTypeBuilder(final FeatureTypeBuilder owner, final Class<V> 
valueClass) {
-        super(owner, null);
+        super(owner);
         this.valueClass = valueClass;
         characteristics = new ArrayList<>();
     }
@@ -140,7 +140,7 @@ public final class AttributeTypeBuilder<
      * @param owner  the builder of the {@code FeatureType} for which to add 
the attribute.
      */
     AttributeTypeBuilder(final FeatureTypeBuilder owner, final 
AttributeType<V> template) {
-        super(owner, template);
+        super(owner);
         property      = template;
         minimumOccurs = template.getMinimumOccurs();
         maximumOccurs = template.getMaximumOccurs();
@@ -151,6 +151,7 @@ public final class AttributeTypeBuilder<
         for (final AttributeType<?> c : tc.values()) {
             characteristics.add(new CharacteristicTypeBuilder<>(this, c));
         }
+        initialize(template);
     }
 
     /**
@@ -185,32 +186,52 @@ public final class AttributeTypeBuilder<
     }
 
     /**
-     * Sets the {@code AttributeType} 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)}.
-     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName 
local name} if no default scope
-     * has been specified, or a {@linkplain 
org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     * Sets the {@code AttributeType} name as a simple string (local name).
+     * The namespace will be the value specified by the last call to {@link 
FeatureTypeBuilder#setNameSpace(CharSequence)},
+     * but that namespace will not be visible in the {@linkplain 
org.apache.sis.util.iso.DefaultLocalName#toString()
+     * string representation} unless the {@linkplain 
org.apache.sis.util.iso.DefaultLocalName#toFullyQualifiedName()
+     * fully qualified name} is requested.
+     *
+     * <p>This convenience method creates a {@link org.opengis.util.LocalName} 
instance from
+     * the given {@code CharSequence}, then delegates to {@link 
#setName(GenericName)}.</p>
      *
      * @return {@code this} for allowing method calls chaining.
      */
     @Override
-    public AttributeTypeBuilder<V> setName(final String localPart) {
+    public AttributeTypeBuilder<V> setName(final CharSequence localPart) {
         super.setName(localPart);
         return this;
     }
 
     /**
      * Sets the {@code AttributeType} name as a string in the given scope.
-     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName 
local name} if the given scope is
-     * {@code null} or empty, or a {@linkplain 
org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
-     * If a {@linkplain FeatureTypeBuilder#setDefaultScope(String) default 
scope} has been specified, then the
-     * {@code scope} argument overrides it.
+     * The {@code components} array must contain at least one element.
+     * The last component (the {@linkplain 
org.apache.sis.util.iso.DefaultScopedName#tip() tip}) will be sufficient
+     * in many cases for calls to the {@link 
org.apache.sis.feature.AbstractFeature#getProperty(String)} method.
+     * The other elements before the last one are optional and can be used for 
resolving ambiguity.
+     * They will be visible as the name {@linkplain 
org.apache.sis.util.iso.DefaultScopedName#path() path}.
+     *
+     * <div class="note"><b>Example:</b>
+     * a call to {@code setName("A", "B", "C")} will create a "A:B:C" name.
+     * An attribute built with this name can be obtained from a feature by a 
call to {@code feature.getProperty("C")}
+     * if there is no ambiguity, or otherwise by a call to {@code 
feature.getProperty("B:C")} (if non-ambiguous) or
+     * {@code feature.getProperty("A:B:C")}.</div>
+     *
+     * In addition to the path specified by the {@code components} array, the 
name may also contain
+     * a namespace specified by the last call to {@link 
FeatureTypeBuilder#setNameSpace(CharSequence)}.
+     * But contrarily to the specified components, the namespace will not be 
visible in the name
+     * {@linkplain org.apache.sis.util.iso.DefaultScopedName#toString() string 
representation} unless the
+     * {@linkplain 
org.apache.sis.util.iso.DefaultScopedName#toFullyQualifiedName() fully 
qualified name} is requested.
+     *
+     * <p>This convenience method creates a {@link org.opengis.util.LocalName} 
or {@link org.opengis.util.ScopedName}
+     * instance depending on whether the {@code names} array contains exactly 
1 element or more than 1 element, then
+     * delegates to {@link #setName(GenericName)}.</p>
      *
      * @return {@code this} for allowing method calls chaining.
      */
     @Override
-    public AttributeTypeBuilder<V> setName(final String scope, final String 
localPart) {
-        super.setName(scope, localPart);
+    public AttributeTypeBuilder<V> setName(final CharSequence... components) {
+        super.setName(components);
         return this;
     }
 

Modified: 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/builder/CharacteristicTypeBuilder.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/builder/CharacteristicTypeBuilder.java?rev=1803070&r1=1803069&r2=1803070&view=diff
==============================================================================
--- 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/builder/CharacteristicTypeBuilder.java
 [UTF-8] (original)
+++ 
sis/branches/JDK9/core/sis-feature/src/main/java/org/apache/sis/feature/builder/CharacteristicTypeBuilder.java
 [UTF-8] Wed Jul 26 16:14:09 2017
@@ -95,7 +95,7 @@ public final class CharacteristicTypeBui
      * @param valueClass  the class of characteristic values.
      */
     CharacteristicTypeBuilder(final AttributeTypeBuilder<?> owner, final 
Class<V> valueClass) {
-        super(null, owner.getLocale());
+        super(owner.getLocale());
         this.owner = owner;
         this.valueClass = valueClass;
     }
@@ -106,11 +106,12 @@ public final class CharacteristicTypeBui
      * @param owner  the builder of the {@code AttributeType} for which to add 
this property.
      */
     CharacteristicTypeBuilder(final AttributeTypeBuilder<?> owner, final 
AttributeType<V> template) {
-        super(template, owner.getLocale());
+        super(owner.getLocale());
         this.owner     = owner;
         characteristic = template;
         valueClass     = template.getValueClass();
         defaultValue   = template.getDefaultValue();
+        initialize(template);
     }
 
     /**
@@ -146,42 +147,66 @@ public final class CharacteristicTypeBui
     }
 
     /**
-     * Sets the characteristic 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)}.
-     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName 
local name} if no default scope
-     * has been specified, or a {@linkplain 
org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
+     * Sets the characteristic name as a simple string (local name).
+     * The namespace will be the value specified by the last call to {@link 
FeatureTypeBuilder#setNameSpace(CharSequence)},
+     * but that namespace will not be visible in the {@linkplain 
org.apache.sis.util.iso.DefaultLocalName#toString()
+     * string representation} unless the {@linkplain 
org.apache.sis.util.iso.DefaultLocalName#toFullyQualifiedName()
+     * fully qualified name} is requested.
+     *
+     * <p>This convenience method creates a {@link org.opengis.util.LocalName} 
instance from
+     * the given {@code CharSequence}, then delegates to {@link 
#setName(GenericName)}.</p>
      *
      * @return {@code this} for allowing method calls chaining.
      */
     @Override
-    public CharacteristicTypeBuilder<V> setName(final String localPart) {
+    public CharacteristicTypeBuilder<V> setName(final CharSequence localPart) {
         super.setName(localPart);
         return this;
     }
 
     /**
      * Sets the characteristic name as a string in the given scope.
-     * The name will be a {@linkplain org.apache.sis.util.iso.DefaultLocalName 
local name} if the given scope is
-     * {@code null} or empty, or a {@linkplain 
org.apache.sis.util.iso.DefaultScopedName scoped name} otherwise.
-     * If a {@linkplain FeatureTypeBuilder#setDefaultScope(String) default 
scope} has been specified, then the
-     * {@code scope} argument overrides it.
+     * The {@code components} array must contain at least one element.
+     * The last component (the {@linkplain 
org.apache.sis.util.iso.DefaultScopedName#tip() tip}) will be sufficient
+     * in many cases for getting values from the {@linkplain 
org.apache.sis.feature.AbstractAttribute#characteristics()
+     * characteristics} map. The other elements before the last one are 
optional and can be used for resolving ambiguity.
+     * They will be visible as the name {@linkplain 
org.apache.sis.util.iso.DefaultScopedName#path() path}.
+     *
+     * <p>In addition to the path specified by the {@code components} array, 
the name may also contain
+     * a namespace specified by the last call to {@link 
FeatureTypeBuilder#setNameSpace(CharSequence)}.
+     * But contrarily to the specified components, the namespace will not be 
visible in the name
+     * {@linkplain org.apache.sis.util.iso.DefaultScopedName#toString() string 
representation} unless the
+     * {@linkplain 
org.apache.sis.util.iso.DefaultScopedName#toFullyQualifiedName() fully 
qualified name}
+     * is requested.</p>
+     *
+     * <p>This convenience method creates a {@link org.opengis.util.LocalName} 
or {@link org.opengis.util.ScopedName}
+     * instance depending on whether the {@code names} array contains exactly 
1 element or more than 1 element, then
+     * delegates to {@link #setName(GenericName)}.</p>
      *
      * @return {@code this} for allowing method calls chaining.
      */
     @Override
-    public CharacteristicTypeBuilder<V> setName(final String scope, final 
String localPart) {
-        super.setName(scope, localPart);
+    public CharacteristicTypeBuilder<V> setName(final CharSequence... 
components) {
+        super.setName(components);
         return this;
     }
 
     /**
-     * Delegates the creation of a new name to the enclosing builder.
+     * Creates a local name in the {@linkplain FeatureTypeBuilder#setNameSpace 
feature namespace}.
+     */
+    @Override
+    final GenericName createLocalName(final CharSequence name) {
+        ensureAlive(owner);
+        return owner.createLocalName(name);
+    }
+
+    /**
+     * Creates a generic name in the {@linkplain 
FeatureTypeBuilder#setNameSpace feature namespace}.
      */
     @Override
-    final GenericName name(final String scope, final String localPart) {
+    final GenericName createGenericName(final CharSequence... names) {
         ensureAlive(owner);
-        return owner.name(scope, localPart);
+        return owner.createGenericName(names);
     }
 
     /**


Reply via email to