sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1481649 - in /sis/branches/JDK7: core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/constraint/ ide-project/NetBeans/nbproject/ storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/
Date Sun, 12 May 2013 21:04:01 GMT
Author: desruisseaux
Date: Sun May 12 21:04:00 2013
New Revision: 1481649

URL: http://svn.apache.org/r1481649
Log:
Partial port of NetCDF metadata transcoder (not yet finished).

Added:
    sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/Decoder.java   (with props)
    sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/DecoderUCAR.java   (with props)
    sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java   (with props)
Modified:
    sis/branches/JDK7/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/constraint/DefaultLegalConstraints.java
    sis/branches/JDK7/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/constraint/DefaultSecurityConstraints.java
    sis/branches/JDK7/ide-project/NetBeans/nbproject/project.properties

Modified: sis/branches/JDK7/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/constraint/DefaultLegalConstraints.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK7/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/constraint/DefaultLegalConstraints.java?rev=1481649&r1=1481648&r2=1481649&view=diff
==============================================================================
--- sis/branches/JDK7/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/constraint/DefaultLegalConstraints.java [UTF-8] (original)
+++ sis/branches/JDK7/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/constraint/DefaultLegalConstraints.java [UTF-8] Sun May 12 21:04:00 2013
@@ -77,6 +77,16 @@ public class DefaultLegalConstraints ext
     }
 
     /**
+     * Constructs a new constraints with the given {@linkplain #getUseLimitations() use limitation}.
+     *
+     * @param useLimitation The use limitation, or {@code null} if none.
+     */
+    public DefaultLegalConstraints(final CharSequence useLimitation) {
+        super(useLimitation);
+    }
+
+
+    /**
      * Constructs a new instance initialized with the values from the specified metadata object.
      * This is a <cite>shallow</cite> copy constructor, since the other metadata contained in the
      * given object are not recursively copied.

Modified: sis/branches/JDK7/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/constraint/DefaultSecurityConstraints.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK7/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/constraint/DefaultSecurityConstraints.java?rev=1481649&r1=1481648&r2=1481649&view=diff
==============================================================================
--- sis/branches/JDK7/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/constraint/DefaultSecurityConstraints.java [UTF-8] (original)
+++ sis/branches/JDK7/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/constraint/DefaultSecurityConstraints.java [UTF-8] Sun May 12 21:04:00 2013
@@ -75,6 +75,15 @@ public class DefaultSecurityConstraints 
     }
 
     /**
+     * Constructs a new constraints with the given {@linkplain #getUseLimitations() use limitation}.
+     *
+     * @param useLimitation The use limitation, or {@code null} if none.
+     */
+    public DefaultSecurityConstraints(final CharSequence useLimitation) {
+        super(useLimitation);
+    }
+
+    /**
      * Creates a security constraints initialized with the specified classification.
      *
      * @param classification The name of the handling restrictions on the resource, or {@code null}.

Modified: sis/branches/JDK7/ide-project/NetBeans/nbproject/project.properties
URL: http://svn.apache.org/viewvc/sis/branches/JDK7/ide-project/NetBeans/nbproject/project.properties?rev=1481649&r1=1481648&r2=1481649&view=diff
==============================================================================
--- sis/branches/JDK7/ide-project/NetBeans/nbproject/project.properties [ISO-8859-1] (original)
+++ sis/branches/JDK7/ide-project/NetBeans/nbproject/project.properties [ISO-8859-1] Sun May 12 21:04:00 2013
@@ -53,6 +53,7 @@ jee.version          = 6.0
 jcip.version         = 1.0
 osgi.version         = 5.0.0
 netcdf.version       = 4.3.16
+joda-time.version    = 2.0
 junit.version        = 4.10
 
 #
@@ -84,7 +85,8 @@ javac.test.processorpath=\
     ${javac.test.classpath}
 run.classpath=\
     ${javac.classpath}:\
-    ${build.classes.dir}
+    ${build.classes.dir}:\
+    ${maven.repository}/joda-time/joda-time/${joda-time.version}/joda-time-${joda-time.version}.jar
 run.test.classpath=\
     ${javac.test.classpath}:\
     ${build.test.classes.dir}

Added: sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/Decoder.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/Decoder.java?rev=1481649&view=auto
==============================================================================
--- sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/Decoder.java (added)
+++ sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/Decoder.java [UTF-8] Sun May 12 21:04:00 2013
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.netcdf;
+
+import java.util.Date;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.io.IOException;
+import javax.measure.unit.Unit;
+import org.apache.sis.measure.Units;
+import org.apache.sis.util.Exceptions;
+import org.apache.sis.util.logging.Logging;
+
+
+/**
+ * The API used internally by Apache SIS for fetching all data from NetCDF files.
+ * We use this API for isolating Apache SIS from the library used for reading the
+ * NetCDF file: it can be either the UCAR library, or our own internal library.
+ *
+ * <p>We do not use systematically the UCAR library because it is quite large (especially when including
+ * all dependencies) while SIS uses only a fraction of it. This is because the UCAR library provides some
+ * features like referencing services which overlap with SIS services. In addition, SIS often needs "raw"
+ * data instead of "high level" data. For example we need the minimal and maximal values of a variable in
+ * its raw format, while the UCAR high level API provides the values converted by the offset and scale
+ * factors.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.3
+ * @version 0.3
+ * @module
+ */
+abstract class Decoder {
+    /**
+     * Creates a new decoder.
+     */
+    Decoder() {
+    }
+
+    /**
+     * Reports a warning. The current implementation just logs the warning. However if we want
+     * to implement a listener mechanism in a future version, this could be done here.
+     *
+     * @param method    The method in which the warning occurred.
+     * @param exception The exception to log.
+     */
+    static void warning(final String method, final Exception exception) {
+        final LogRecord record = new LogRecord(Level.WARNING, Exceptions.formatChainedMessages(null, null, exception));
+        Logging.log(Decoder.class, method, record);
+    }
+
+    /**
+     * Defines the groups where to search for named attributes, in preference order.
+     * The {@code null} group name stands for the global attributes.
+     *
+     * @param  groupNames The name of the group where to search, in preference order.
+     * @throws IOException If an I/O operation was necessary but failed.
+     */
+    public abstract void setSearchPath(final String... groupNames) throws IOException;
+
+    /**
+     * Returns the path which is currently set. The array returned by this method may be only
+     * a subset of the array given to {@link #setSearchPath(String[])} since only the name of
+     * groups which have been found in the NetCDF file are returned by this method.
+     *
+     * @throws IOException If an I/O operation was necessary but failed.
+     */
+    public abstract String[] getSearchPath() throws IOException;
+
+    /**
+     * Returns the value for the attribute of the given name, or {@code null} if none.
+     * This method searches in the groups specified by the last call to {@link #setSearchPath(String[])}.
+     * Null values and empty strings are ignored.
+     *
+     * @param  name The name of the attribute to search, or {@code null}.
+     * @return The attribute value, or {@code null} if none or empty or if the given name was null.
+     * @throws IOException If an I/O operation was necessary but failed.
+     */
+    public abstract String stringValue(final String name) throws IOException;
+
+    /**
+     * Returns the value of the attribute of the given name as a number, or {@code null} if none.
+     *
+     * @param  name The name of the attribute to search, or {@code null}.
+     * @return The attribute value, or {@code null} if none or unparseable or if the given name was null.
+     * @throws IOException If an I/O operation was necessary but failed.
+     */
+    public abstract Number numericValue(final String name) throws IOException;
+
+    /**
+     * Returns the value of the attribute of the given name as a date, or {@code null} if none.
+     *
+     * @param  name The name of the attribute to search, or {@code null}.
+     * @return The attribute value, or {@code null} if none or unparseable or if the given name was null.
+     * @throws IOException If an I/O operation was necessary but failed.
+     */
+    public abstract Date dateValue(final String name) throws IOException;
+
+    /**
+     * Returns the value of the attribute of the given name as a unit of measurement, or {@code null} if none.
+     *
+     * @param  name The name of the attribute to search, or {@code null}.
+     * @return The attribute value, or {@code null} if none or unparseable or if the given name was null.
+     * @throws IOException If an I/O operation was necessary but failed.
+     *
+     * @todo Current Units.valueOf(String) implementation ignore direction in "degrees_east" or "degrees_west".
+     *       We may need to take that in account (with "degrees_west" to "degrees_east" converter that reverse
+     *       the sign).
+     */
+    public final Unit<?> unitValue(final String name) throws IOException {
+        final String unit = stringValue(name);
+        if (unit != null) try {
+            return Units.valueOf(unit);
+        } catch (IllegalArgumentException e) {
+            warning("unitValue", e);
+        }
+        return null;
+    }
+
+    /**
+     * Returns the value of the {@code "_Id"} global attribute. The UCAR library defines a
+     * {@link ucar.nc2.NetcdfFile#getId()} method for that purpose, which we will use when
+     * possible in case that {@code getId()} method is defined in an other way.
+     *
+     * @throws IOException If an I/O operation was necessary but failed.
+     */
+    public String getId() throws IOException {
+        return stringValue("_Id");
+    }
+
+    /**
+     * Returns the value of the {@code "_Title"} global attribute. The UCAR library defines a
+     * {@link ucar.nc2.NetcdfFile#getTitle()} method for that purpose, which we will use when
+     * possible in case that {@code getTitle()} method is defined in an other way.
+     *
+     * @throws IOException If an I/O operation was necessary but failed.
+     */
+    public String getTitle() throws IOException {
+        return stringValue("_Title");
+    }
+}

Propchange: sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/Decoder.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/Decoder.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain;charset=UTF-8

Added: sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/DecoderUCAR.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/DecoderUCAR.java?rev=1481649&view=auto
==============================================================================
--- sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/DecoderUCAR.java (added)
+++ sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/DecoderUCAR.java [UTF-8] Sun May 12 21:04:00 2013
@@ -0,0 +1,227 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.netcdf;
+
+import java.util.Date;
+import ucar.nc2.Group;
+import ucar.nc2.Attribute;
+import ucar.nc2.NetcdfFile;
+import ucar.nc2.time.Calendar;
+import ucar.nc2.time.CalendarDate;
+import ucar.nc2.time.CalendarDateFormatter;
+import org.apache.sis.util.ArraysExt;
+
+
+/**
+ * Provides NetCDF decoding services based on the NetCDF library.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.3 (derived from geotk-3.20)
+ * @version 0.3
+ * @module
+ */
+final class DecoderUCAR extends Decoder {
+    /**
+     * The NetCDF file to read.
+     * This file is set at construction time.
+     *
+     * <p>This {@code DecoderUCAR} class does <strong>not</strong> close this file.
+     * Closing this file after usage is the user responsibility.</p>
+     */
+    private final NetcdfFile file;
+
+    /**
+     * The groups where to look for named attributes, in preference order. When used for constructing
+     * ISO 19115 metadata, the first group shall be {@code null} (which stands for global attributes)
+     * and all other groups shall be non-null values for the {@code "NCISOMetadata"}, {@code "THREDDSMetadata"}
+     * and {@code "CFMetadata"} groups, if they exist.
+     *
+     * @see #setSearchPath(String[])
+     */
+    private Group[] groups;
+
+    /**
+     * Creates a new decoder for the given NetCDF file. While this constructor accepts arbitrary
+     * {@link NetcdfFile} instance, the {@link NetcdfDataset} subclass is necessary in order to
+     * get coordinate system information.
+     *
+     * @param file The NetCDF file from which to parse metadata.
+     */
+    DecoderUCAR(final NetcdfFile file) {
+        this.file = file;
+    }
+
+    /**
+     * Defines the groups where to search for named attributes, in preference order.
+     * The {@code null} group name stands for the global attributes.
+     */
+    @Override
+    public void setSearchPath(final String... groupNames) {
+        final Group[] groups = new Group[groupNames.length];
+        int count = 0;
+        for (final String name : groupNames) {
+            if (name != null) {
+                final Group group = file.findGroup(name);
+                if (group == null) {
+                    continue; // Group not found - do not increment the counter.
+                }
+                groups[count] = group;
+            }
+            count++;
+        }
+        this.groups = ArraysExt.resize(groups, count);
+    }
+
+    /**
+     * Returns the path which is currently set. The array returned by this method may be only
+     * a subset of the array given to {@link #setSearchPath(String[])} since only the name of
+     * groups which have been found in the NetCDF file are returned by this method.
+     */
+    @Override
+    public String[] getSearchPath() {
+        final String[] path = new String[groups.length];
+        for (int i=0; i<path.length; i++) {
+            final Group group = groups[i];
+            if (group != null) {
+                path[i] = group.getShortName();
+            }
+        }
+        return path;
+    }
+
+    /**
+     * Returns the NetCDF attribute of the given name in the given group, or {@code null} if none.
+     * This method is invoked for every global and group attributes to be read by this class (but
+     * not {@linkplain VariableSimpleIF variable} attributes), thus providing a single point where
+     * we can filter the attributes to be read - if we want to do that in a future version.
+     *
+     * <p>The {@code name} argument is typically (but is not restricted too) one of the constants
+     * defined in the {@link AttributeNames} class.</p>
+     *
+     * @param  group The group in which to search the attribute, or {@code null} for global attributes.
+     * @param  name  The name of the attribute to search (can not be null).
+     * @return The attribute, or {@code null} if none.
+     */
+    private Attribute findAttribute(final Group group, final String name) {
+        return (group != null) ? group.findAttributeIgnoreCase(name) : file.findGlobalAttributeIgnoreCase(name);
+    }
+
+    /**
+     * Returns the value for the attribute of the given name, or {@code null} if none.
+     * This method searches in the groups specified by the last call to {@link #setSearchPath(String[])}.
+     * Null values and empty strings are ignored.
+     *
+     * @param  name The name of the attribute to search, or {@code null}.
+     * @return The attribute value, or {@code null} if none or empty or if the given name was null.
+     */
+    @Override
+    public String stringValue(final String name) {
+        if (name != null) { // For createResponsibleParty(...) convenience.
+            for (final Group group : groups) {
+                final Attribute attribute = findAttribute(group, name);
+                if (attribute != null && attribute.isString()) {
+                    String value = attribute.getStringValue();
+                    if (value != null && !(value = value.trim()).isEmpty()) {
+                        return value;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the value of the attribute of the given name as a number, or {@code null} if none.
+     *
+     * @param  name The name of the attribute to search, or {@code null}.
+     * @return The attribute value, or {@code null} if none or unparseable or if the given name was null.
+     */
+    @Override
+    public Number numericValue(final String name) {
+        if (name != null) {
+            for (final Group group : groups) {
+                final Attribute attribute = findAttribute(group, name);
+                if (attribute != null) {
+                    final Number value = attribute.getNumericValue();
+                    if (value != null) {
+                        return value;
+                    }
+                    String asString = attribute.getStringValue();
+                    if (asString != null && !(asString = asString.trim()).isEmpty()) {
+                        final int s = asString.indexOf(' ');
+                        if (s >= 0) {
+                            // Sometime, numeric values as string are followed by
+                            // a unit of measurement. We ignore that unit for now...
+                            asString = asString.substring(0, s);
+                        }
+                        try {
+                            return Double.valueOf(asString);
+                        } catch (NumberFormatException e) {
+                            warning("numericValue", e);
+                        }
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the value of the attribute of the given name as a date, or {@code null} if none.
+     *
+     * @param  name The name of the attribute to search, or {@code null}.
+     * @return The attribute value, or {@code null} if none or unparseable or if the given name was null.
+     */
+    @Override
+    public Date dateValue(final String name) {
+        if (name != null) {
+            for (final Group group : groups) {
+                final Attribute attribute = findAttribute(group, name);
+                if (attribute != null && attribute.isString()) {
+                    String value = attribute.getStringValue();
+                    if (value != null && !(value = value.trim()).isEmpty()) {
+                        final CalendarDate date;
+                        try {
+                            date = CalendarDateFormatter.isoStringToCalendarDate(Calendar.proleptic_gregorian, value);
+                        } catch (IllegalArgumentException e) {
+                            warning("dateValue", e);
+                            continue;
+                        }
+                        return new Date(date.getMillis());
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the globally unique dataset identifier as determined by the UCAR library.
+     */
+    @Override
+    public String getId() {
+        return file.getId();
+    }
+
+    /**
+     * Returns the human readable title as determined by the UCAR library.
+     */
+    @Override
+    public String getTitle() {
+        return file.getTitle();
+    }
+}

Propchange: sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/DecoderUCAR.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/DecoderUCAR.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain;charset=UTF-8

Added: sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java?rev=1481649&view=auto
==============================================================================
--- sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java (added)
+++ sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java [UTF-8] Sun May 12 21:04:00 2013
@@ -0,0 +1,681 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.netcdf;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.EnumSet;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.io.IOException;
+import javax.measure.unit.Unit;
+import javax.measure.unit.SI;
+import javax.measure.unit.NonSI;
+import javax.measure.converter.UnitConverter;
+import javax.measure.converter.ConversionException;
+
+import ucar.nc2.constants.CF;
+import ucar.nc2.units.DateUnit;
+
+import org.opengis.util.NameFactory;
+import org.opengis.util.InternationalString;
+import org.opengis.metadata.Metadata;
+import org.opengis.metadata.Identifier;
+import org.opengis.metadata.spatial.*;
+import org.opengis.metadata.content.*;
+import org.opengis.metadata.citation.*;
+import org.opengis.metadata.identification.*;
+import org.opengis.metadata.extent.Extent;
+import org.opengis.metadata.maintenance.ScopeCode;
+import org.opengis.metadata.constraint.Restriction;
+import org.opengis.referencing.crs.VerticalCRS;
+
+import org.apache.sis.util.Exceptions;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.iso.Types;
+import org.apache.sis.util.iso.DefaultNameSpace;
+import org.apache.sis.util.iso.SimpleInternationalString;
+import org.apache.sis.metadata.iso.DefaultMetadata;
+import org.apache.sis.metadata.iso.DefaultIdentifier;
+import org.apache.sis.metadata.iso.extent.*;
+import org.apache.sis.metadata.iso.spatial.*;
+import org.apache.sis.metadata.iso.content.*;
+import org.apache.sis.metadata.iso.citation.*;
+import org.apache.sis.metadata.iso.distribution.*;
+import org.apache.sis.metadata.iso.identification.*;
+import org.apache.sis.metadata.iso.lineage.DefaultLineage;
+import org.apache.sis.metadata.iso.quality.DefaultDataQuality;
+import org.apache.sis.metadata.iso.constraint.DefaultLegalConstraints;
+
+import static org.apache.sis.storage.netcdf.AttributeNames.*;
+
+
+/**
+ * Mapping from NetCDF metadata to ISO 19115-2 metadata. The {@link String} constants declared in
+ * the {@linkplain AttributeNames parent class} are the name of attributes examined by this class.
+ * The current implementation searches the attribute values in the following places, in that order:
+ *
+ * <ol>
+ *   <li>{@code "NCISOMetadata"} group</li>
+ *   <li>{@code "CFMetadata"} group</li>
+ *   <li>Global attributes</li>
+ *   <li>{@code "THREDDSMetadata"} group</li>
+ * </ol>
+ *
+ * The {@code "CFMetadata"} group has precedence over the global attributes because the
+ * {@linkplain #LONGITUDE longitude} and {@linkplain #LATITUDE latitude} resolutions are
+ * often more accurate in that group.
+ *
+ * {@section Known limitations}
+ * <ul>
+ *   <li>{@code "degrees_west"} and {@code "degrees_south"} units not correctly handled.</li>
+ *   <li>Units of measurement not yet declared in the {@link Band} elements.</li>
+ *   <li>{@link #FLAG_VALUES} and {@link #FLAG_MASKS} not yet included in the
+ *       {@link RangeElementDescription} elements.</li>
+ *   <li>Services (WMS, WCS, OPeNDAP, THREDDS) <i>etc.</i>) and transfer options not yet declared.</li>
+ * </ul>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.3 (derived from geotk-3.20)
+ * @version 0.3
+ * @module
+ */
+final class MetadataReader {
+    /**
+     * Names of groups where to search for metadata, in precedence order.
+     * The {@code null} value stands for global attributes.
+     *
+     * <p>REMINDER: if modified, update class javadoc too.</p>
+     */
+    private static final String[] SEARCH_PATH = {"NCISOMetadata", "CFMetadata", null, "THREDDSMetadata"};
+
+    /**
+     * The character to use as a keyword separator. This separator is used for parsing the
+     * {@value org.apache.sis.metadata.netcdf.AttributeNames#KEYWORDS} attribute value.
+     */
+    private static final char KEYWORD_SEPARATOR = ',';
+
+    /**
+     * The vertical coordinate reference system to be given to the object created by {@link #createExtent()}.
+     *
+     * @todo Should be set to {@link org.apache.sis.referencing.crs.DefaultVerticalCRS#GEOIDAL_HEIGHT}
+     *       after we ported the {@code sis-referencing} module.
+     */
+    private static final VerticalCRS VERTICAL_CRS = null;
+
+    /**
+     * The source of NetCDF attributes from which to infer ISO metadata.
+     * This source is set at construction time.
+     *
+     * <p>This {@code MetadataReader} class does <strong>not</strong> close this source.
+     * Closing this source after usage is the user responsibility.</p>
+     */
+    private final Decoder decoder;
+
+    /**
+     * The actual search path, as a subset of {@link #SEARCH_PATH} with only the name of the groups
+     * which have been found in the NeCDF file.
+     */
+    private final String[] searchPath;
+
+    /**
+     * The name factory, created when first needed.
+     */
+    private transient NameFactory nameFactory;
+
+    /**
+     * The contact, used at metadata creation time for avoiding to construct identical objects
+     * more than once.
+     *
+     * <p>The point of contact is stored in the two following places. The semantic of those two
+     * contacts is not strictly identical, but the distinction is not used in NetCDF file:</p>
+     *
+     * <ul>
+     *   <li>{@link DefaultMetadata#getContacts()}</li>
+     *   <li>{@link DefaultDataIdentification#getPointOfContacts()}</li>
+     * </ul>
+     *
+     * An object very similar is used as the creator. The point of contact and the creator
+     * are often identical except for their role attribute.
+     */
+    private transient ResponsibleParty pointOfContact;
+
+    /**
+     * Creates a new <cite>NetCDF to ISO</cite> mapper for the given source.
+     *
+     * @param  decoder The source of NetCDF attributes.
+     * @throws IOException If an I/O operation was necessary but failed.
+     */
+    MetadataReader(final Decoder decoder) throws IOException {
+        this.decoder = decoder;
+        decoder.setSearchPath(SEARCH_PATH);
+        searchPath = decoder.getSearchPath();
+    }
+
+    /**
+     * Reports a warning. The current implementation just logs the warning. However if we want
+     * to implement a listener mechanism in a future version, this could be done here.
+     *
+     * @param method    The method in which the warning occurred.
+     * @param exception The exception to log.
+     */
+    private static void warning(final String method, final Exception exception) {
+        final LogRecord record = new LogRecord(Level.WARNING, Exceptions.formatChainedMessages(null, null, exception));
+        Logging.log(MetadataReader.class, method, record);
+    }
+
+    /**
+     * Returns the given string as an {@code InternationalString} if non-null, or {@code null} otherwise.
+     */
+    private static InternationalString toInternationalString(final String value) {
+        return (value != null) ? new SimpleInternationalString(value) : null;
+    }
+
+    /**
+     * Adds the given element in the given collection if the element is not already present in the collection.
+     * We define this method because the metadata API uses collections while the SIS implementation uses lists.
+     * The lists are usually very short (typically 0 or 1 element), so the call to {@link List#contains(Object)}
+     * should be cheap.
+     */
+    private static <T> void addIfAbsent(final Collection<T> collection, final T element) {
+        if (!collection.contains(element)) {
+            collection.add(element);
+        }
+    }
+
+    /**
+     * Adds the given element in the given collection if the element is non-null.
+     * If the element is non-null and the collection is null, a new collection is
+     * created. The given collection, or the new collection if it has been created,
+     * is returned.
+     */
+    private static <T> Set<T> addIfNonNull(Set<T> collection, final T element) {
+        if (element != null) {
+            if (collection == null) {
+                collection = new LinkedHashSet<>(4);
+            }
+            collection.add(element);
+        }
+        return collection;
+    }
+
+    /**
+     * Returns {@code true} if the given NetCDF attribute is either null or equals to the
+     * string value of the given metadata value.
+     *
+     * @param metadata  The value stored in the metadata object.
+     * @param attribute The value parsed from the NetCDF file.
+     */
+    private static boolean isDefined(final CharSequence metadata, final String attribute) {
+        return (attribute == null) || (metadata != null && metadata.toString().equals(attribute));
+    }
+
+    /**
+     * Returns {@code true} if the given NetCDF attribute is either null or equals to one
+     * of the values in the given collection.
+     *
+     * @param metadata  The value stored in the metadata object.
+     * @param attribute The value parsed from the NetCDF file.
+     */
+    private static boolean isDefined(final Collection<String> metadata, final String attribute) {
+        return (attribute == null) || metadata.contains(attribute);
+    }
+
+    /**
+     * Returns {@code true} if the given URL is null, or if the given resource contains that URL.
+     *
+     * @param resource  The value stored in the metadata object.
+     * @param url       The value parsed from the NetCDF file.
+     */
+    private static boolean isDefined(final OnlineResource resource, final String url) {
+        return (url == null) || (resource != null && isDefined(resource.getLinkage().toString(), url));
+    }
+
+    /**
+     * Returns {@code true} if the given email is null, or if the given address contains that email.
+     *
+     * @param address  The value stored in the metadata object.
+     * @param email    The value parsed from the NetCDF file.
+     */
+    private static boolean isDefined(final Address address, final String email) {
+        return (email == null) || (address != null && isDefined(address.getElectronicMailAddresses(), email));
+    }
+
+    /**
+     * Creates an {@code OnlineResource} element if the given URL is not null. Since ISO 19115
+     * declares the URL as a mandatory attribute, this method will ignore all other attributes
+     * if the given URL is null.
+     *
+     * @param  url The URL (mandatory - if {@code null}, no resource will be created).
+     * @return The online resource, or {@code null} if the URL was null.
+     */
+    private OnlineResource createOnlineResource(final String url) {
+        if (url != null) try {
+            final DefaultOnlineResource resource = new DefaultOnlineResource(new URI(url));
+            resource.setProtocol("http");
+            resource.setApplicationProfile("web browser");
+            resource.setFunction(OnLineFunction.INFORMATION);
+            return resource;
+        } catch (URISyntaxException e) {
+            warning("createOnlineResource", e);
+        }
+        return null;
+    }
+
+    /**
+     * Creates an {@code Address} element if at least one of the given attributes is non-null.
+     */
+    private static Address createAddress(final String email) {
+        if (email != null) {
+            final DefaultAddress address = new DefaultAddress();
+            address.getElectronicMailAddresses().add(email);
+            return address;
+        }
+        return null;
+    }
+
+    /**
+     * Creates a {@code Contact} element if at least one of the given attributes is non-null.
+     */
+    private static Contact createContact(final Address address, final OnlineResource url) {
+        if (address != null || url != null) {
+            final DefaultContact contact = new DefaultContact();
+            contact.setAddress(address);
+            contact.setOnlineResource(url);
+            return contact;
+        }
+        return null;
+    }
+
+    /**
+     * Returns a globally unique identifier for the current NetCDF {@linkplain #decoder}.
+     * The default implementation builds the identifier from the following attributes:
+     *
+     * <ul>
+     *   <li>{@value #NAMING_AUTHORITY} used as the {@linkplain Identifier#getAuthority() authority}.</li>
+     *   <li>{@value #IDENTIFIER}, or {@link ucar.nc2.NetcdfFile#getId()} if no identifier attribute was found.</li>
+     * </ul>
+     *
+     * @return The globally unique identifier, or {@code null} if none.
+     * @throws IOException If an I/O operation was necessary but failed.
+     */
+    private Identifier getFileIdentifier() throws IOException {
+        String identifier = decoder.stringValue(IDENTIFIER);
+        if (identifier == null) {
+            identifier = decoder.getId();
+            if (identifier == null) {
+                return null;
+            }
+        }
+        final String namespace  = decoder.stringValue(NAMING_AUTHORITY);
+        return new DefaultIdentifier((namespace != null) ? new DefaultCitation(namespace) : null, identifier);
+    }
+
+    /**
+     * Creates a {@code ResponsibleParty} element if at least one of the name, email or URL attributes is defined.
+     * For more consistent results, the caller should restrict the {@linkplain Decoder#setSearchPath search path}
+     * to a single group before invoking this method.
+     *
+     * <p>Implementation note: this method tries to reuse the existing {@link #pointOfContact} instance,
+     * or part of it, if it is suitable.</p>
+     *
+     * @param  keys The group of attribute names to use for fetching the values.
+     * @param  isPointOfContact {@code true} for forcing the role to {@link Role#POINT_OF_CONTACT}.
+     * @return The responsible party, or {@code null} if none.
+     * @throws IOException If an I/O operation was necessary but failed.
+     *
+     * @see AttributeNames#CREATOR
+     * @see AttributeNames#CONTRIBUTOR
+     * @see AttributeNames#PUBLISHER
+     */
+    private ResponsibleParty createResponsibleParty(final Responsible keys, final boolean isPointOfContact)
+            throws IOException
+    {
+        final String individualName   = decoder.stringValue(keys.NAME);
+        final String organisationName = decoder.stringValue(keys.INSTITUTION);
+        final String email            = decoder.stringValue(keys.EMAIL);
+        final String url              = decoder.stringValue(keys.URL);
+        if (individualName == null && organisationName == null && email == null && url == null) {
+            return null;
+        }
+        Role role = Types.forCodeName(Role.class, decoder.stringValue(keys.ROLE), true);
+        if (role == null) {
+            role = isPointOfContact ? Role.POINT_OF_CONTACT : keys.DEFAULT_ROLE;
+        }
+        ResponsibleParty party    = pointOfContact;
+        Contact          contact  = null;
+        Address          address  = null;
+        OnlineResource   resource = null;
+        if (party != null) {
+            contact = party.getContactInfo();
+            if (contact != null) {
+                address  = contact.getAddress();
+                resource = contact.getOnlineResource();
+            }
+            if (!isDefined(resource, url)) {
+                resource = null;
+                contact  = null; // Clear the parents all the way up to the root.
+                party    = null;
+            }
+            if (!isDefined(address, email)) {
+                address = null;
+                contact = null; // Clear the parents all the way up to the root.
+                party   = null;
+            }
+            if (party != null) {
+                if (!isDefined(party.getOrganisationName(), organisationName) ||
+                    !isDefined(party.getIndividualName(),   individualName))
+                {
+                    party = null;
+                }
+            }
+        }
+        if (party == null) {
+            if (contact == null) {
+                if (address  == null) address  = createAddress(email);
+                if (resource == null) resource = createOnlineResource(url);
+                contact = createContact(address, resource);
+            }
+            if (individualName != null || organisationName != null || contact != null) { // Do not test role.
+                final DefaultResponsibleParty np = new DefaultResponsibleParty(role);
+                np.setIndividualName(individualName);
+                np.setOrganisationName(toInternationalString(organisationName));
+                np.setContactInfo(contact);
+                party = np;
+            }
+        }
+        return party;
+    }
+
+    /**
+     * Creates a {@code Citation} element if at least one of the required attributes is non-null.
+     * This method will reuse the {@link #pointOfContact} field, if non-null and suitable.
+     *
+     * @param  identifier The citation {@code <gmd:identifier> attribute.
+     * @throws IOException If an I/O operation was necessary but failed.
+     */
+    private Citation createCitation(final Identifier identifier) throws IOException {
+        String title = decoder.stringValue(TITLE);
+        if (title == null) {
+            title = decoder.stringValue("full_name"); // THREDDS attribute documented in TITLE javadoc.
+            if (title == null) {
+                title = decoder.stringValue("name"); // THREDDS attribute documented in TITLE javadoc.
+                if (title == null) {
+                    title = decoder.getTitle();
+                }
+            }
+        }
+        final Date   creation   = decoder.dateValue(DATE_CREATED);
+        final Date   modified   = decoder.dateValue(DATE_MODIFIED);
+        final Date   issued     = decoder.dateValue(DATE_ISSUED);
+        final String references = decoder.stringValue(REFERENCES);
+        final DefaultCitation citation = new DefaultCitation(title);
+        if (identifier != null) {
+            citation.getIdentifiers().add(identifier);
+        }
+        if (creation != null) citation.getDates().add(new DefaultCitationDate(creation, DateType.CREATION));
+        if (modified != null) citation.getDates().add(new DefaultCitationDate(modified, DateType.REVISION));
+        if (issued   != null) citation.getDates().add(new DefaultCitationDate(issued,   DateType.PUBLICATION));
+        if (pointOfContact != null) {
+            // Same responsible party than the contact, except for the role.
+            final DefaultResponsibleParty np = new DefaultResponsibleParty(Role.ORIGINATOR);
+            np.setIndividualName  (pointOfContact.getIndividualName());
+            np.setOrganisationName(pointOfContact.getOrganisationName());
+            np.setContactInfo     (pointOfContact.getContactInfo());
+            citation.getCitedResponsibleParties().add(np);
+        }
+        for (final String path : searchPath) {
+            decoder.setSearchPath(path);
+            final ResponsibleParty contributor = createResponsibleParty(CONTRIBUTOR, false);
+            if (contributor != null && contributor != pointOfContact) {
+                addIfAbsent(citation.getCitedResponsibleParties(), contributor);
+            }
+        }
+        decoder.setSearchPath(searchPath);
+        citation.setOtherCitationDetails(toInternationalString(references));
+        return citation.isEmpty() ? null : citation;
+    }
+
+    /**
+     * Creates a {@code DataIdentification} element if at least one of the required attributes is non-null.
+     * This method will reuse the {@link #pointOfContact} value, if non-null and suitable.
+     *
+     * @param  identifier The citation {@code <gmd:identifier>} attribute.
+     * @param  publisher  The publisher names, built by the caller in an opportunist way.
+     * @throws IOException If an I/O operation was necessary but failed.
+     */
+    private DataIdentification createIdentificationInfo(final Identifier identifier,
+            final Set<InternationalString> publisher) throws IOException
+    {
+        DefaultDataIdentification identification = null;
+        Set<InternationalString>  project        = null;
+        DefaultLegalConstraints   constraints    = null;
+        boolean hasExtent = false;
+        for (final String path : searchPath) {
+            decoder.setSearchPath(path);
+            final Keywords standard = createKeywords(KeywordType.THEME, true);
+            final Keywords keywords = createKeywords(KeywordType.THEME, false);
+            final String   topic    = decoder.stringValue(TOPIC_CATEGORY);
+            final String   type     = decoder.stringValue(DATA_TYPE);
+            final String   credits  = decoder.stringValue(ACKNOWLEDGMENT);
+            final String   license  = decoder.stringValue(LICENSE);
+            final String   access   = decoder.stringValue(ACCESS_CONSTRAINT);
+            final Extent   extent   = hasExtent ? null : createExtent();
+            if (standard!=null || keywords!=null || topic != null || type!=null || credits!=null || license!=null || access!= null || extent!=null) {
+                if (identification == null) {
+                    identification = new DefaultDataIdentification();
+                }
+                if (topic    != null) addIfAbsent(identification.getTopicCategories(), Types.forCodeName(TopicCategory.class, topic, true));
+                if (type     != null) addIfAbsent(identification.getSpatialRepresentationTypes(), Types.forCodeName(SpatialRepresentationType.class, type, true));
+                if (standard != null) addIfAbsent(identification.getDescriptiveKeywords(), standard);
+                if (keywords != null) addIfAbsent(identification.getDescriptiveKeywords(), keywords);
+                if (credits  != null) addIfAbsent(identification.getCredits(), credits);
+                if (license  != null) addIfAbsent(identification.getResourceConstraints(), constraints = new DefaultLegalConstraints(license));
+                if (access   != null) {
+                    for (final CharSequence token : CharSequences.split(access, ',')) {
+                        final String t = token.toString();
+                        if (!t.isEmpty()) {
+                            if (constraints == null) {
+                                identification.getResourceConstraints().add(constraints = new DefaultLegalConstraints());
+                            }
+                            addIfAbsent(constraints.getAccessConstraints(), Types.forCodeName(Restriction.class, t, true));
+                        }
+                    }
+                }
+                if (extent != null) {
+                    // Takes only ONE extent, because a NetCDF file may declare many time the same
+                    // extent with different precision. The groups are ordered in such a way that
+                    // the first extent should be the most accurate one.
+                    identification.getExtents().add(extent);
+                    hasExtent = true;
+                }
+            }
+            project = addIfNonNull(project, toInternationalString(decoder.stringValue(PROJECT)));
+        }
+        decoder.setSearchPath(searchPath);
+        final Citation citation = createCitation(identifier);
+        final String   summary  = decoder.stringValue(SUMMARY);
+        final String   purpose  = decoder.stringValue(PURPOSE);
+        if (identification == null) {
+            if (citation==null && summary==null && purpose==null && project==null && publisher==null && pointOfContact==null) {
+                return null;
+            }
+            identification = new DefaultDataIdentification();
+        }
+        identification.setCitation(citation);
+        identification.setAbstract(toInternationalString(summary));
+        identification.setPurpose (toInternationalString(purpose));
+        if (pointOfContact != null) {
+            identification.getPointOfContacts().add(pointOfContact);
+        }
+        addKeywords(identification, project,   "project"); // Not necessarily the same string than PROJECT.
+        addKeywords(identification, publisher, "dataCenter");
+        identification.setSupplementalInformation(toInternationalString(decoder.stringValue(COMMENT)));
+        return identification;
+    }
+
+    /**
+     * Adds the given keywords to the given identification info if the given set is non-null.
+     */
+    private static void addKeywords(final DefaultDataIdentification addTo,
+            final Set<InternationalString> words, final String type)
+    {
+        if (words != null) {
+            final DefaultKeywords keywords = new DefaultKeywords();
+            keywords.setKeywords(words);
+            keywords.setType(Types.forCodeName(KeywordType.class, type, true));
+            addTo.getDescriptiveKeywords().add(keywords);
+        }
+    }
+
+    /**
+     * Returns the keywords if at least one required attribute is found, or {@code null} otherwise.
+     * For more consistent results, the caller should restrict the {@linkplain Decoder#setSearchPath
+     * search path} to a single group before invoking this method.
+     *
+     * @throws IOException If an I/O operation was necessary but failed.
+     */
+    private Keywords createKeywords(final KeywordType type, final boolean standard) throws IOException {
+        final String list = decoder.stringValue(standard ? STANDARD_NAME : KEYWORDS);
+        DefaultKeywords keywords = null;
+        if (list != null) {
+            final Set<InternationalString> words = new LinkedHashSet<>();
+            for (CharSequence token : CharSequences.split(list, KEYWORD_SEPARATOR)) {
+                final String keyword = token.toString().trim();
+                if (!keyword.isEmpty()) {
+                    words.add(new SimpleInternationalString(keyword));
+                }
+            }
+            if (!words.isEmpty()) {
+                keywords = new DefaultKeywords();
+                keywords.setKeywords(words);
+                keywords.setType(type);
+                final String vocabulary = decoder.stringValue(standard ? STANDARD_NAME_VOCABULARY : VOCABULARY);
+                if (vocabulary != null) {
+                    keywords.setThesaurusName(new DefaultCitation(vocabulary));
+                }
+            }
+        }
+        return keywords;
+    }
+
+    /**
+     * Returns the extent declared in the given group, or {@code null} if none. For more consistent results,
+     * the caller should restrict the {@linkplain Decoder#setSearchPath search path} to a single group before
+     * invoking this method.
+     */
+    private Extent createExtent() throws IOException {
+        DefaultExtent extent = null;
+        final Number xmin = decoder.numericValue(LONGITUDE.MINIMUM);
+        final Number xmax = decoder.numericValue(LONGITUDE.MAXIMUM);
+        final Number ymin = decoder.numericValue(LATITUDE .MINIMUM);
+        final Number ymax = decoder.numericValue(LATITUDE .MAXIMUM);
+        final Number zmin = decoder.numericValue(VERTICAL .MINIMUM);
+        final Number zmax = decoder.numericValue(VERTICAL .MAXIMUM);
+        if (xmin != null || xmax != null || ymin != null || ymax != null) {
+            extent = new DefaultExtent();
+            final UnitConverter xConv = getConverterTo(decoder.unitValue(LONGITUDE.UNITS), NonSI.DEGREE_ANGLE);
+            final UnitConverter yConv = getConverterTo(decoder.unitValue(LATITUDE .UNITS), NonSI.DEGREE_ANGLE);
+            extent.getGeographicElements().add(new DefaultGeographicBoundingBox(
+                    valueOf(xmin, xConv), valueOf(xmax, xConv),
+                    valueOf(ymin, yConv), valueOf(ymax, yConv)));
+        }
+        if (zmin != null || zmax != null) {
+            if (extent == null) {
+                extent = new DefaultExtent();
+            }
+            final UnitConverter c = getConverterTo(decoder.unitValue(VERTICAL.UNITS), SI.METRE);
+            double min = valueOf(zmin, c);
+            double max = valueOf(zmax, c);
+            if (CF.POSITIVE_DOWN.equals(decoder.stringValue(VERTICAL.POSITIVE))) {
+                final double tmp = min;
+                min = -max;
+                max = -tmp;
+            }
+            extent.getVerticalElements().add(new DefaultVerticalExtent(min, max, VERTICAL_CRS));
+        }
+        /*
+         * Temporal extent.
+         */
+        Date startTime = decoder.dateValue(TIME.MINIMUM);
+        Date endTime   = decoder.dateValue(TIME.MAXIMUM);
+        if (startTime == null && endTime == null) {
+            final Number tmin = decoder.numericValue(TIME.MINIMUM);
+            final Number tmax = decoder.numericValue(TIME.MAXIMUM);
+            if (tmin != null || tmax != null) {
+                final String symbol = decoder.stringValue(TIME.UNITS);
+                if (symbol != null) try {
+                    final DateUnit unit = new DateUnit(symbol);
+                    if (tmin != null) startTime = unit.makeDate(tmin.doubleValue());
+                    if (tmax != null)   endTime = unit.makeDate(tmax.doubleValue());
+                } catch (Exception e) { // Declared by the DateUnit constructor.
+                    warning("createExtent", e);
+                }
+            }
+        }
+        if (startTime != null || endTime != null) {
+            if (extent == null) {
+                extent = new DefaultExtent();
+            }
+            extent.getTemporalElements().add(new DefaultTemporalExtent(/*startTime, endTime*/)); // TODO
+        }
+        final String identifier = decoder.stringValue(GEOGRAPHIC_IDENTIFIER);
+        if (identifier != null) {
+            if (extent == null) {
+                extent = new DefaultExtent();
+            }
+            extent.getGeographicElements().add(new DefaultGeographicDescription(null, identifier));
+        }
+        return extent;
+    }
+
+    /**
+     * Returns the converter from the given source unit (which may be {@code null}) to the
+     * given target unit, or {@code null} if none or incompatible.
+     */
+    private static UnitConverter getConverterTo(final Unit<?> source, final Unit<?> target) {
+        if (source != null) try {
+            return source.getConverterToAny(target);
+        } catch (ConversionException e) {
+            warning("getConverterTo", e);
+        }
+        return null;
+    }
+
+    /**
+     * Returns the values of the given number if non-null, or NaN if null. If the given
+     * converter is non-null, it is applied.
+     */
+    private static double valueOf(final Number value, final UnitConverter converter) {
+        double n = Double.NaN;
+        if (value != null) {
+            n = value.doubleValue();
+            if (converter != null) {
+                n = converter.convert(n);
+            }
+        }
+        return n;
+    }
+}

Propchange: sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sis/branches/JDK7/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/MetadataReader.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain;charset=UTF-8



Mime
View raw message