sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Modify the way attribute values are stored in netCDF reader. This change makes easier to fix parsing of map projection parameters.
Date Tue, 28 May 2019 17:05:48 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new f646188  Modify the way attribute values are stored in netCDF reader. This change makes easier to fix parsing of map projection parameters.
f646188 is described below

commit f6461882be269d21254cdc7b3651966ebaef4320
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Tue May 28 19:04:58 2019 +0200

    Modify the way attribute values are stored in netCDF reader.
    This change makes easier to fix parsing of map projection parameters.
---
 .../org/apache/sis/internal/netcdf/Convention.java |  65 ++++----
 .../org/apache/sis/internal/netcdf/DataType.java   |   2 +-
 .../org/apache/sis/internal/netcdf/Decoder.java    |  31 +++-
 .../java/org/apache/sis/internal/netcdf/Node.java  | 170 ++++++++++++++++-----
 .../apache/sis/internal/netcdf/RasterResource.java |  28 ++--
 .../org/apache/sis/internal/netcdf/Resources.java  |   5 +
 .../sis/internal/netcdf/Resources.properties       |   1 +
 .../sis/internal/netcdf/Resources_fr.properties    |   1 +
 .../sis/internal/netcdf/impl/ChannelDecoder.java   |  53 ++++---
 .../sis/internal/netcdf/impl/FeaturesInfo.java     |  18 +--
 .../sis/internal/netcdf/impl/VariableInfo.java     | 149 +++---------------
 .../sis/internal/netcdf/ucar/DecoderWrapper.java   |   2 +-
 .../sis/internal/netcdf/ucar/GroupWrapper.java     |   8 +-
 .../sis/internal/netcdf/ucar/VariableWrapper.java  |  54 ++++---
 .../apache/sis/storage/netcdf/MetadataReader.java  |  26 ++--
 .../org/apache/sis/internal/netcdf/GridTest.java   |  23 ++-
 .../org/apache/sis/internal/netcdf/TestCase.java   |   2 +-
 .../apache/sis/internal/netcdf/VariableTest.java   |  47 +++++-
 .../sis/internal/netcdf/impl/VariableInfoTest.java |  13 --
 19 files changed, 393 insertions(+), 305 deletions(-)

diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
index 0c59f8e..699db49 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Convention.java
@@ -439,7 +439,7 @@ public class Convention {
         }
         final Map<String,Object> definition = new HashMap<>();
         definition.put(CF.GRID_MAPPING_NAME, method);
-        for (final String name : node.getAttributeNames()) {
+        for (final String name : node.getAttributeNames()) try {
             final String ln = name.toLowerCase(Locale.US);
             Object value;
             switch (ln) {
@@ -450,10 +450,10 @@ public class Convention {
                      * with the CRS is deprecated (the hard-coded WGS84 target datum is not always suitable) but still
                      * a common practice as of 2019. We require at least the 3 translation parameters.
                      */
-                    final Object[] values = node.getAttributeValues(name, true);
-                    if (values.length < 3) continue;
+                    final Vector values = node.getAttributeAsVector(name);
+                    if (values == null || values.size() < 3) continue;
                     final BursaWolfParameters bp = new BursaWolfParameters(CommonCRS.WGS84.datum(), null);
-                    bp.setValues(Vector.create(values, false).doubleValues());
+                    bp.setValues(values.doubleValues());
                     value = bp;
                     break;
                 }
@@ -472,23 +472,14 @@ public class Convention {
                     }
                     /*
                      * Assume that all map projection parameters in netCDF files are numbers or array of numbers.
-                     * If the array contains float value, returns a `float[]` array for letting the caller know
-                     * that it may need to use base 10 for conversion to `double[]` array.
+                     * If values are array, then they are converted to an array of {@code double[]} type.
                      */
-                    final Object[] values = node.getAttributeValues(name, true);
-                    switch (values.length) {
-                        case 0:  continue;                       // Attribute not found or not numeric.
-                        case 1:  value = values[0]; break;       // This is the usual case.
-                        default: {
-                            boolean isFloat = false;
-                            for (final Object e : values) {
-                                isFloat = (e instanceof Float);
-                                if (!isFloat) break;
-                            }
-                            final Vector v = Vector.create(values, false);
-                            value = isFloat ? v.floatValues() : v.doubleValues();
-                            break;
-                        }
+                    final Vector data = node.getAttributeAsVector(name);
+                    if (data == null) continue;
+                    switch (data.size()) {
+                        case 0:  continue;
+                        case 1:  value = data.get(0); break;
+                        default: value = data.doubleValues(); break;
                     }
                     break;
                 }
@@ -496,6 +487,9 @@ public class Convention {
             if (definition.putIfAbsent(name, value) != null) {
                 node.error(Convention.class, "projection", null, Errors.Keys.DuplicatedIdentifier_1, name);
             }
+        } catch (NumberFormatException e) {
+            // May happen in the vector contains number stored as texts.
+            node.decoder.illegalAttributeValue(name, node.getAttributeAsString(name), e);
         }
         return definition;
     }
@@ -574,24 +568,28 @@ public class Convention {
         Number maximum = null;
         Class<? extends Number> type = null;
         for (final String attribute : RANGE_ATTRIBUTES) {
-            for (final Object element : data.getAttributeValues(attribute, true)) {
-                if (element instanceof Number) {
-                    Number value = (Number) element;
-                    if (element instanceof Float) {
-                        final float fp = (Float) element;
+            final Vector values = data.getAttributeAsVector(attribute);
+            if (values != null) {
+                final int length = values.size();
+                for (int i=0; i<length; i++) try {
+                    Number value = values.get(i);           // May throw NumberFormatException if value was stored as text.
+                    if (value instanceof Float) {
+                        final float fp = (Float) value;
                         if      (fp == +Float.MAX_VALUE) value = Float.POSITIVE_INFINITY;
                         else if (fp == -Float.MAX_VALUE) value = Float.NEGATIVE_INFINITY;
-                    } else if (element instanceof Double) {
-                        final double fp = (Double) element;
+                    } else if (value instanceof Double) {
+                        final double fp = (Double) value;
                         if      (fp == +Double.MAX_VALUE) value = Double.POSITIVE_INFINITY;
                         else if (fp == -Double.MAX_VALUE) value = Double.NEGATIVE_INFINITY;
                     }
-                    type = Numbers.widestClass(type, value.getClass());
+                    type    = Numbers.widestClass(type, value.getClass());
                     minimum = Numbers.cast(minimum, type);
                     maximum = Numbers.cast(maximum, type);
                     value   = Numbers.cast(value,   type);
                     if (!attribute.endsWith("max") && (minimum == null || compare(value, minimum) < 0)) minimum = value;
                     if (!attribute.endsWith("min") && (maximum == null || compare(value, maximum) > 0)) maximum = value;
+                } catch (NumberFormatException e) {
+                    data.decoder.illegalAttributeValue(attribute, values.stringValue(i), e);
                 }
             }
             if (minimum != null && maximum != null) {
@@ -651,9 +649,14 @@ public class Convention {
     public Map<Number,Object> nodataValues(final Variable data) {
         final Map<Number,Object> pads = new LinkedHashMap<>();
         for (int i=0; i < NODATA_ATTRIBUTES.length; i++) {
-            for (final Object value : data.getAttributeValues(NODATA_ATTRIBUTES[i], true)) {
-                if (value instanceof Number) {
-                    pads.merge((Number) value, 1 << i, (v1, v2) -> ((Integer) v1) | ((Integer) v2));
+            final String name = NODATA_ATTRIBUTES[i];
+            final Vector values = data.getAttributeAsVector(name);
+            if (values != null) {
+                final int length = values.size();
+                for (int j=0; j<length; j++) try {
+                    pads.merge(values.get(i), 1 << i, (v1, v2) -> ((Integer) v1) | ((Integer) v2));
+                } catch (NumberFormatException e) {
+                    data.decoder.illegalAttributeValue(name, values.stringValue(i), e);
                 }
             }
         }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DataType.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DataType.java
index b86a8cc..45a4184 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DataType.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/DataType.java
@@ -94,7 +94,7 @@ public enum DataType {
     USHORT(Numbers.SHORT, true, true, (byte) 3, DataBuffer.TYPE_USHORT),
 
     /**
-     * 43 bits unsigned integer (netCDF type 9).
+     * 32 bits unsigned integer (netCDF type 9).
      * Not available in netCDF classic format.
      */
     UINT(Numbers.INTEGER, true, true, (byte) 4, DataBuffer.TYPE_INT),
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
index 6e069df..4977861 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Decoder.java
@@ -229,24 +229,45 @@ public abstract class Decoder extends ReferencingFactoryContainer implements Clo
     /**
      * Convenience method for {@link #numericValue(String)} implementation.
      *
+     * @param  name   the attribute name, used only in case of error.
      * @param  value  the attribute value to parse.
      * @return the parsed attribute value, or {@code null} if the given value can not be parsed.
      */
-    protected final Number parseNumber(String value) {
+    protected final Number parseNumber(final String name, String value) {
         final int s = value.indexOf(' ');
         if (s >= 0) {
             /*
              * Sometime, numeric values as string are followed by
-             * a unit of measurement. We ignore that unit for now...
+             * a unit of measurement. We ignore that unit for now.
              */
             value = value.substring(0, s);
         }
+        Number n;
         try {
-            return Double.valueOf(value);
+            if (value.indexOf('.') >= 0) {
+                n = Double.valueOf(value);
+            } else {
+                n = Long.valueOf(value);
+            }
         } catch (NumberFormatException e) {
-            listeners.warning(null, e);
+            illegalAttributeValue(name, value, e);
+            n = null;
         }
-        return null;
+        return n;
+    }
+
+    /**
+     * Logs a warning for an illegal attribute value. This may be due to a failure to parse a string as a number.
+     * This method should be invoked from methods that are invoked only once per attribute because we do not keep
+     * track of which warnings have already been emitted.
+     *
+     * @param  name   the attribute name.
+     * @param  value  the illegal value.
+     * @param  e      the exception, or {@code null} if none.
+     */
+    final void illegalAttributeValue(final String name, final String value, final NumberFormatException e) {
+        listeners.warning(Resources.forLocale(listeners.getLocale()).getString(
+                Resources.Keys.IllegalAttributeValue_3, getFilename(), name, value), e);
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Node.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Node.java
index 0135eef..876920a 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Node.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Node.java
@@ -18,7 +18,9 @@ package org.apache.sis.internal.netcdf;
 
 import java.util.Locale;
 import java.util.Collection;
+import org.apache.sis.math.Vector;
 import org.apache.sis.math.DecimalFunctions;
+import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.resources.Errors;
 
 
@@ -54,8 +56,8 @@ public abstract class Node extends NamedElement {
     public abstract Collection<String> getAttributeNames();
 
     /**
-     * Returns the type of the attribute of the given name,
-     * or {@code null} if the given attribute is not found.
+     * Returns the type of the attribute of the given name, or {@code null} if the given attribute is not found.
+     * If the attribute contains more than one value, then this method returns {@code Vector.class}.
      *
      * @param  attributeName  the name of the attribute for which to get the type.
      * @return type of the given attribute, or {@code null} if the attribute does not exist.
@@ -65,56 +67,111 @@ public abstract class Node extends NamedElement {
     public abstract Class<?> getAttributeType(String attributeName);
 
     /**
-     * Returns the sequence of values for the given attribute, or an empty array if none.
-     * The elements will be of class {@link String} if {@code numeric} is {@code false},
-     * or {@link Number} if {@code numeric} is {@code true}. Some elements may be null
-     * if they are not of the expected type.
+     * Returns the single value or vector of values for the given attribute, or {@code null} if none.
+     * The returned value can be an instance of:
+     *
+     * <ul>
+     *   <li>{@link String} if the attribute contains a single textual value.</li>
+     *   <li>{@link Number} if the attribute contains a single numerical value.</li>
+     *   <li>{@link Vector} if the attribute contains many numerical values.</li>
+     *   <li>{@code String[]} if the attribute contains many textual values.</li>
+     * </ul>
+     *
+     * If the value is a {@code String}, then leading and trailing spaces and control characters
+     * should be trimmed by {@link String#trim()}.
      *
      * @param  attributeName  the name of the attribute for which to get the values.
-     * @param  numeric        {@code true} if the values are expected to be numeric, or {@code false} for strings.
-     * @return the sequence of {@link String} or {@link Number} values for the named attribute.
-     *         May contain null elements.
+     * @return value(s) for the named attribute, or {@code null} if none.
      */
-    public abstract Object[] getAttributeValues(String attributeName, boolean numeric);
+    protected abstract Object getAttributeValue(String attributeName);
 
     /**
-     * Returns the singleton value for the given attribute, or {@code null} if none or ambiguous.
+     * Returns the value of the given attribute as a non-blank string with leading/trailing spaces removed.
      *
      * @param  attributeName  the name of the attribute for which to get the value.
-     * @param  numeric        {@code true} if the value is expected to be numeric, or {@code false} for string.
-     * @return the {@link String} or {@link Number} value for the named attribute.
+     * @return the singleton attribute value, or {@code null} if none, empty, blank or ambiguous.
      */
-    private Object getAttributeValue(final String attributeName, final boolean numeric) {
-        Object singleton = null;
-        for (final Object value : getAttributeValues(attributeName, numeric)) {
-            if (value != null) {
-                if (singleton != null && !singleton.equals(value)) {              // Paranoiac check.
-                    return null;
-                }
-                singleton = value;
+    public final String getAttributeAsString(final String attributeName) {
+        final Object value = getAttributeValue(attributeName);
+        if (value == null || value instanceof String) {
+            return (String) value;
+        }
+        final String[] values = toArray(value);
+        if (values == null) {
+            return value.toString();
+        }
+        String singleton = null;
+        for (final String c : values) {
+            if (singleton == null) {
+                singleton = c;
+            } else if (!singleton.equals(c)) {
+                return null;
             }
         }
         return singleton;
     }
 
     /**
-     * Returns the value of the given attribute as a non-blank string with leading/trailing spaces removed.
-     * This is a convenience method for {@link #getAttributeValues(String, boolean)} when a singleton value
-     * is expected and blank strings ignored.
+     * Returns the values of the given attribute as an array of non-blank texts.
+     * If the attribute is not stored as an array in the netCDF file, then this
+     * method splits the single {@link String} value around the given separator.
      *
-     * @param  attributeName  the name of the attribute for which to get the value.
-     * @return the singleton attribute value, or {@code null} if none, empty, blank or ambiguous.
+     * @param  attributeName  the name of the attribute for which to get the values.
+     * @param  separator      separator to use for splitting a single {@link String} value into a list of values.
+     * @return the attribute values, or {@code null} if none.
      */
-    public String getAttributeAsString(final String attributeName) {
-        final Object value = getAttributeValue(attributeName, false);
+    public final CharSequence[] getAttributeAsStrings(final String attributeName, final char separator) {
+        final Object value = getAttributeValue(attributeName);
         if (value != null) {
-            final String text = value.toString().trim();
-            if (!text.isEmpty()) return text;
+            CharSequence[] ts = toArray(value);
+            if (ts == null) {
+                ts = CharSequences.split(value.toString(), separator);
+            }
+            if (ts.length != 0) {
+                return ts;
+            }
         }
         return null;
     }
 
     /**
+     * Converts the given value into an array of strings, or returns {@code null}
+     * if the given value is not an array or a vector or contains only null values.
+     */
+    private static String[] toArray(final Object value) {
+        final String[] array;
+        if (value instanceof Object[]) {
+            final Object[] values = (Object[]) value;
+            array = new String[values.length];
+            for (int i=0; i<array.length; i++) {
+                final Object e = values[i];
+                if (e != null) {
+                    array[i] = e.toString();
+                }
+            }
+        } else if (value instanceof Vector) {
+            final Vector values = (Vector) value;
+            array = new String[values.size()];
+            for (int i=0; i<array.length; i++) {
+                array[i] = values.stringValue(i);
+            }
+        } else {
+            return null;
+        }
+        boolean hasValues = false;
+        for (int i=0; i<array.length; i++) {
+            String e = array[i];
+            if (e != null) {
+                e = e.trim();
+                if (e.isEmpty()) e = null;
+                else hasValues = true;
+                array[i] = e;
+            }
+        }
+        return hasValues ? array : null;
+    }
+
+    /**
      * Returns the value of the given attribute as a number, or {@link Double#NaN}.
      * If the number is stored with single-precision, it is assumed casted from a
      * representation in base 10.
@@ -123,16 +180,55 @@ public abstract class Node extends NamedElement {
      * @return the singleton attribute value, or {@code NaN} if none or ambiguous.
      */
     public final double getAttributeAsNumber(final String attributeName) {
-        final Object value = getAttributeValue(attributeName, true);
+        final Object value = getAttributeValue(attributeName);
+        Number singleton = null;
         if (value instanceof Number) {
-            double dp = ((Number) value).doubleValue();
-            final float sp = (float) dp;
-            if (sp == dp) {                              // May happen even if the number was stored as a double.
-                dp = DecimalFunctions.floatToDouble(sp);
+            singleton = (Number) value;
+        } else if (value instanceof String) {
+            singleton = decoder.parseNumber(attributeName, (String) value);
+        } else if (value instanceof Vector) {
+            final Vector data = (Vector) value;
+            final int length = data.size();
+            for (int i=0; i<length; i++) {
+                final Number n = data.get(i);
+                if (n != null) {
+                    if (singleton == null) {
+                        singleton = n;
+                    } else if (!singleton.equals(n)) {
+                        return Double.NaN;
+                    }
+                }
             }
-            return dp;
         }
-        return Double.NaN;
+        if (singleton == null) {
+            return Double.NaN;
+        }
+        double dp = singleton.doubleValue();
+        final float sp = (float) dp;
+        if (sp == dp) {                                     // May happen even if the number was stored as a double.
+            dp = DecimalFunctions.floatToDouble(sp);
+        }
+        return dp;
+    }
+
+    /**
+     * Returns the values of the given attribute as a vector of numbers, or {@code null} if none.
+     * If the numbers are stored with single-precision, they are assumed casted from a representation in base 10.
+     *
+     * @param  attributeName  the name of the attribute for which to get the values.
+     * @return the attribute values, or {@code null} if none, ambiguous or not a vector.
+     */
+    public final Vector getAttributeAsVector(final String attributeName) {
+        final Object value = getAttributeValue(attributeName);
+        if (value instanceof Vector) {
+            return (Vector) value;
+        } else if (value instanceof Float) {
+            return Vector.createForDecimal(new float[] {(Float) value});
+        } else if (value instanceof Number) {
+            return Vector.create(new Number[] {(Number) value}, false);
+        } else {
+            return null;
+        }
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
index 08ffea1..ee94c4a 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/RasterResource.java
@@ -45,8 +45,8 @@ import org.apache.sis.storage.Resource;
 import org.apache.sis.math.MathFunctions;
 import org.apache.sis.measure.MeasurementRange;
 import org.apache.sis.measure.NumberRange;
+import org.apache.sis.math.Vector;
 import org.apache.sis.util.Numbers;
-import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.internal.jdk9.JDK9;
@@ -500,24 +500,22 @@ public final class RasterResource extends AbstractGridResource implements Resour
      * @return {@code true} if flag attributes have been found, or {@code false} otherwise.
      */
     private static boolean createEnumeration(final SampleDimension.Builder builder, final Variable band, final int index) {
-        Object[] names = band.getAttributeValues(AttributeNames.FLAG_NAMES, false);
-        if (names.length == 0) {
-            names = band.getAttributeValues(AttributeNames.FLAG_MEANINGS, false);
-            if (names.length == 0) return false;
+        CharSequence[] names = band.getAttributeAsStrings(AttributeNames.FLAG_NAMES, ' ');
+        if (names == null) {
+            names = band.getAttributeAsStrings(AttributeNames.FLAG_MEANINGS, ' ');
+            if (names == null) return false;
         }
-        Object[] values = band.getAttributeValues(AttributeNames.FLAG_VALUES, true);
-        if (values.length == 0) {
-            values = band.getAttributeValues(AttributeNames.FLAG_MASKS, true);
-            if (values.length == 0) return false;
+        Vector values = band.getAttributeAsVector(AttributeNames.FLAG_VALUES);
+        if (values == null) {
+            values = band.getAttributeAsVector(AttributeNames.FLAG_MASKS);
+            if (values == null) return false;
         }
-        if (names.length == 1) {
-            names = CharSequences.split((CharSequence) names[0], ' ');
-        }
-        for (int i=0; i<values.length; i++) {
-            final Number value = (Number) values[i];
+        final int length = values.size();
+        for (int i=0; i<length; i++) {
+            final Number value = values.get(i);
             final CharSequence name;
             if (i < names.length) {
-                name = (CharSequence) names[i];
+                name = names[i];
             } else {
                 name = Vocabulary.formatInternational(Vocabulary.Keys.Unnamed);
             }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
index 4baff51..be7780a 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.java
@@ -118,6 +118,11 @@ public final class Resources extends IndexedResourceBundle {
         public static final short DuplicatedAxis_2 = 7;
 
         /**
+         * Illegal value “{2}” for attribute “{1}” in netCDF file “{0}”.
+         */
+        public static final short IllegalAttributeValue_3 = 21;
+
+        /**
          * Illegal value range {2,number} … {3,number} for variable “{1}” in netCDF file “{0}”.
          */
         public static final short IllegalValueRange_4 = 16;
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
index 82f6080..839956f 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources.properties
@@ -30,6 +30,7 @@ CanNotUseAxis_1                   = Can not use axis \u201c{0}\u201d in a grid g
 CanNotUseUCAR                     = Can not use UCAR library for netCDF format. Fallback on Apache SIS implementation.
 DimensionNotFound_3               = Dimension \u201c{2}\u201d declared by attribute \u201c{1}\u201d is not found in the \u201c{0}\u201d file.
 DuplicatedAxis_2                  = Duplicated axis \u201c{1}\u201d in a grid of netCDF file \u201c{0}\u201d.
+IllegalAttributeValue_3           = Illegal value \u201c{2}\u201d for attribute \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
 IllegalValueRange_4               = Illegal value range {2,number} \u2026 {3,number} for variable \u201c{1}\u201d in netCDF file \u201c{0}\u201d.
 MismatchedVariableSize_3          = The declared size of variable \u201c{1}\u201d in netCDF file \u201c{0}\u201d is {2,number} bytes greater than expected.
 MismatchedVariableType_3          = Variables \u201c{1}\u201d and \u201c{2}\u201d in netCDF file \u201c{0}\u201d does not have the same type.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
index 79e59f4..bf8df73 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Resources_fr.properties
@@ -35,6 +35,7 @@ CanNotUseAxis_1                   = Ne peut pas utiliser l\u2019axe \u00ab\u202f
 CanNotUseUCAR                     = Ne peut pas utiliser la biblioth\u00e8que de l\u2019UCAR pour le format netCDF. L\u2019impl\u00e9mentation de Apache SIS sera utilis\u00e9e \u00e0 la place.
 DimensionNotFound_3               = La dimension \u00ab\u202f{2}\u202f\u00bb d\u00e9clar\u00e9e par l\u2019attribut \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e dans le fichier \u00ab\u202f{0}\u202f\u00bb.
 DuplicatedAxis_2                  = Axe \u00ab\u202f{1}\u202f\u00bb dupliqu\u00e9 dans une grille du fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
+IllegalAttributeValue_3           = La valeur \u00ab\u202f{2}\u202f\u00bb est ill\u00e9gale pour l\u2019attribut \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
 IllegalValueRange_4               = Plage de valeurs {2,number} \u2026 {3,number} ill\u00e9gale pour la variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb.
 MismatchedVariableSize_3          = La longueur d\u00e9clar\u00e9e de la variable \u00ab\u202f{1}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb d\u00e9passe de {2,number} octets la valeur attendue.
 MismatchedVariableType_3          = Les variables \u00ab\u202f{1}\u202f\u00bb et \u00ab\u202f{2}\u202f\u00bb dans le fichier netCDF \u00ab\u202f{0}\u202f\u00bb ne sont pas du m\u00eame type.
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java
index 70113e6..38a7804 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/ChannelDecoder.java
@@ -57,11 +57,13 @@ import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.util.iso.DefaultNameSpace;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.logging.WarningListeners;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.measure.Units;
+import org.apache.sis.math.Vector;
 import ucar.nc2.constants.CF;
 
 
@@ -184,6 +186,7 @@ public final class ChannelDecoder extends Decoder {
     /**
      * The attributes found in the netCDF file.
      * Values in this map give directly the attribute value (there is no {@code Attribute} object).
+     * Values are {@link String}, wrappers such as {@link Double}, or {@link Vector} objects.
      *
      * @see #findAttribute(String)
      */
@@ -416,8 +419,8 @@ public final class ChannelDecoder extends Decoder {
 
     /**
      * Returns the values of the given type. In the given type is {@code CHAR}, then this method returns the values
-     * as a {@link String}. Otherwise this method returns the value as an array of the corresponding primitive type
-     * and the given length.
+     * as a {@link String}. Otherwise if the length is 1, then this method returns the primitive value in its wrapper.
+     * Otherwise this method returns the value as a {@link Vector} of the corresponding primitive type and given length.
      *
      * <p>If the value is a {@code String}, then leading and trailing spaces and control characters have been trimmed
      * by {@link String#trim()}.</p>
@@ -425,10 +428,10 @@ public final class ChannelDecoder extends Decoder {
      * @return the value, or {@code null} if it was an empty string or an empty array.
      */
     private Object readValues(final DataType type, final int length) throws IOException, DataStoreContentException {
-        if (length == 0) {
-            return null;
-        }
-        if (length < 0) {
+        if (length <= 0) {
+            if (length == 0) {
+                return null;
+            }
             throw malformedHeader();
         }
         if (length == 1) {
@@ -444,6 +447,7 @@ public final class ChannelDecoder extends Decoder {
                 case DOUBLE: return input.readDouble();
             }
         }
+        final Object data;
         switch (type) {
             case CHAR: {
                 final String text = input.readString(length, encoding);
@@ -455,41 +459,49 @@ public final class ChannelDecoder extends Decoder {
                 final byte[] array = new byte[length];
                 input.readFully(array);
                 align(length);
-                return array;
+                data = array;
+                break;
             }
             case SHORT:
             case USHORT: {
                 final short[] array = new short[length];
                 input.readFully(array, 0, length);
                 align(length << 1);
-                return array;
+                data = array;
+                break;
             }
             case INT:
             case UINT: {
                 final int[] array = new int[length];
                 input.readFully(array, 0, length);
-                return array;
+                data =  array;
+                break;
             }
             case INT64:
             case UINT64: {
                 final long[] array = new long[length];
                 input.readFully(array, 0, length);
-                return array;
+                data = array;
+                break;
             }
             case FLOAT: {
                 final float[] array = new float[length];
                 input.readFully(array, 0, length);
-                return array;
+                return Vector.createForDecimal(array);
             }
             case DOUBLE: {
                 final double[] array = new double[length];
                 input.readFully(array, 0, length);
-                return array;
+                final float[] asFloats = ArraysExt.copyAsFloatsIfLossless(array);
+                if (asFloats != null) return Vector.createForDecimal(asFloats);
+                data = array;
+                break;
             }
             default: {
                 throw malformedHeader();
             }
         }
+        return Vector.create(data, type.isUnsigned);
     }
 
     /**
@@ -532,8 +544,9 @@ public final class ChannelDecoder extends Decoder {
      *   <li>The actual values as a variable length list    (use {@link #readValues(DataType,int)})</li>
      * </ul>
      *
-     * If the value is a {@code String}, then leading and trailing spaces and control characters
-     * have been trimmed by {@link String#trim()}.
+     * If the value is a {@code String}, then leading and trailing spaces and control characters have been
+     * trimmed by {@link String#trim()}. If the value has more than one element, then the values are stored
+     * in a {@link Vector}.
      *
      * @param  nelems  the number of attributes to read.
      */
@@ -813,11 +826,15 @@ public final class ChannelDecoder extends Decoder {
     @Override
     public Number numericValue(final String name) {
         final Object value = findAttribute(name);
-        if (value instanceof String) {
-            return parseNumber((String) value);
+        if (value instanceof Number) {
+            return (Number) value;
+        } else if (value instanceof String) {
+            return parseNumber(name, (String) value);
+        } else if (value instanceof Vector) {
+            return ((Vector) value).get(0);
+        } else {
+            return null;
         }
-        final Number[] v = VariableInfo.numberValues(value);
-        return (v.length != 0) ? v[0] : null;
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
index 0bcd1fb..373f7af 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/FeaturesInfo.java
@@ -156,8 +156,8 @@ final class FeaturesInfo extends DiscreteSampling {
      * Returns {@code true} if the given attribute value is one of the {@code cf_role} attribute values
      * supported by this implementation.
      */
-    private static boolean isSupportedRole(final Object role) {
-        return (role instanceof String) && ((String) role).equalsIgnoreCase(CF.TRAJECTORY_ID);
+    private static boolean isSupportedRole(final String role) {
+        return CF.TRAJECTORY_ID.equalsIgnoreCase(role);
     }
 
     /**
@@ -186,10 +186,10 @@ search: for (final VariableInfo counts : decoder.variables) {
              *             counts:sample_dimension = "points";
              */
             if (counts.dimensions.length == 1 && counts.getDataType().isInteger) {
-                final Object sampleDimName = counts.getAttributeValue(CF.SAMPLE_DIMENSION);
-                if (sampleDimName instanceof String) {
+                final String sampleDimName = counts.getAttributeAsString(CF.SAMPLE_DIMENSION);
+                if (sampleDimName != null) {
                     final DimensionInfo featureDimension = counts.dimensions[0];
-                    final DimensionInfo sampleDimension = decoder.findDimension((String) sampleDimName);
+                    final DimensionInfo sampleDimension = decoder.findDimension(sampleDimName);
                     if (sampleDimension == null) {
                         decoder.listeners.warning(decoder.resources().getString(Resources.Keys.DimensionNotFound_3,
                                 decoder.getFilename(), counts.getName(), sampleDimName), null);
@@ -203,11 +203,11 @@ search: for (final VariableInfo counts : decoder.variables) {
                      * part of CF convention and will be accepted only if there is no ambiguity.
                      */
                     VariableInfo identifiers = decoder.findVariable(featureDimension.name);
-                    if (identifiers == null || !isSupportedRole(identifiers.getAttributeValue(CF.CF_ROLE))) {
+                    if (identifiers == null || !isSupportedRole(identifiers.getAttributeAsString(CF.CF_ROLE))) {
                         VariableInfo replacement = null;
                         for (final VariableInfo alt : decoder.variables) {
                             if (alt.dimensions.length != 0 && alt.dimensions[0] == featureDimension
-                                    && isSupportedRole(alt.getAttributeValue(CF.CF_ROLE)))
+                                    && isSupportedRole(alt.getAttributeAsString(CF.CF_ROLE)))
                             {
                                 if (replacement != null) {
                                     replacement = null;
@@ -267,10 +267,10 @@ search: for (final VariableInfo counts : decoder.variables) {
                     final List<VariableInfo> properties  = new ArrayList<>();
                     for (final VariableInfo data : decoder.variables) {
                         if (data.dimensions.length == 1 && data.dimensions[0] == sampleDimension) {
-                            final Object axisType = data.getAttributeValue(CF.AXIS);
+                            final String axisType = data.getAttributeAsString(CF.AXIS);
                             if (axisType == null) {
                                 properties.add(data);
-                            } else if (coordinates.put(axisType.toString(), data) != null) {
+                            } else if (coordinates.put(axisType, data) != null) {
                                 continue search;    // Two axes of the same type: abort.
                             }
                         }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java
index 40e8fc6..be6f2e7 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/impl/VariableInfo.java
@@ -24,7 +24,6 @@ import java.util.HashSet;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.regex.Matcher;
-import java.lang.reflect.Array;
 import java.io.IOException;
 import javax.measure.Unit;
 import ucar.nc2.constants.CF;
@@ -47,6 +46,7 @@ import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.netcdf.AttributeNames;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Classes;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.measure.Units;
 import org.apache.sis.math.Vector;
@@ -65,11 +65,6 @@ import org.apache.sis.math.Vector;
  */
 final class VariableInfo extends Variable implements Comparable<VariableInfo> {
     /**
-     * The array to be returned by {@link #numberValues(Object)} when the given value is null.
-     */
-    private static final Number[] EMPTY = new Number[0];
-
-    /**
      * The names of attributes where to look for the description to be returned by {@link #getDescription()}.
      * We use the same attributes than the one documented in the {@link ucar.nc2.Variable#getDescription()} javadoc.
      */
@@ -127,17 +122,13 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
      * Values can be:
      *
      * <ul>
-     *   <li>a {@link String}</li>
-     *   <li>A {@link Number}</li>
-     *   <li>an array of primitive type</li>
+     *   <li>{@link String} if the attribute contains a single textual value.</li>
+     *   <li>{@link Number} if the attribute contains a single numerical value.</li>
+     *   <li>{@link Vector} if the attribute contains many numerical values.</li>
      * </ul>
      *
      * If the value is a {@code String}, then leading and trailing spaces and control characters
      * should be trimmed by {@link String#trim()}.
-     *
-     * @see #stringValues(Object)
-     * @see #numberValues(Object)
-     * @see #booleanValue(Object)
      */
     private final Map<String,Object> attributes;
 
@@ -211,8 +202,8 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
         this.dimensions = dimensions;
         this.attributes = attributes;
         final Object isUnsigned = getAttributeValue(CDM.UNSIGNED, "_unsigned");
-        if (isUnsigned != null) {
-            dataType = dataType.unsigned(booleanValue(isUnsigned));
+        if (isUnsigned instanceof String) {
+            dataType = dataType.unsigned(Boolean.valueOf((String) isUnsigned));
         }
         this.dataType = dataType;
         /*
@@ -272,15 +263,13 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
          * enumeration since those attributes may be verbose and "pollute" the variable definition.
          */
         if (!attributes.isEmpty()) {    // For avoiding UnsupportedOperationException if unmodifiable map.
-            String[] meanings = stringValues(attributes.remove(AttributeNames.FLAG_MEANINGS));
-            switch (meanings.length) {
-                case 0: meanings = null; break;
-                case 1: meanings = (String[]) CharSequences.split(meanings[0], ' '); break;
+            final Object flags = attributes.remove(AttributeNames.FLAG_MEANINGS);
+            if (flags != null) {
+                meanings = (String[]) CharSequences.split(flags.toString(), ' ');
+                return;
             }
-            this.meanings = meanings;
-        } else {
-            meanings = null;
         }
+        meanings = null;
     }
 
     /**
@@ -514,18 +503,15 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
     }
 
     /**
-     * Returns the type of the attribute of the given name,
-     * or {@code null} if the given attribute is not found.
+     * Returns the type of the attribute of the given name, or {@code null} if the given attribute is not found.
+     * If the attribute contains more than one value, then this method returns a {@code Vector.class} subtype.
+     *
+     * @param  attributeName  the name of the attribute for which to get the type, preferably in lowercase.
+     * @return type of the given attribute, or {@code null} if the attribute does not exist.
      */
     @Override
     public Class<?> getAttributeType(final String attributeName) {
-        final Object value = getAttributeValue(attributeName);
-        if (value != null) {
-            final Class<?> type = value.getClass();
-            final Class<?> c = type.getComponentType();
-            return (c != null) ? c : type;
-        }
-        return null;
+        return Classes.getClass(getAttributeValue(attributeName));
     }
 
     /**
@@ -549,108 +535,15 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> {
      * This method does not search the lower-case variant of the given name because the argument given to this method
      * is usually a hard-coded value from {@link CF} or {@link CDM} conventions, which are already in lower-cases.
      *
-     * <p>All {@code getAttributeValue(…)} methods in this class ultimately invokes this method.
-     * This provide a single point to override if the functionality needs to be extended.</p>
+     * <p>All other {@code getAttributeFoo(…)} methods in this class or parent class ultimately invoke this method.
+     * This provides a single point to override if the functionality needs to be extended.</p>
      *
      * @param  attributeName  name of attribute to search, in the expected case.
      * @return variable attribute value of the given name, or {@code null} if none.
      */
-    final Object getAttributeValue(final String attributeName) {
-        return attributes.get(attributeName);
-    }
-
-    /**
-     * Returns the sequence of values for the given attribute, or an empty array if none.
-     * The elements will be of class {@link String} if {@code numeric} is {@code false},
-     * or {@link Number} if {@code numeric} is {@code true}.
-     */
     @Override
-    public Object[] getAttributeValues(final String attributeName, final boolean numeric) {
-        final Object value = getAttributeValue(attributeName);
-        return numeric ? numberValues(value) : stringValues(value);
-    }
-
-    /**
-     * Returns the value of the given attribute as a non-blank string, or {@code null} if none.
-     */
-    @Override
-    public String getAttributeAsString(final String attributeName) {
-        final Object value = getAttributeValue(attributeName);
-        if (value instanceof String) {
-            final String text = ((String) value).trim();
-            if (!text.isEmpty()) return text;
-        }
-        return null;
-    }
-
-    /**
-     * Returns the attribute values as an array of {@link String}s, or an empty array if none.
-     * The given argument is typically a value of the {@link #attributes} map.
-     *
-     * @see #getAttributeValues(String, boolean)
-     */
-    static String[] stringValues(final Object value) {
-        if (value == null) {
-            return CharSequences.EMPTY_ARRAY;
-        }
-        if (value.getClass().isArray()) {
-            final String[] values = new String[Array.getLength(value)];
-            for (int i=0; i<values.length; i++) {
-                values[i] = Array.get(value, i).toString();
-            }
-            return values;
-        }
-        return new String[] {value.toString()};
-    }
-
-    /**
-     * Returns the attribute values as an array of {@link Number}, or an empty array if none.
-     * The given argument is typically a value of the {@link #attributes} map.
-     *
-     * @see #getAttributeValues(String, boolean)
-     */
-    @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    static Number[] numberValues(final Object value) {
-        if (value != null) {
-            if (value instanceof Number) {
-                return new Number[] {(Number) value};
-            }
-            if (value.getClass().isArray()) {
-                final Number[] values = new Number[Array.getLength(value)];
-                for (int i=0; i<values.length; i++) {
-                    final Object element = Array.get(value, i);
-                    final Number n;
-                    if (element instanceof Number) {
-                        n = (Number) element;
-                    } else if (element instanceof String) {
-                        final String t = (String) element;
-                        try {
-                            if (t.indexOf('.') >= 0) {
-                                n = Double.valueOf(t);
-                            } else {
-                                n = Long.valueOf(t);
-                            }
-                        } catch (NumberFormatException e) {
-                            // TODO: log warning. See also Decoder.parseNumber(String).
-                            continue;
-                        }
-                    } else {
-                        continue;
-                    }
-                    values[i] = n;
-                }
-                return values;
-            }
-        }
-        return EMPTY;
-    }
-
-    /**
-     * Returns the attribute value as a boolean, or {@code false} if the attribute is not a boolean.
-     * The given argument is typically a value of the {@link #attributes} map.
-     */
-    private static boolean booleanValue(final Object value) {
-        return (value instanceof String) && Boolean.valueOf((String) value);
+    protected Object getAttributeValue(final String attributeName) {
+        return attributes.get(attributeName);
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java
index a5d9fe2..69e52a5 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/DecoderWrapper.java
@@ -294,7 +294,7 @@ public final class DecoderWrapper extends Decoder implements CancelTask {
                     }
                     String asString = Utils.nonEmpty(attribute.getStringValue());
                     if (asString != null) {
-                        return parseNumber(asString);
+                        return parseNumber(name, asString);
                     }
                 }
             }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GroupWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GroupWrapper.java
index 2a7dc45..6b5664d 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GroupWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/GroupWrapper.java
@@ -69,10 +69,12 @@ final class GroupWrapper extends Node {
     }
 
     /**
-     * Returns the sequence of values for the given attribute, or an empty array if none.
+     * Returns the single value or vector of values for the given attribute, or {@code null} if none.
+     * The returned value can be an instance of {@link String}, {@link Number},
+     * {@link org.apache.sis.math.Vector} or {@code String[]}.
      */
     @Override
-    public Object[] getAttributeValues(final String attributeName, final boolean numeric) {
-        return VariableWrapper.getAttributeValues(group.findAttributeIgnoreCase(attributeName), numeric);
+    protected Object getAttributeValue(final String attributeName) {
+        return VariableWrapper.getAttributeValues(group.findAttributeIgnoreCase(attributeName));
     }
 }
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java
index 497710d..953a15e 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/ucar/VariableWrapper.java
@@ -311,6 +311,9 @@ final class VariableWrapper extends Variable {
      */
     static Class<?> getAttributeType(final Attribute attribute) {
         if (attribute != null) {
+            if (attribute.isArray()) {
+                return Vector.class;
+            }
             switch (attribute.getDataType()) {
                 case BYTE:   return Byte.class;
                 case SHORT:  return Short.class;
@@ -326,42 +329,43 @@ final class VariableWrapper extends Variable {
     }
 
     /**
-     * Returns the sequence of values for the given attribute, or an empty array if none.
-     * The elements will be of class {@link String} if {@code numeric} is {@code false},
-     * or {@link Number} if {@code numeric} is {@code true}.
+     * Returns the single value or vector of values for the given attribute, or {@code null} if none.
+     * The returned value can be an instance of {@link String}, {@link Number}, {@link Vector} or {@code String[]}.
+     *
+     * @param  attributeName  the name of the attribute for which to get the values.
+     * @return value(s) for the named attribute, or {@code null} if none.
      */
     @Override
-    public Object[] getAttributeValues(final String attributeName, final boolean numeric) {
-        return getAttributeValues(raw.findAttributeIgnoreCase(attributeName), numeric);
+    protected Object getAttributeValue(final String attributeName) {
+        return getAttributeValues(raw.findAttributeIgnoreCase(attributeName));
     }
 
     /**
-     * Implementation of {@link #getAttributeValues(String, boolean)} shared with {@link GroupWrapper}.
+     * Implementation of {@link #getAttributeValue(String)} shared with {@link GroupWrapper}.
      */
-    static Object[] getAttributeValues(final Attribute attribute, final boolean numeric) {
+    static Object getAttributeValues(final Attribute attribute) {
         if (attribute != null) {
-            boolean hasValues = false;
-            final Object[] values = new Object[attribute.getLength()];
-            for (int i=0; i<values.length; i++) {
-                if (numeric) {
-                    values[i] = Utils.fixSign(attribute.getNumericValue(i), attribute.isUnsigned());
-                    hasValues |= (values[i] != null);
-                } else {
-                    Object value = attribute.getValue(i);
-                    if (value != null) {
-                        String text = Utils.nonEmpty(value.toString());
-                        if (text != null) {
-                            values[i] = text;
-                            hasValues = true;
-                        }
+            final int length = attribute.getLength();
+            if (length != 0) {
+                boolean hasValues = false;
+                final boolean isString = attribute.isString();
+                final Object[] values = isString ? new String[length] : new Number[length];
+                for (int i=0; i<values.length; i++) {
+                    final Object value;
+                    if (isString) {
+                        value = Utils.nonEmpty(attribute.getStringValue(i));
+                    } else {
+                        value = Utils.fixSign(attribute.getNumericValue(i), attribute.isUnsigned());
                     }
+                    values[i] = value;
+                    hasValues |= (value != null);
+                }
+                if (hasValues) {
+                    return (values.length == 1) ? values[0] : Vector.create(values, attribute.isUnsigned());
                 }
-            }
-            if (hasValues) {
-                return values;
             }
         }
-        return new Object[0];
+        return null;
     }
 
     /**
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
index 9818d05..010ff70 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
@@ -71,6 +71,7 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.measure.Units;
+import org.apache.sis.math.Vector;
 
 // The following dependency is used only for static final String constants.
 // Consequently the compiled class files should not have this dependency.
@@ -892,6 +893,7 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
      * Adds information about all netCDF variables. This is the {@code <mdb:contentInfo>} element in XML.
      * This method groups variables by their domains, i.e. variables having the same set of axes are grouped together.
      */
+    @SuppressWarnings("null")
     private void addContentInfo() {
         final Map<List<String>, List<Variable>> contents = new HashMap<>(4);
         for (final Variable variable : decoder.getVariables()) {
@@ -915,17 +917,21 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
             setProcessingLevelCode(null, processingLevel);
             for (final Variable variable : group) {
                 addSampleDimension(variable);
-                final Object[] names    = variable.getAttributeValues(FLAG_NAMES,    false);
-                final Object[] meanings = variable.getAttributeValues(FLAG_MEANINGS, false);
-                final Object[] masks    = variable.getAttributeValues(FLAG_MASKS,    true);
-                final Object[] values   = variable.getAttributeValues(FLAG_VALUES,   true);
-                final int length = Math.max(masks.length, Math.max(values.length, Math.max(names.length, meanings.length)));
+                final CharSequence[] names    = variable.getAttributeAsStrings(FLAG_NAMES, ' ');
+                final CharSequence[] meanings = variable.getAttributeAsStrings(FLAG_MEANINGS, ' ');
+                final Vector         masks    = variable.getAttributeAsVector (FLAG_MASKS);
+                final Vector         values   = variable.getAttributeAsVector (FLAG_VALUES);
+                final int s1 = (names    != null) ? names.length    : 0;
+                final int s2 = (meanings != null) ? meanings.length : 0;
+                final int s3 = (masks    != null) ? masks .size()   : 0;
+                final int s4 = (values   != null) ? values.size()   : 0;
+                final int length = Math.max(s1, Math.max(s2, Math.max(s3, s4)));
                 for (int i=0; i<length; i++) {
                     addSampleValueDescription(variable,
-                            (i < names   .length) ? (String) names   [i] : null,
-                            (i < meanings.length) ? (String) meanings[i] : null,
-                            (i < masks   .length) ? (Number) masks   [i] : null,
-                            (i < values  .length) ? (Number) values  [i] : null);
+                            (i < s1) ? names     [i] : null,
+                            (i < s2) ? meanings  [i] : null,
+                            (i < s3) ? masks .get(i) : null,
+                            (i < s4) ? values.get(i) : null);
                 }
             }
         }
@@ -974,7 +980,7 @@ split:  while ((start = CharSequences.skipLeadingWhitespaces(value, start, lengt
      * @param  value     one of the elements in the {@link AttributeNames#FLAG_VALUES} attribute or {@code null}.
      */
     private void addSampleValueDescription(final Variable variable,
-            final String name, final String meaning, final Number mask, final Number value)
+            final CharSequence name, final CharSequence meaning, final Number mask, final Number value)
     {
         addSampleValueDescription(name, meaning);
         // TODO: create a record from values (and possibly from the masks).
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java
index c754a42..60639fe 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/GridTest.java
@@ -17,13 +17,15 @@
 package org.apache.sis.internal.netcdf;
 
 import java.io.IOException;
+import org.opengis.referencing.crs.ProjectedCRS;
+import org.opengis.parameter.ParameterValueGroup;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.DependsOnMethod;
 import org.opengis.test.dataset.TestData;
 import org.junit.Test;
 
-import static org.junit.Assert.*;
+import static org.opengis.test.Assert.*;
 import static org.apache.sis.test.TestUtilities.getSingleton;
 
 
@@ -125,4 +127,23 @@ public strictfp class GridTest extends TestCase {
         assertEquals( 4, z.getSize());
         assertEquals( 1, t.getSize());
     }
+
+    /**
+     * Tests {@link GridMapping#forVariable(Variable)} with a projected CRS.
+     *
+     * @throws IOException if an I/O error occurred while opening the file.
+     * @throws DataStoreException if a logical error occurred.
+     */
+    @Test
+    public void testGridMapping() throws IOException, DataStoreException {
+        final Node data = selectDataset(TestData.NETCDF_4D_PROJECTED).findNode("CIP");
+        final GridMapping mapping = GridMapping.forVariable((Variable) data);
+        assertNotNull("mapping", mapping);
+        assertInstanceOf("crs", ProjectedCRS.class, mapping.crs);
+        final ParameterValueGroup pg = ((ProjectedCRS) mapping.crs).getConversionFromBase().getParameterValues();
+        assertEquals("Latitude of false origin",           25,    pg.parameter("Latitude of false origin")         .doubleValue(), STRICT);
+        assertEquals("Longitude of false origin",         -95,    pg.parameter("Longitude of false origin")        .doubleValue(), STRICT);
+        assertEquals("Latitude of 1st standard parallel",  25,    pg.parameter("Latitude of 1st standard parallel").doubleValue(), STRICT);
+        assertEquals("Latitude of 2nd standard parallel",  25.05, pg.parameter("Latitude of 2nd standard parallel").doubleValue(), 1E-6);
+    }
 }
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java
index c49ef04..157922d 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/TestCase.java
@@ -61,7 +61,7 @@ public abstract strictfp class TestCase extends org.apache.sis.test.TestCase {
 
     /**
      * The decoders cached by {@link #selectDataset(TestData)}.
-     * The make is non-empty only during the execution of a single test class.
+     * The map is non-empty only during the execution of a single test class.
      *
      * @see #closeAllDecoders()
      */
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/VariableTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/VariableTest.java
index e200ff0..5572cc0 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/VariableTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/VariableTest.java
@@ -17,6 +17,7 @@
 package org.apache.sis.internal.netcdf;
 
 import java.util.List;
+import java.util.Arrays;
 import java.io.IOException;
 import java.time.Instant;
 import org.apache.sis.math.Vector;
@@ -227,7 +228,7 @@ public strictfp class VariableTest extends TestCase {
     }
 
     /**
-     * Tests {@link Variable#getAttributeValues(String, boolean)}.
+     * Tests {@link Variable#getAttributeValue(String)} and related methods.
      *
      * @throws IOException if an I/O error occurred while opening the file.
      * @throws DataStoreException if a logical error occurred.
@@ -237,15 +238,47 @@ public strictfp class VariableTest extends TestCase {
         final Variable[] variables = getVariablesCIP(selectDataset(TestData.NETCDF_4D_PROJECTED));
         Variable variable = variables[6];
         assertEquals("CIP", variable.getName());
-        assertArrayEquals("CIP:_FillValue", new Number[] { -1f  }, variable.getAttributeValues("_FillValue", true));
-        assertArrayEquals("CIP:_FillValue", new String[] {"-1.0"}, variable.getAttributeValues("_FillValue", false));
-        assertArrayEquals("CIP:units",      new String[] {   "%"}, variable.getAttributeValues("units",      false));
-        assertArrayEquals("CIP:units",      new Number[] {      }, variable.getAttributeValues("units",      true));
+        assertSingletonEquals(variable, "_FillValue", -1f);
+        assertSingletonEquals(variable, "units",      "%");
 
         variable = variables[0];
         assertEquals("grid_mapping_0", variable.getName());
-        assertArrayEquals("standard_parallel", new Number[] { 25.f,   25.05f}, variable.getAttributeValues("standard_parallel", true));
-        assertArrayEquals("standard_parallel", new String[] {"25.0", "25.05"}, variable.getAttributeValues("standard_parallel", false));
+        assertVectorEquals(variable, "standard_parallel", 25.f, 25.05f);
+    }
+
+    /**
+     * Asserts that the attribute of given name contains a value equals to the expected value.
+     * This method is used for attributes that are expected to contain singleton.
+     */
+    private static void assertSingletonEquals(final Node variable, final String name, final Object expected) {
+        final String t = expected.toString();
+        assertEquals     ("getAttributeValue",     expected,         variable.getAttributeValue    (name));
+        assertEquals     ("getAttributeAsString",  t,                variable.getAttributeAsString (name));
+        assertArrayEquals("getAttributeAsStrings", new String[] {t}, variable.getAttributeAsStrings(name, ' '));
+        if (expected instanceof Number) {
+            final double en = ((Number) expected).doubleValue();
+            assertEquals("getAttributeAsNumber", en, variable.getAttributeAsNumber(name), STRICT);
+            final Vector vector = variable.getAttributeAsVector(name);
+            assertNotNull("getAttributeAsVector", vector);
+            assertEquals(1, vector.size());
+            assertEquals(en, vector.get(0).doubleValue(), STRICT);
+        } else {
+            assertNull("getAttributeAsVector", variable.getAttributeAsVector(name));
+        }
+    }
+
+    /**
+     * Asserts that the attribute of given name contains a value equals to the expected value.
+     * This method is used for attributes that are expected to contain vector.
+     */
+    private static void assertVectorEquals(final Node variable, final String name, final Number... expected) {
+        final Vector values = variable.getAttributeAsVector(name);
+        assertNotNull(name, values);
+        assertEquals ("size", expected.length, values.size());
+        assertTrue   ("getAttributeAsNumber", Double.isNaN(variable.getAttributeAsNumber(name)));
+        assertEquals ("getAttributeValue", values, variable.getAttributeValue(name));
+        final Object[] texts = Arrays.stream(expected).map(Object::toString).toArray();
+        assertArrayEquals("getAttributeAsStrings", texts, variable.getAttributeAsStrings(name, ' '));
     }
 
     /**
diff --git a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/VariableInfoTest.java b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/VariableInfoTest.java
index 97f9708..4bc4e4d 100644
--- a/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/VariableInfoTest.java
+++ b/storage/sis-netcdf/src/test/java/org/apache/sis/internal/netcdf/impl/VariableInfoTest.java
@@ -22,9 +22,6 @@ import org.apache.sis.internal.netcdf.VariableTest;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.test.DependsOn;
 import org.opengis.test.dataset.TestData;
-import org.junit.Test;
-
-import static org.junit.Assert.*;
 
 
 /**
@@ -57,14 +54,4 @@ public final strictfp class VariableInfoTest extends VariableTest {
     protected Decoder createDecoder(final TestData file) throws IOException, DataStoreException {
         return ChannelDecoderTest.createChannelDecoder(file);
     }
-
-    /**
-     * Tests the {@link VariableInfo#numberValues(Object)} and {@link VariableInfo#stringValues(Object)} methods.
-     */
-    @Test
-    public void testNumberValues() {
-        final float[] a = new float[] {10, 20, 1};
-        assertArrayEquals("numberValues", new Number[] { 10f,    20f,    1f  }, VariableInfo.numberValues(a));
-        assertArrayEquals("stringValues", new String[] {"10.0", "20.0", "1.0"}, VariableInfo.stringValues(a));
-    }
 }


Mime
View raw message