This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit fa1e45bb27119fd27294c6c132727944a4bbe916 Author: Martin Desruisseaux <[email protected]> AuthorDate: Wed Oct 28 14:44:28 2020 +0100 Share the same code for enumeration support between `RasterResource` et `FeatureSet`. --- .../org/apache/sis/internal/netcdf/CRSBuilder.java | 4 +- .../org/apache/sis/internal/netcdf/FeatureSet.java | 9 +- .../java/org/apache/sis/internal/netcdf/Node.java | 33 ++-- .../apache/sis/internal/netcdf/RasterResource.java | 30 ++-- .../org/apache/sis/internal/netcdf/Variable.java | 173 +++++++++++---------- .../sis/internal/netcdf/impl/VariableInfo.java | 22 ++- .../sis/internal/netcdf/ucar/VariableWrapper.java | 39 +---- 7 files changed, 146 insertions(+), 164 deletions(-) diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java index 1329169..782f947 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java @@ -176,7 +176,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> { public static CoordinateReferenceSystem assemble(final Decoder decoder, final Grid grid) throws DataStoreException, FactoryException, IOException { - final List<CRSBuilder<?,?>> builders = new ArrayList<>(); + final List<CRSBuilder<?,?>> builders = new ArrayList<>(4); for (final Axis axis : grid.getAxes(decoder)) { dispatch(builders, axis); } @@ -206,7 +206,7 @@ abstract class CRSBuilder<D extends Datum, CS extends CoordinateSystem> { static CoordinateReferenceSystem assemble(final Decoder decoder, final Iterable<Variable> axes, final SingleCRS[] time) throws DataStoreException, FactoryException, IOException { - final List<CRSBuilder<?,?>> builders = new ArrayList<>(); + final List<CRSBuilder<?,?>> builders = new ArrayList<>(4); for (final Variable axis : axes) { dispatch(builders, new Axis(axis)); } diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java index 7af08a2..fbc5bd4 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/FeatureSet.java @@ -212,7 +212,7 @@ final class FeatureSet extends DiscreteSampling { for (int i = getReferencingDimension(false); i < properties.length; i++) { final Variable v = properties[i]; final Class<?> type; - if (v.isEnumeration()) { + if (v.getEnumeration() != null) { type = String.class; } else { type = v.getDataType().getClass(v.getNumDimensions() > 1); @@ -251,7 +251,7 @@ final class FeatureSet extends DiscreteSampling { */ for (int i = getReferencingDimension(true); i < dynamicProperties.length; i++) { final Variable v = dynamicProperties[i]; - final Class<?> type = (v.isEnumeration() || v.isString()) ? String.class : Number.class; + final Class<?> type = (v.getEnumeration() != null || v.isString()) ? String.class : Number.class; describe(v, builder.addAttribute(type).setMaximumOccurs(Integer.MAX_VALUE)); } /* @@ -902,11 +902,12 @@ makeGeom: if (!isEmpty) { } else { value = p.read(extent, null); // Force the type to `Vector`. } - if (p.isEnumeration() && value instanceof Vector) { + final Map<Integer,String> enumeration = p.getEnumeration(); + if (enumeration != null && value instanceof Vector) { final Vector data = (Vector) value; final String[] meanings = new String[data.size()]; for (int j=0; j<meanings.length; j++) { - String m = p.meaning(data.intValue(j)); + String m = enumeration.get(data.intValue(j)); meanings[j] = (m != null) ? m : ""; } value = Arrays.asList(meanings); 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 ea30af4..3210871 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 @@ -88,6 +88,8 @@ public abstract class Node extends NamedElement { /** * Returns the value of the given attribute as a non-blank string with leading/trailing spaces removed. + * If the attribute value is an array, this method returns a non-null value only if the array contains + * a single value (possibly duplicated), ignoring null or empty values. * * @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. @@ -103,10 +105,12 @@ public abstract class Node extends NamedElement { } String singleton = null; for (final String c : values) { - if (singleton == null) { - singleton = c; - } else if (!singleton.equals(c)) { - return null; + if (c != null) { + if (singleton == null) { + singleton = c; + } else if (!singleton.equals(c)) { + return null; + } } } return singleton; @@ -116,10 +120,14 @@ public abstract class Node extends NamedElement { * 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. + * Empty or blank strings are replaced by {@code null} values. + * + * <p>This method may return a direct reference to an internal array; + * <strong>do not modify array content</strong>.</p> * * @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. + * @return the attribute values, or {@code null} if none. The array may contain {@code null} elements. */ public final CharSequence[] getAttributeAsStrings(final String attributeName, final char separator) { final Object value = getAttributeValue(attributeName); @@ -138,10 +146,17 @@ public abstract class Node extends NamedElement { /** * 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. + * The returned array contains no blank strings, but may contain null values. + * + * <p>This method may return a direct reference to an internal array; + * <strong>do not modify array content</strong>.</p> */ private static String[] toArray(final Object value) { final String[] array; - if (value instanceof Object[]) { + if (value instanceof String[]) { + // Empty strings are already replaced by null values. + return (String[]) value; + } else if (value instanceof Object[]) { final Object[] values = (Object[]) value; array = new String[values.length]; for (int i=0; i<array.length; i++) { @@ -161,11 +176,7 @@ public abstract class Node extends NamedElement { } boolean hasValues = false; for (int i=0; i<array.length; i++) { - String e = Strings.trimOrNull(array[i]); - if (e != null) { - hasValues = true; - array[i] = e; - } + hasValues |= (array[i] = Strings.trimOrNull(array[i])) != null; } return hasValues ? array : 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 e02059c..43f524d 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 @@ -43,7 +43,6 @@ import org.apache.sis.coverage.grid.GridDerivation; import org.apache.sis.coverage.grid.GridRoundingMode; import org.apache.sis.coverage.grid.DisjointExtentException; import org.apache.sis.coverage.IllegalSampleDimensionException; -import org.apache.sis.storage.netcdf.AttributeNames; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.DataStoreContentException; import org.apache.sis.storage.DataStoreReferencingException; @@ -52,7 +51,6 @@ 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; @@ -466,7 +464,7 @@ public final class RasterResource extends AbstractGridResource implements Resour * by UCAR because we need the range of packed values instead than the range of converted values. */ NumberRange<?> range; - if (!createEnumeration(builder, band, index) && (range = band.getValidRange()) != null) try { + if (!createEnumeration(builder, band) && (range = band.getValidRange()) != null) try { final MathTransform1D mt = band.getTransferFunction().getTransform(); if (!mt.isIdentity() && range instanceof MeasurementRange<?>) { /* @@ -583,27 +581,17 @@ public final class RasterResource extends AbstractGridResource implements Resour * * @param builder the builder to use for creating the sample dimension. * @param band the data for which to create a sample dimension. - * @param index index in the variable dimension identified by {@link #bandDimension}. * @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) { - CharSequence[] names = band.getAttributeAsStrings(AttributeNames.FLAG_NAMES, ' '); - if (names == null) { - names = band.getAttributeAsStrings(AttributeNames.FLAG_MEANINGS, ' '); - if (names == null) return false; - } - Vector values = band.getAttributeAsVector(AttributeNames.FLAG_VALUES); - if (values == null) { - values = band.getAttributeAsVector(AttributeNames.FLAG_MASKS); - if (values == null) return false; + private static boolean createEnumeration(final SampleDimension.Builder builder, final Variable band) { + final Map<Integer,String> enumeration = band.getEnumeration(); + if (enumeration == null) { + return false; } - 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 = names[i]; - } else { + for (final Map.Entry<Integer,String> entry : enumeration.entrySet()) { + final Number value = entry.getKey(); + CharSequence name = entry.getValue(); + if (name == null) { name = Vocabulary.formatInternational(Vocabulary.Keys.Unnamed); } builder.addQualitative(name, value, value); diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java index 1ffddad..133bf00 100644 --- a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java +++ b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/Variable.java @@ -38,7 +38,6 @@ import org.apache.sis.math.MathFunctions; import org.apache.sis.measure.NumberRange; import org.apache.sis.util.Numbers; import org.apache.sis.util.ArraysExt; -import org.apache.sis.util.CharSequences; import org.apache.sis.util.collection.Containers; import org.apache.sis.util.collection.WeakHashSet; import org.apache.sis.internal.util.Numerics; @@ -125,11 +124,10 @@ public abstract class Variable extends Node { * The {@code flag_meanings} values (used for enumeration values), * or {@code null} if this variable is not an enumeration. * - * @see #isEnumeration() - * @see #meaning(int) - * @see #setFlagMeanings(Object, Object) + * @see #setEnumeration(Map) + * @see #getEnumeration() */ - private Map<Integer,String> meanings; + private Map<Integer,String> enumeration; /** * The grid associated to this variable, or {@code null} if none or not yet computed. @@ -192,73 +190,85 @@ public abstract class Variable extends Node { } /** - * If {@code flags} is non-null, declares this variable as an enumeration. - * This method stores the information needed for {@link #meaning(int)} default implementation. - * This method is invoked by subclass constructors for completing {@code Variable} creation. + * Initializes the map of enumeration values. If the given map is non-null, then the enumerations are set + * to the specified map (by direct reference; the map is not cloned). Otherwise this method auto-detects + * if this variable is an enumeration. * - * @param flags the flag meanings as a space-separated string, or {@code null} if none. - * @param values the flag values as a vector of integer values, or {@code null} if none. + * <p>This method is invoked by subclass constructors for completing {@code Variable} creation. + * It should not be invoked after creation, for keeping {@link Variable} immutable.</p> * - * @see #isEnumeration() - * @see #meaning(int) + * @param enumeration the enumeration map, or {@code null} for auto-detection. + * + * @see #getEnumeration() */ - @SuppressWarnings("null") - protected final void setFlagMeanings(final Object flags, final Object values) { - if (flags == null) { - return; - } - final String[] labels = (String[]) CharSequences.split(flags.toString(), ' '); - int count = labels.length; - meanings = new HashMap<>(Containers.hashMapCapacity(count)); - final Vector numbers; - if (values instanceof Vector) { - numbers = (Vector) values; - final int n = numbers.size(); - if (n != count) { - warning(Variable.class, "setFlagMeanings", Resources.Keys.MismatchedAttributeLength_5, - getName(), AttributeNames.FLAG_VALUES, AttributeNames.FLAG_MEANINGS, n, count); - if (n < count) count = n; + @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter") + protected final void setEnumeration(Map<Integer,String> enumeration) { + if (enumeration == null) { + String srcLabels, srcNumbers; // For more accurate message in case of warning. + CharSequence[] labels = getAttributeAsStrings(srcLabels = AttributeNames.FLAG_NAMES, ' '); + if (labels == null) { + labels = getAttributeAsStrings(srcLabels = AttributeNames.FLAG_MEANINGS, ' '); + if (labels == null) return; } - } else { - numbers = Vector.createSequence(0, 1, count); - warning(Variable.class, "setFlagMeanings", Resources.Keys.MissingVariableAttribute_3, - getFilename(), getName(), AttributeNames.FLAG_VALUES); - } - /* - * Copy (numbers, labels) entries in an HashMap with keys converted to 32-bits signed integer. - * If a key can not be converted, we will log a warning after all errors have been collected - * in order to produce only one log message. We put a limit on the amount of reported errors - * for avoiding to flood the logger. - */ - Exception error = null; - StringBuilder invalids = null; - for (int i=0; i<count; i++) try { - meanings.merge(numbers.intValue(i), labels[i], (o,n) -> o + " | " + n); - } catch (NumberFormatException | ArithmeticException e) { - if (error == null) { - error = e; - invalids = new StringBuilder(); + Vector numbers = getAttributeAsVector(srcNumbers = AttributeNames.FLAG_VALUES); + if (numbers == null) { + numbers = getAttributeAsVector(srcNumbers = AttributeNames.FLAG_MASKS); + } + int count = labels.length; + if (numbers != null) { + final int n = numbers.size(); + if (n != count) { + warning(Variable.class, "setEnumeration", Resources.Keys.MismatchedAttributeLength_5, + getName(), srcNumbers, srcLabels, n, count); + if (n < count) count = n; + } } else { - final int length = invalids.length(); - final boolean tooManyErrors = (length > 100); // Arbitrary limit. - if (tooManyErrors && invalids.charAt(length - 1) == '…') { - continue; + numbers = Vector.createSequence(0, 1, count); + warning(Variable.class, "setEnumeration", Resources.Keys.MissingVariableAttribute_3, + getFilename(), getName(), AttributeNames.FLAG_VALUES); + } + /* + * Copy (numbers, labels) entries in an HashMap with keys converted to 32-bits signed integer. + * If a key can not be converted, we will log a warning after all errors have been collected + * in order to produce only one log message. We put a limit on the amount of reported errors + * for avoiding to flood the logger. + */ + Exception error = null; + StringBuilder invalids = null; + enumeration = new HashMap<>(Containers.hashMapCapacity(count)); + for (int i=0; i<count; i++) try { + final CharSequence label = labels[i]; + if (label != null) { + enumeration.merge(numbers.intValue(i), label.toString(), (o,n) -> { + return o.equals(n) ? o : o + " | " + n; + }); } - error.addSuppressed(e); - invalids.append(", "); - if (tooManyErrors) { - invalids.append('…'); - continue; + } catch (NumberFormatException | ArithmeticException e) { + if (error == null) { + error = e; + invalids = new StringBuilder(); + } else { + final int length = invalids.length(); + final boolean tooManyErrors = (length > 100); // Arbitrary limit. + if (tooManyErrors && invalids.charAt(length - 1) == '…') { + continue; + } + error.addSuppressed(e); + invalids.append(", "); + if (tooManyErrors) { + invalids.append('…'); + continue; + } } + invalids.append(numbers.stringValue(i)); + } + if (invalids != null) { + error(Variable.class, "setEnumeration", error, + Errors.Keys.CanNotConvertValue_2, invalids, numbers.getElementType()); } - invalids.append(numbers.stringValue(i)); - } - if (invalids != null) { - error(Variable.class, "setFlagMeanings", error, - Errors.Keys.CanNotConvertValue_2, invalids, numbers.getElementType()); } - if (meanings.isEmpty()) { - meanings = null; + if (!enumeration.isEmpty()) { + this.enumeration = enumeration; } } @@ -458,17 +468,6 @@ public abstract class Variable extends Node { } /** - * Returns {@code true} if this variable is an enumeration. - * - * @return whether this variable is an enumeration. - * - * @see #meaning(int) - */ - protected boolean isEnumeration() { - return meanings != null; - } - - /** * Returns whether this variable can grow. A variable is unlimited if at least one of its dimension is unlimited. * In netCDF 3 classic format, only the first dimension can be unlimited. * @@ -871,6 +870,20 @@ public abstract class Variable extends Node { } /** + * Returns enumeration values (keys) and their meanings (values), or {@code null} if this + * variable is not an enumeration. This method returns a direct reference to internal map + * (no clone, no unmodifiable wrapper); <strong>Do not modify the returned map.</strong> + * + * @return the ordinals and values associated to ordinals, or {@code null} if none. + * + * @see #setEnumeration(Map) + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") + final Map<Integer,String> getEnumeration() { + return enumeration; + } + + /** * Returns all no-data values declared for this variable, or an empty map if none. * The map keys are the no-data values (pad sample values or missing sample values). * The map values can be either {@link String} or {@link org.opengis.util.InternationalString} values @@ -1243,18 +1256,6 @@ public abstract class Variable extends Node { } /** - * Returns the meaning of the given ordinal value, or {@code null} if none. - * Callers must have verified that {@link #isEnumeration()} returned {@code true} - * before to invoke this method - * - * @param ordinal the ordinal of the enumeration for which to get the value. - * @return the value associated to the given ordinal, or {@code null} if none. - */ - protected String meaning(final int ordinal) { - return meanings.get(ordinal); - } - - /** * Returns a string representation of this variable for debugging purpose. * * @return a string representation of this variable. 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 1746fa7..f067fc3 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 @@ -131,6 +131,8 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> { * <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 is one of predefined attributes + * for which many text values are expected (e.g. an enumeration).</li> * </ul> * * If the value is a {@code String}, then leading and trailing spaces and control characters @@ -247,12 +249,22 @@ final class VariableInfo extends Variable implements Comparable<VariableInfo> { isCoordinateSystemAxis = dimensions[0].name.equals(value); } /* - * Verify if this variable is an enumeration. If yes, we remove the attributes that define the - * enumeration since those attributes may be verbose and "pollute" the variable definition. + * Rewrite the enumeration names as an array for avoiding to parse the string if this information + * is asked twice (e.g. in `setEnumeration(…)` and in `MetadataReader`). Note that there is no need + * to perform similar operation for vectors of numbers. */ - if (!attributes.isEmpty()) { // For avoiding UnsupportedOperationException if unmodifiable map. - setFlagMeanings(attributes.remove(AttributeNames.FLAG_MEANINGS), - attributes.remove(AttributeNames.FLAG_VALUES)); + split(AttributeNames.FLAG_NAMES); + split(AttributeNames.FLAG_MEANINGS); + setEnumeration(null); + } + + /** + * Splits a space-separated attribute value into an array of strings. + */ + private void split(final String attribute) { + final CharSequence[] values = getAttributeAsStrings(attribute, ' '); + if (values != null) { + attributes.put(attribute, values); } } 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 82b6912..b277617 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 @@ -16,6 +16,7 @@ */ package org.apache.sis.internal.netcdf.ucar; +import java.util.Map; import java.util.List; import java.util.Collection; import java.io.File; @@ -49,7 +50,6 @@ import org.apache.sis.internal.util.UnmodifiableArrayList; import org.apache.sis.internal.util.Strings; import org.apache.sis.internal.jdk9.JDK9; import org.apache.sis.storage.DataStoreException; -import org.apache.sis.storage.netcdf.AttributeNames; import org.apache.sis.measure.MeasurementRange; import org.apache.sis.measure.NumberRange; import org.apache.sis.measure.Units; @@ -81,11 +81,6 @@ final class VariableWrapper extends Variable { private final VariableIF raw; /** - * {@code true} if this variable is an enumeration. - */ - private final boolean isEnumeration; - - /** * Creates a new variable wrapping the given netCDF interface. */ VariableWrapper(final Decoder decoder, VariableIF v) { @@ -102,13 +97,11 @@ final class VariableWrapper extends Variable { * If the UCAR library recognizes this variable as an enumeration, we will use UCAR services. * Only if UCAR did not recognized the enumeration, fallback on Apache SIS implementation. */ + Map<Integer,String> enumeration = null; if (variable.getDataType().isEnum() && (variable instanceof ucar.nc2.Variable)) { - isEnumeration = true; - } else { - setFlagMeanings(getAttributeAsString(AttributeNames.FLAG_MEANINGS), - getAttributeAsVector(AttributeNames.FLAG_VALUES)); - isEnumeration = super.isEnumeration(); + enumeration = ((ucar.nc2.Variable) variable).getEnumTypedef().getMap(); } + setEnumeration(enumeration); // Use SIS fallback if `enumeration` is null. } /** @@ -226,16 +219,6 @@ final class VariableWrapper extends Variable { } /** - * Returns {@code true} if this variable is an enumeration. - * - * @see #meaning(int) - */ - @Override - protected boolean isEnumeration() { - return isEnumeration; - } - - /** * Returns whether this variable can grow. A variable is unlimited if at least one of its dimension is unlimited. */ @Override @@ -637,20 +620,6 @@ final class VariableWrapper extends Variable { } /** - * Returns the meaning of the given ordinal value, or {@code null} if none. - * Callers must have verified that {@link #isEnumeration()} returned {@code true} - * before to invoke this method - * - * @param ordinal the ordinal of the enumeration for which to get the value. - * @return the value associated to the given ordinal, or {@code null} if none. - */ - @Override - protected String meaning(final int ordinal) { - return super.isEnumeration() ? super.meaning(ordinal) - : ((ucar.nc2.Variable) variable).lookupEnumString(ordinal); - } - - /** * Returns {@code true} if this Apache SIS variable is a wrapper for the given UCAR variable. */ final boolean isWrapperFor(final VariableIF v) {
