sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1759701 - in /sis/branches/JDK8: core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/distribution/ core/sis-utility/src/main/java/org/apache/sis/internal/util/ storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ stor...
Date Wed, 07 Sep 2016 20:50:07 GMT
Author: desruisseaux
Date: Wed Sep  7 20:50:07 2016
New Revision: 1759701

URL: http://svn.apache.org/viewvc?rev=1759701&view=rev
Log:
Continue work on CSV data store: complete metadata parsing and move the parsing of Features in a Stream.

Modified:
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/distribution/DefaultFormat.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/util/StandardDateFormat.java
    sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
    sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
    sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/TimeEncoding.java
    sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/package-info.java
    sis/branches/JDK8/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/csv/StoreTest.java

Modified: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/distribution/DefaultFormat.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/distribution/DefaultFormat.java?rev=1759701&r1=1759700&r2=1759701&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/distribution/DefaultFormat.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/distribution/DefaultFormat.java [UTF-8] Wed Sep  7 20:50:07 2016
@@ -113,6 +113,7 @@ public class DefaultFormat extends ISOMe
      * <table class="sis">
      *   <caption>Specification titles for well-known format names</caption>
      *   <tr><th>Name</th>    <th>Specification title</th></tr>
+     *   <tr><td>CSV</td>     <td>Common Format and MIME Type for Comma-Separated Values (CSV) Files</td></tr>
      *   <tr><td>GeoTIFF</td> <td>GeoTIFF Coverage Encoding Profile</td></tr>
      *   <tr><td>NetCDF</td>  <td>NetCDF Classic and 64-bit Offset Format</td></tr>
      *   <tr><td>PNG</td>     <td>PNG (Portable Network Graphics) Specification</td></tr>
@@ -137,6 +138,8 @@ public class DefaultFormat extends ISOMe
                     title = "NetCDF Classic and 64-bit Offset Format";
                 } else if (keyword.equalsIgnoreCase("PNG")) {
                     title = "PNG (Portable Network Graphics) Specification";
+                } else if (keyword.equalsIgnoreCase("CSV")) {
+                    title = "Common Format and MIME Type for Comma-Separated Values (CSV) Files";
                 }
                 citation.setTitle(Types.toInternationalString(title));
                 citation.setAlternateTitles(Collections.singleton(Types.toInternationalString(name)));

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/util/StandardDateFormat.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/util/StandardDateFormat.java?rev=1759701&r1=1759700&r2=1759701&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/util/StandardDateFormat.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/util/StandardDateFormat.java [UTF-8] Wed Sep  7 20:50:07 2016
@@ -30,6 +30,7 @@ import java.text.ParseException;
 import java.time.DateTimeException;
 import java.time.Instant;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.OffsetDateTime;
 import java.time.ZoneId;
 import java.time.ZoneOffset;
@@ -111,14 +112,14 @@ public final class StandardDateFormat ex
      *       the date is considered approximative). Note that this is consistent with ISO 19162 requirement that
      *       dates are always in UTC, even if Apache SIS allows some flexibility.</li>
      *   <li>Otherwise if the timezone is not {@code null} and not UTC, then this method returns an {@link OffsetDateTime}.</li>
-     *   <li>Otherwise this method returns an {@link Instant}.</li>
+     *   <li>Otherwise this method returns a {@link LocalDateTime} in the given timezone.</li>
      * </ul>
      *
      * @param  date the date to convert, or {@code null}.
      * @param  zone the timezone of the temporal object to obtain, or {@code null} for UTC.
      * @return the temporal object for the given date, or {@code null} if the given argument was null.
      */
-    public static Temporal toHeuristicTemporal(final Date date, final ZoneId zone) {
+    public static Temporal toHeuristicTemporal(final Date date, ZoneId zone) {
         if (date == null) {
             return null;
         }
@@ -127,10 +128,12 @@ public final class StandardDateFormat ex
             return LocalDate.ofEpochDay(time / MILLISECONDS_PER_DAY);
         }
         final Instant instant = Instant.ofEpochMilli(time);
-        if (zone != null && !zone.equals(ZoneOffset.UTC)) {
+        if (zone == null) {
+            zone = ZoneOffset.UTC;
+        } else if (!zone.equals(ZoneOffset.UTC)) {
             return OffsetDateTime.ofInstant(instant, zone);
         }
-        return instant;
+        return LocalDateTime.ofInstant(instant, zone);
     }
 
     /**

Modified: sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java?rev=1759701&r1=1759700&r2=1759701&view=diff
==============================================================================
--- sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java [UTF-8] (original)
+++ sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/MetadataBuilder.java [UTF-8] Wed Sep  7 20:50:07 2016
@@ -50,7 +50,9 @@ import org.apache.sis.metadata.iso.spati
 import org.apache.sis.metadata.iso.content.DefaultAttributeGroup;
 import org.apache.sis.metadata.iso.content.DefaultSampleDimension;
 import org.apache.sis.metadata.iso.content.DefaultCoverageDescription;
+import org.apache.sis.metadata.iso.content.DefaultFeatureCatalogueDescription;
 import org.apache.sis.metadata.iso.content.DefaultImageDescription;
+import org.apache.sis.metadata.iso.content.DefaultFeatureTypeInfo;
 import org.apache.sis.metadata.iso.citation.AbstractParty;
 import org.apache.sis.metadata.iso.citation.DefaultCitation;
 import org.apache.sis.metadata.iso.citation.DefaultCitationDate;
@@ -77,6 +79,7 @@ import static org.apache.sis.internal.ut
 
 // Branch-dependent imports
 import java.time.LocalDate;
+import org.opengis.feature.FeatureType;
 
 
 /**
@@ -167,6 +170,11 @@ public class MetadataBuilder {
     private DefaultCoverageDescription coverageDescription;
 
     /**
+     * Information about the feature types, or {@code null} if none.
+     */
+    private DefaultFeatureCatalogueDescription featureDescription;
+
+    /**
      * Information about content type for groups of attributes for a specific range dimension, or {@code null} if none.
      */
     private DefaultAttributeGroup attributGroup;
@@ -337,6 +345,20 @@ public class MetadataBuilder {
     }
 
     /**
+     * Commits all pending information under the metadata "feature catalog" node.
+     * If there is no pending feature description, then invoking this method has no effect.
+     * If new feature descriptions are added after this method call, they will be stored in a new element.
+     *
+     * <p>This method does not need to be invoked unless a new "feature catalog description" node is desired.</p>
+     */
+    public final void newFeatureTypes() {
+        if (featureDescription != null) {
+            metadata().getContentInfo().add(featureDescription);
+            featureDescription = null;
+        }
+    }
+
+    /**
      * Commits all pending information under the metadata "distribution info" node (format, <i>etc</i>).
      * If there is no pending distribution information, then invoking this method has no effect.
      * If new distribution information are added after this method call, they will be stored in a new element.
@@ -521,6 +543,18 @@ public class MetadataBuilder {
     }
 
     /**
+     * Creates the feature descriptions object if it does not already exists, then returns it.
+     *
+     * @return the feature descriptions (never {@code null}).
+     */
+    private DefaultFeatureCatalogueDescription featureDescription() {
+        if (featureDescription == null) {
+            featureDescription = new DefaultFeatureCatalogueDescription();
+        }
+        return featureDescription;
+    }
+
+    /**
      * Creates the distribution format object if it does not already exists, then returns it.
      *
      * @return the distribution format (never {@code null}).
@@ -592,6 +626,22 @@ public class MetadataBuilder {
     }
 
     /**
+     * Adds descriptions for the given feature.
+     *
+     * @param  type         the feature type to add, or {@code null}.
+     * @param  occurrences  number of instances of the given features, or {@code null} if unknown.
+     */
+    public final void add(final FeatureType type, final Integer occurrences) {
+        if (type != null) {
+            final DefaultFeatureTypeInfo info = new DefaultFeatureTypeInfo(type.getName());
+            if (occurrences != null) {
+                info.setFeatureInstanceCount(shared(occurrences));
+            }
+            featureDescription().getFeatureTypeInfo().add(info);
+        }
+    }
+
+    /**
      * Adds the given coordinate reference system to metadata, if it does not already exists.
      * This method ensures that there is no duplicated values. Comparisons ignore metadata.
      *
@@ -1245,6 +1295,7 @@ parse:      for (int i = 0; i < length;)
     public final DefaultMetadata build(final boolean freeze) {
         newIdentification();
         newGridRepresentation();
+        newFeatureTypes();
         newCoverage(false);
         newDistribution();
         newAcquisition();

Modified: sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java?rev=1759701&r1=1759700&r2=1759701&view=diff
==============================================================================
--- sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java [UTF-8] (original)
+++ sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/Store.java [UTF-8] Wed Sep  7 20:50:07 2016
@@ -20,7 +20,6 @@ import java.util.List;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Iterator;
 import java.util.Date;
 import java.util.Locale;
 import java.util.logging.Level;
@@ -37,6 +36,7 @@ import javax.measure.unit.NonSI;
 import javax.measure.quantity.Duration;
 import org.opengis.metadata.Metadata;
 import org.opengis.util.FactoryException;
+import org.opengis.metadata.maintenance.ScopeCode;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.TemporalCRS;
 import org.opengis.referencing.operation.TransformException;
@@ -58,10 +58,15 @@ import org.apache.sis.util.ObjectConvert
 import org.apache.sis.util.ObjectConverters;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.IndexedResourceBundle;
+import org.apache.sis.util.collection.BackingStoreException;
 
 // Branch-dependent imports
 import java.time.Instant;
 import java.time.DateTimeException;
+import java.util.function.Consumer;
+import java.util.Spliterator;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.PropertyType;
@@ -112,9 +117,9 @@ public final class Store extends DataSto
     private static final String TYPE_PREFIX = "xsd:";
 
     /**
-     * The file name.
+     * The file name, used for reporting error messages.
      */
-    private final String name;
+    private final String filename;
 
     /**
      * The reader, set by the constructor and cleared when no longer needed.
@@ -123,6 +128,7 @@ public final class Store extends DataSto
 
     /**
      * The character encoding, or {@code null} if unspecified (in which case the platform default is assumed).
+     * Note that the default value is different than the moving feature specification, which requires UTF-8.
      */
     private final Charset encoding;
 
@@ -148,19 +154,12 @@ public final class Store extends DataSto
     private final FeatureType featureType;
 
     /**
-     * The features created while parsing the CSV file.
-     *
-     * @todo We should not keep them in memory, but instead use some kind of iterator or stream.
-     */
-    private final List<Feature> features;
-
-    /**
      * {@code true} if {@link #featureType} contains a trajectory column.
      */
     private boolean hasTrajectories;
 
     /**
-     * Appearing order of trajectories, or {@code null} if unspecified.
+     * Appearing order of trajectories (time or sequential), or {@code null} if unspecified.
      *
      * @see #parseFoliation(List)
      */
@@ -177,15 +176,15 @@ public final class Store extends DataSto
      * <p>If the CSV file is known to be a Moving Feature file, then the given connector should
      * have an {@link org.apache.sis.setup.OptionKey#ENCODING} associated to the UTF-8 value.</p>
      *
-     * @param  connector Information about the storage (URL, stream, <i>etc</i>).
+     * @param  connector  information about the storage (URL, stream, <i>etc</i>).
      * @throws DataStoreException if an error occurred while opening the stream.
      */
     public Store(final StorageConnector connector) throws DataStoreException {
-        name = connector.getStorageName();
+        filename = connector.getStorageName();
         final Reader r = connector.getStorageAs(Reader.class);
         connector.closeAllExcept(r);
         if (r == null) {
-            throw new DataStoreException(Errors.format(Errors.Keys.CanNotOpen_1, name));
+            throw new DataStoreException(Errors.format(Errors.Keys.CanNotOpen_1, filename));
         }
         source = (r instanceof BufferedReader) ? (BufferedReader) r : new LineNumberReader(r);
         GeneralEnvelope envelope    = null;
@@ -216,9 +215,6 @@ public final class Store extends DataSto
                             throw new DataStoreContentException(duplicated("@columns"));
                         }
                         featureType = parseFeatureType(elements);
-                        if (foliation == null) {
-                            foliation = Foliation.TIME;
-                        }
                         break;
                     }
                     case "@foliation": {
@@ -241,13 +237,12 @@ public final class Store extends DataSto
             }
             source.reset();
         } catch (IOException | FactoryException | IllegalArgumentException | DateTimeException e) {
-            throw new DataStoreException(errors().getString(Errors.Keys.CanNotParseFile_2, "CSV", name), e);
+            throw new DataStoreException(errors().getString(Errors.Keys.CanNotParseFile_2, "CSV", filename), e);
         }
         this.encoding    = connector.getOption(OptionKey.ENCODING);
         this.envelope    = envelope;
         this.featureType = featureType;
         this.foliation   = foliation;
-        this.features    = new ArrayList<>();
     }
 
     /**
@@ -259,49 +254,57 @@ public final class Store extends DataSto
      *   &#64;stboundedby, urn:ogc:def:crs:CRS:1.3:84, 2D, 50.23 9.23, 50.31 9.27, 2012-01-17T12:33:41Z, 2012-01-17T12:37:00Z, sec
      * }
      *
-     * @param  elements The line elements. The first elements should be {@code "@stboundedby"}.
-     * @return The envelope, or {@code null} if the given list does not contain enough elements.
+     * @param  elements  the line elements. The first elements should be {@code "@stboundedby"}.
+     * @return the envelope, or {@code null} if the given list does not contain enough elements.
      */
     @SuppressWarnings("fallthrough")
     private GeneralEnvelope parseEnvelope(final List<String> elements) throws DataStoreException, FactoryException {
-        double[]       lowerCorner    = null;
-        double[]       upperCorner    = null;
+        CoordinateReferenceSystem crs = null;
+        int spatialDimensionCount     = 2;
+        double[]       lowerCorner    = ArraysExt.EMPTY_DOUBLE;
+        double[]       upperCorner    = ArraysExt.EMPTY_DOUBLE;
         Instant        startTime      = null;
         Instant        endTime        = null;
         Unit<Duration> timeUnit       = SI.SECOND;
         boolean        isTimeAbsolute = false;
-        boolean        is3D           = false;
-        CoordinateReferenceSystem crs = null;
-        GeneralEnvelope envelope      = null;
-        switch (elements.size()) {
-            default:final String unit = elements.get(7);
-                    switch (unit.toLowerCase(Locale.US)) {
-                        case "":
-                        case "sec":
-                        case "second":   /* Already SI.SECOND. */ break;
-                        case "minute":   timeUnit = NonSI.MINUTE; break;
-                        case "hour":     timeUnit = NonSI.HOUR;   break;
-                        case "day":      timeUnit = NonSI.DAY;    break;
-                        case "absolute": isTimeAbsolute = true;   break;
-                        default: throw new DataStoreContentException(errors().getString(Errors.Keys.UnknownUnit_1, unit));
-                    }
-                    // Fall through
-            case 7: endTime     = Instant      .parse(       elements.get(6));
-            case 6: startTime   = Instant      .parse(       elements.get(5));
-            case 5: upperCorner = CharSequences.parseDoubles(elements.get(4), ORDINATE_SEPARATOR);
-            case 4: lowerCorner = CharSequences.parseDoubles(elements.get(3), ORDINATE_SEPARATOR);
-            case 3: final String dimension = elements.get(2);
-                    switch (dimension.toUpperCase(Locale.US)) {
-                        case "":   // Default to 2D.
-                        case "2D": break;
-                        case "3D": is3D = true; break;
-                        default: throw new DataStoreContentException(errors().getString(
-                                        Errors.Keys.IllegalCoordinateSystem_1, dimension));
-                    }
-                    // Fall through
-            case 2: crs = CRS.forCode(elements.get(1));
-            case 1:
-            case 0:
+        int ordinal = -1;
+        for (final String element : elements) {
+            ordinal++;
+            if (!element.isEmpty()) {
+                switch (ordinal) {
+                    case 0: continue;                                       // The "@stboundedby" header.
+                    case 1: crs = CRS.forCode(element); continue;
+                    case 2: if (element.length() == 2 && Character.toUpperCase(element.charAt(1)) == 'D') {
+                                spatialDimensionCount = element.charAt(0) - '0';
+                                if (spatialDimensionCount < 2 || spatialDimensionCount > 3) {
+                                    throw new DataStoreContentException(errors().getString(
+                                        Errors.Keys.IllegalCoordinateSystem_1, element));
+                                }
+                                continue;
+                            }
+                            /*
+                             * According the Moving Feature specification, the [dim] element is optional.
+                             * If we did not recognized the dimension, assume that we have the next element
+                             * (i.e. the lower corner). Fall-through so we can process it.
+                             */
+                            ordinal++;  // Fall through
+                    case 3: lowerCorner = CharSequences.parseDoubles(element, ORDINATE_SEPARATOR); continue;
+                    case 4: upperCorner = CharSequences.parseDoubles(element, ORDINATE_SEPARATOR); continue;
+                    case 5: startTime   = Instant.parse(element); continue;
+                    case 6: endTime     = Instant.parse(element); continue;
+                    case 7: switch (element.toLowerCase(Locale.US)) {
+                                case "sec":
+                                case "second":   /* Already SI.SECOND. */ continue;
+                                case "minute":   timeUnit = NonSI.MINUTE; continue;
+                                case "hour":     timeUnit = NonSI.HOUR;   continue;
+                                case "day":      timeUnit = NonSI.DAY;    continue;
+                                case "absolute": isTimeAbsolute = true;   continue;
+                                default: throw new DataStoreContentException(errors().getString(Errors.Keys.UnknownUnit_1, element));
+                            }
+                }
+                // If we reach this point, there is some remaining unknown elements. Ignore them.
+                break;
+            }
         }
         /*
          * Complete the CRS by adding a vertical component if needed, then a temporal component.
@@ -315,15 +318,19 @@ public final class Store extends DataSto
          *   Assumed never part of the authority code. We need to build the temporal component ourselves
          *   in order to set the origin to the start time.
          */
+        final GeneralEnvelope envelope;
         if (crs != null) {
-            final CoordinateReferenceSystem[] components = new CoordinateReferenceSystem[3];
-            final int spatialDimension = crs.getCoordinateSystem().getDimension();
             int count = 0;
+            final CoordinateReferenceSystem[] components = new CoordinateReferenceSystem[3];
             components[count++] = crs;
-            if (is3D && spatialDimension == 2) {
+
+            // If the coordinates are three-dimensional but the CRS is 2D, add a vertical axis.
+            if (spatialDimensionCount >= 3 && crs.getCoordinateSystem().getDimension() == 2) {
                 components[count++] = CommonCRS.Vertical.MEAN_SEA_LEVEL.crs();
             }
+            // Add a temporal axis if we have a start time (no need for end time).
             final GeodeticObjectBuilder builder = new GeodeticObjectBuilder();
+            String name = crs.getName().getCode();
             if (startTime != null) {
                 final TemporalCRS temporal;
                 if (isTimeAbsolute) {
@@ -334,29 +341,36 @@ public final class Store extends DataSto
                     timeEncoding = new TimeEncoding(temporal.getDatum(), timeUnit);
                 }
                 components[count++] = temporal;
+                name = name + " + " + temporal.getName().getCode();
             }
             crs = builder.addName(name).createCompoundCRS(ArraysExt.resize(components, count));
+            envelope = new GeneralEnvelope(crs);
+        } else {
             /*
-             * At this point we got the three- or four-dimensional spatio-temporal CRS.
-             * We can now set the envelope coordinate values.
+             * While illegal in principle, Apache SIS accepts missing CRS.
+             * In such case, use only the number of dimensions.
              */
-            envelope = new GeneralEnvelope(crs);
-            if (lowerCorner != null && upperCorner != null) {
-                int dim;
-                if ((dim = lowerCorner.length) != spatialDimension ||
-                    (dim = upperCorner.length) != spatialDimension)
-                {
-                    throw new DataStoreContentException(errors().getString(
-                            Errors.Keys.MismatchedDimension_2, dim, spatialDimension));
-                }
-                for (int i=0; i<spatialDimension; i++) {
-                    envelope.setRange(i, lowerCorner[i], upperCorner[i]);
-                }
-            }
-            if (startTime != null && endTime != null) {
-                envelope.setRange(spatialDimension, timeEncoding.toCRS(startTime.toEpochMilli()),
-                                                    timeEncoding.toCRS(endTime.toEpochMilli()));
-            }
+            int dim = spatialDimensionCount;
+            if (startTime != null) dim++;           // Same criterion than in above block.
+            envelope = new GeneralEnvelope(dim);
+        }
+        /*
+         * At this point we got the three- or four-dimensional spatio-temporal CRS.
+         * We can now set the envelope coordinate values, including temporal values.
+         */
+        int dim;
+        if ((dim = lowerCorner.length) != spatialDimensionCount ||
+            (dim = upperCorner.length) != spatialDimensionCount)
+        {
+            throw new DataStoreContentException(errors().getString(
+                    Errors.Keys.MismatchedDimension_2, dim, spatialDimensionCount));
+        }
+        for (int i=0; i<spatialDimensionCount; i++) {
+            envelope.setRange(i, lowerCorner[i], upperCorner[i]);
+        }
+        if (startTime != null) {
+            envelope.setRange(spatialDimensionCount, timeEncoding.toCRS(startTime.toEpochMilli()),
+                    (endTime == null) ? Double.NaN : timeEncoding.toCRS(endTime.toEpochMilli()));
         }
         return envelope;
     }
@@ -370,8 +384,8 @@ public final class Store extends DataSto
      *   &#64;columns, mfidref, trajectory, state,xsd:token, "type code",xsd:integer
      * }
      *
-     * @param  elements The line elements. The first elements should be {@code "@columns"}.
-     * @return The column metadata, or {@code null} if the given list does not contain enough elements.
+     * @param  elements  the line elements. The first elements should be {@code "@columns"}.
+     * @return the column metadata, or {@code null} if the given list does not contain enough elements.
      */
     private FeatureType parseFeatureType(final List<String> elements) throws DataStoreException {
         final int size = elements.size();
@@ -426,7 +440,7 @@ public final class Store extends DataSto
             }
             properties.add(createProperty(name, type, minOccurrence));
         }
-        return new DefaultFeatureType(Collections.singletonMap(DefaultFeatureType.NAME_KEY, name),
+        return new DefaultFeatureType(Collections.singletonMap(DefaultFeatureType.NAME_KEY, filename),
                 false, null, properties.toArray(new PropertyType[properties.size()]));
     }
 
@@ -439,15 +453,15 @@ public final class Store extends DataSto
 
     /**
      * Parses the metadata described by the header line starting with {@code @foliation}.
-     * The value returned by this method will be stored in the {@link #order} field.
+     * The value returned by this method will be stored in the {@link #foliation} field.
      *
      * <p>Example:</p>
      * {@preformat text
      *   &#64;foliation,Sequential
      * }
      *
-     * @param  elements The line elements. The first elements should be {@code "@foliation"}.
-     * @return The foliation metadata.
+     * @param  elements  the line elements. The first elements should be {@code "@foliation"}.
+     * @return the foliation metadata.
      */
     private Foliation parseFoliation(final List<String> elements) {
         if (elements.size() >= 2) {
@@ -459,130 +473,201 @@ public final class Store extends DataSto
     /**
      * Returns the metadata associated to the CSV file, or {@code null} if none.
      *
-     * @return The metadata associated to the CSV file, or {@code null} if none.
+     * @return the metadata associated to the CSV file, or {@code null} if none.
      * @throws DataStoreException if an error occurred during the parsing process.
      */
     @Override
-    public Metadata getMetadata() throws DataStoreException {
+    public synchronized Metadata getMetadata() throws DataStoreException {
         if (metadata == null) {
             final MetadataBuilder builder = new MetadataBuilder();
             builder.add(encoding);
+            builder.addFormat("CSV");
+            builder.add(ScopeCode.DATASET);
             try {
                 builder.addExtent(envelope);
             } catch (TransformException e) {
-                throw new DataStoreContentException(errors().getString(Errors.Keys.CanNotParseFile_2, "CSV", name), e);
+                throw new DataStoreContentException(errors().getString(Errors.Keys.CanNotParseFile_2, "CSV", filename), e);
+            } catch (UnsupportedOperationException e) {
+                // Failed to set the temporal components if the sis-temporal module was
+                // not on the classpath, but the other dimensions still have been set.
+                listeners.warning(null, e);
             }
+            builder.add(featureType, null);
             metadata = builder.build(true);
         }
         return metadata;
     }
 
     /**
-     * Returns an iterator over the features.
+     * Returns the stream of features.
      *
-     * @todo THIS IS AN EXPERIMENTAL API. We may change the return type to {@link java.util.stream.Stream} later.
-     * @todo Current implementation is inefficient. We should not parse all features immediately.
+     * @return a stream over all features in the CSV file.
      *
-     * @return An iterator over all features in the CSV file.
-     * @throws DataStoreException if an error occurred while creating the iterator.
+     * @todo Needs to reset the position when doing another pass on the features.
      */
-    @SuppressWarnings({"unchecked", "rawtypes", "fallthrough"})
-    public Iterator<Feature> getFeatures() throws DataStoreException {
-        if (features.isEmpty()) try {
-            final Collection<? extends PropertyType> properties = featureType.getProperties(false);
-            final ObjectConverter<String,?>[] converters = new ObjectConverter[properties.size()];
-            final String[]     propertyNames   = new String[converters.length];
-            final boolean      hasTrajectories = this.hasTrajectories;
-            final TimeEncoding timeEncoding    = this.timeEncoding;
-            final List<String> values          = new ArrayList<>();
-            int i = -1;
-            for (final PropertyType p : properties) {
-                propertyNames[++i] = p.getName().tip().toString();
-                switch (i) {    // This switch shall follow the same cases than the swith in the loop.
-                    case 1:
-                    case 2: if (timeEncoding != null) continue;     // else fall through
-                    case 3: if (hasTrajectories) continue;
+    public Stream<Feature> getFeatures() {
+        return StreamSupport.stream(new Spliterator<Feature>() {
+            /**
+             * Guarantees that we will not return null element.
+             */
+            @Override
+            public int characteristics() {
+                return NONNULL;
+            }
+
+            /**
+             * We do not know the number of features.
+             */
+            @Override
+            public long estimateSize() {
+                return Long.MAX_VALUE;
+            }
+
+            /**
+             * Current implementation can not split this iterator.
+             */
+            @Override
+            public Spliterator<Feature> trySplit() {
+                return null;
+            }
+
+            /**
+             * Returns the error message for a file that can not be parsed.
+             */
+            private String canNotParse() {
+                return errors().getString(Errors.Keys.CanNotParseFile_2, "CSV", filename);
+            }
+
+            /**
+             * Executes the given action only on the next feature, if any.
+             */
+            @Override
+            public boolean tryAdvance(final Consumer<? super Feature> action) {
+                try {
+                    return read(action, false);
+                } catch (IOException | IllegalArgumentException | DateTimeException e) {
+                    throw new BackingStoreException(canNotParse(), e);
                 }
-                converters[i] = ObjectConverters.find(String.class, ((AttributeType) p).getValueClass());
             }
-            /*
-             * Above lines prepared the constants. Now parse all lines.
-             * TODO: We should move the code below this point in a custom Iterator implementation.
+
+            /**
+             * Executes the given action only on all remaining features.
              */
-            String line;
-            while ((line = source.readLine()) != null) {
-                split(line, values);
-                final int length = Math.min(propertyNames.length, values.size());
-                if (length != 0) {
-                    final Feature feature = featureType.newInstance();
-                    for (i=0; i<length; i++) {
-                        final String text = values.get(i);
-                        final String name = propertyNames[i];
-                        final Object value;
-                        /*
-                         * According Moving Features specification:
-                         *   Column 0 is the feature identifier (mfidref). There is nothing special to do here.
-                         *   Column 1 is the start time.
-                         *   Column 2 is the end time.
-                         *   Column 3 is the trajectory.
-                         *   Columns 4+ are custom attributes.
-                         *
-                         * TODO: we should replace that switch case by custom ObjectConverter.
-                         */
-                        switch (i) {
-                            case 1:
-                            case 2: {
-                                if (timeEncoding != null) {
-                                    if (timeEncoding == TimeEncoding.ABSOLUTE) {
-                                        value = Instant.parse(text).toEpochMilli();
-                                    } else {
-                                        value = Instant.ofEpochMilli(timeEncoding.toMillis(Double.parseDouble(text)));
+            @Override
+            public void forEachRemaining(final Consumer<? super Feature> action) {
+                try {
+                    read(action, true);
+                } catch (IOException | IllegalArgumentException | DateTimeException e) {
+                    throw new BackingStoreException(canNotParse(), e);
+                }
+            }
+
+            /**
+             * Executes the given action for the next feature or for all remaining features.
+             *
+             * @param  action  the action to execute.
+             * @param  all     {@code true} for executing the given action on all remaining features.
+             * @return {@code false} if there is no remaining feature after this method call.
+             * @throws IOException if an I/O error occurred while reading a feature.
+             * @throws IllegalArgumentException if parsing of a number failed, or other error.
+             * @throws DateTimeException if parsing of a date failed.
+             */
+            @SuppressWarnings({"unchecked", "rawtypes", "fallthrough"})
+            private boolean read(final Consumer<? super Feature> action, boolean all) throws IOException {
+                final Collection<? extends PropertyType> properties = featureType.getProperties(false);
+                final ObjectConverter<String,?>[] converters = new ObjectConverter[properties.size()];
+                final String[]     propertyNames   = new String[converters.length];
+                final boolean      hasTrajectories = Store.this.hasTrajectories;
+                final TimeEncoding timeEncoding    = Store.this.timeEncoding;
+                final List<String> values          = new ArrayList<>();
+                int i = -1;
+                for (final PropertyType p : properties) {
+                    propertyNames[++i] = p.getName().tip().toString();
+                    switch (i) {    // This switch shall follow the same cases than the swith in the loop.
+                        case 1:
+                        case 2: if (timeEncoding != null) continue;     // else fall through
+                        case 3: if (hasTrajectories) continue;
+                    }
+                    converters[i] = ObjectConverters.find(String.class, ((AttributeType) p).getValueClass());
+                }
+                /*
+                 * Above lines prepared the constants. Now parse all lines.
+                 * TODO: We should move the code below this point in a custom Iterator implementation.
+                 */
+                String line;
+                while ((line = source.readLine()) != null) {
+                    split(line, values);
+                    final int length = Math.min(propertyNames.length, values.size());
+                    if (length != 0) {
+                        final Feature feature = featureType.newInstance();
+                        for (i=0; i<length; i++) {
+                            final String text = values.get(i);
+                            final String name = propertyNames[i];
+                            final Object value;
+                            /*
+                             * According Moving Features specification:
+                             *   Column 0 is the feature identifier (mfidref). There is nothing special to do here.
+                             *   Column 1 is the start time.
+                             *   Column 2 is the end time.
+                             *   Column 3 is the trajectory.
+                             *   Columns 4+ are custom attributes.
+                             *
+                             * TODO: we should replace that switch case by custom ObjectConverter.
+                             */
+                            switch (i) {
+                                case 1:
+                                case 2: {
+                                    if (timeEncoding != null) {
+                                        if (timeEncoding == TimeEncoding.ABSOLUTE) {
+                                            value = Instant.parse(text).toEpochMilli();
+                                        } else {
+                                            value = Instant.ofEpochMilli(timeEncoding.toMillis(Double.parseDouble(text)));
+                                        }
+                                        break;
                                     }
-                                    break;
+                                    /*
+                                     * If there is no time columns, then this column may the trajectory (note that allowing
+                                     * CSV files without time is obviously a departure from Moving Features specification.
+                                     * The intend is to have a CSV format applicable to other features than moving ones).
+                                     * Fall through in order to process trajectory.
+                                     */
                                 }
-                                /*
-                                 * If there is no time columns, then this column may the trajectory (note that allowing
-                                 * CSV files without time is obviously a departure from Moving Features specification.
-                                 * The intend is to have a CSV format applicable to other features than moving ones).
-                                 * Fall through in order to process trajectory.
-                                 */
-                            }
-                            case 3: {
-                                if (hasTrajectories) {
-                                    value = CharSequences.parseDoubles(text, ORDINATE_SEPARATOR);
+                                case 3: {
+                                    if (hasTrajectories) {
+                                        value = CharSequences.parseDoubles(text, ORDINATE_SEPARATOR);
+                                        break;
+                                    }
+                                    /*
+                                     * If there is no trajectory columns, than this column is a custum attribute.
+                                     * CSV files without trajectories are not compliant with Moving Feature spec.,
+                                     * but we try to keep this reader a little bit more generic.
+                                     */
+                                }
+                                default: {
+                                    value = converters[i].apply(text);
                                     break;
                                 }
-                                /*
-                                 * If there is no trajectory columns, than this column is a custum attribute.
-                                 * CSV files without trajectories are not compliant with Moving Feature spec.,
-                                 * but we try to keep this reader a little bit more generic.
-                                 */
-                            }
-                            default: {
-                                value = converters[i].apply(text);
-                                break;
                             }
+                            feature.setPropertyValue(name, value);
                         }
-                        feature.setPropertyValue(name, value);
+                        action.accept(feature);
+                        if (!all) return true;
                     }
-                    features.add(feature);
+                    values.clear();
                 }
-                values.clear();
+                return false;
             }
-        } catch (IOException | IllegalArgumentException | DateTimeException e) {
-            throw new DataStoreException(errors().getString(Errors.Keys.CanNotParseFile_2, "CSV", name), e);
-        }
-        return features.iterator();
+        }, false);
     }
 
     /**
      * Splits the content of the given line around the column separator.
      * Quotes are taken in account. The elements are added in the given list.
      *
-     * @param line the line to parse.
-     * @param elements an initially empty list where to add elements.
+     * @param line      the line to parse.
+     * @param elements  an initially empty list where to add elements.
      */
-    private static void split(final String line, final List<String> elements) {
+    static void split(final String line, final List<String> elements) {
         int startAt = 0;
         boolean hasQuotes = false;
         boolean isQuoting = false;
@@ -629,7 +714,7 @@ public final class Store extends DataSto
             }
             text = CharSequences.trimWhitespaces(buffer);
         } else {
-            text = CharSequences.trimWhitespaces(text, lower, upper).toString();
+            text = CharSequences.trimWhitespaces(text, lower, upper);
         }
         return text.toString();
     }
@@ -651,13 +736,12 @@ public final class Store extends DataSto
     /**
      * Closes this data store and releases any underlying resources.
      *
-     * @throws DataStoreException If an error occurred while closing this data store.
+     * @throws DataStoreException if an error occurred while closing this data store.
      */
     @Override
-    public void close() throws DataStoreException {
+    public synchronized void close() throws DataStoreException {
         final BufferedReader s = source;
         source = null;                  // Cleared first in case of failure.
-        features.clear();
         if (s != null) try {
             s.close();
         } catch (IOException e) {

Modified: sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/TimeEncoding.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/TimeEncoding.java?rev=1759701&r1=1759700&r2=1759701&view=diff
==============================================================================
--- sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/TimeEncoding.java [UTF-8] (original)
+++ sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/TimeEncoding.java [UTF-8] Wed Sep  7 20:50:07 2016
@@ -66,8 +66,8 @@ final class TimeEncoding {
     /**
      * Converts the given timestamp to the values used in the temporal coordinate reference system.
      *
-     * @param time Number of milliseconds elapsed since January 1st, 1970 midnight UTC.
-     * @return The value to use with the temporal coordinate reference system.
+     * @param  time  number of milliseconds elapsed since January 1st, 1970 midnight UTC.
+     * @return the value to use with the temporal coordinate reference system.
      */
     final double toCRS(final long time) {
         return (time - origin) / interval;
@@ -76,8 +76,8 @@ final class TimeEncoding {
     /**
      * Reverse of {@link #toCRS(long)}.
      *
-     * @param time The value used with the temporal coordinate reference system.
-     * @return Number of milliseconds elapsed since January 1st, 1970 midnight UTC.
+     * @param  time  the value used with the temporal coordinate reference system.
+     * @return number of milliseconds elapsed since January 1st, 1970 midnight UTC.
      */
     final long toMillis(final double time) {
         return Math.round(time * interval) + origin;

Modified: sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/package-info.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/package-info.java?rev=1759701&r1=1759700&r2=1759701&view=diff
==============================================================================
--- sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/package-info.java [UTF-8] (original)
+++ sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/csv/package-info.java [UTF-8] Wed Sep  7 20:50:07 2016
@@ -23,14 +23,27 @@
  *   <li><a href="http://docs.opengeospatial.org/is/14-084r2/14-084r2.html">OGC® Moving Features Encoding Extension:
  *     Simple Comma Separated Values (CSV)</a> with some rules relaxed. The Apache SIS implementation allows the CSV
  *     file to have no date, thus allowing use of OGC 14-084 syntax for static features in addition to moving ones.</li>
+ *   <li><a href="http://www.ietf.org/rfc/rfc4180.txt">Common Format and MIME Type for Comma-Separated Values (CSV) Files</a>.
+ *     This is supported indirectly since above OGC specification is an extension of this IETF specification.</li>
  * </ul>
  *
- * The above extends the <a href="http://www.ietf.org/rfc/rfc4180.txt">Common Format and MIME Type for
- * Comma-Separated Values (CSV) Files</a> specification.
+ * Example of moving features CSV file (adapted from OGC specification):
+ *
+ * {@preformat text
+ *   &#64;stboundedby, urn:x-ogc:def:crs:EPSG::4326, 2D, 9.23 50.23, 9.27 50.31, 2012-01-17T12:33:41Z, 2012-01-17T12:37:00Z, sec
+ *   &#64;columns,mfidref,trajectory,state,xsd:token,”type code”,xsd:integer
+ *   a,  10, 150, 11.0 2.0 12.0 3.0, walking, 1
+ *   b,  10, 190, 10.0 2.0 11.0 3.0, walking, 2
+ *   a, 150, 190, 12.0 3.0 10.0 3.0, walking, 2
+ *   c,  10, 190, 12.0 1.0 10.0 2.0 11.0 3.0, vehicle, 1
+ * }
+ *
+ * <div class="section">Departures from OGC specification</div>
+ * If the character encoding is not explicitely specified, then Apache SIS uses the platform default instead of UTF-8.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.7
- * @version 0.7
+ * @version 0.8
  * @module
  */
 package org.apache.sis.internal.storage.csv;

Modified: sis/branches/JDK8/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/csv/StoreTest.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/csv/StoreTest.java?rev=1759701&r1=1759700&r2=1759701&view=diff
==============================================================================
--- sis/branches/JDK8/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/csv/StoreTest.java [UTF-8] (original)
+++ sis/branches/JDK8/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/csv/StoreTest.java [UTF-8] Wed Sep  7 20:50:07 2016
@@ -19,8 +19,8 @@ package org.apache.sis.internal.storage.
 import java.util.Iterator;
 import java.io.StringReader;
 import org.opengis.metadata.Metadata;
+import org.opengis.metadata.extent.Extent;
 import org.opengis.metadata.extent.GeographicBoundingBox;
-import org.opengis.metadata.extent.SpatialTemporalExtent;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.test.TestCase;
@@ -38,7 +38,7 @@ import org.opengis.feature.Feature;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.7
- * @version 0.7
+ * @version 0.8
  * @module
  */
 public final class StoreTest extends TestCase {
@@ -60,21 +60,18 @@ public final class StoreTest extends Tes
      * @throws DataStoreException if an error occurred while parsing the data.
      */
     @Test
-    @org.junit.Ignore("Pending completion of sis-referencing")
     public void testGetMetadata() throws DataStoreException {
         final Metadata metadata;
         try (Store store = new Store(new StorageConnector(new StringReader(TEST_DATA)))) {
             metadata = store.getMetadata();
         }
-        final SpatialTemporalExtent extent = (SpatialTemporalExtent) getSingleton(getSingleton(getSingleton(
-                metadata.getIdentificationInfo()).getExtents()).getTemporalElements());
-        final GeographicBoundingBox bbox = (GeographicBoundingBox) getSingleton(extent.getSpatialExtent());
+        final Extent extent = getSingleton(getSingleton(metadata.getIdentificationInfo()).getExtents());
+        final GeographicBoundingBox bbox = (GeographicBoundingBox) getSingleton(extent.getGeographicElements());
         assertEquals("westBoundLongitude", 50.23, bbox.getWestBoundLongitude(), STRICT);
         assertEquals("eastBoundLongitude", 50.31, bbox.getEastBoundLongitude(), STRICT);
         assertEquals("southBoundLatitude",  9.23, bbox.getSouthBoundLatitude(), STRICT);
         assertEquals("northBoundLatitude",  9.27, bbox.getNorthBoundLatitude(), STRICT);
-        assertNull("Should not have a vertical extent.", extent.getVerticalExtent());
-        assertNotNull("Should have a temporal extent", extent.getExtent());
+        assertTrue("Should not have a vertical extent.", extent.getVerticalElements().isEmpty());
     }
 
     /**
@@ -85,7 +82,7 @@ public final class StoreTest extends Tes
     @Test
     public void testGetFeatures() throws DataStoreException {
         try (Store store = new Store(new StorageConnector(new StringReader(TEST_DATA)))) {
-            final Iterator<Feature> it = store.getFeatures();
+            final Iterator<Feature> it = store.getFeatures().iterator();
             assertFeatureEquals(it.next(), "a", new double[] {11, 2, 12, 3},        "walking", 1);
             assertFeatureEquals(it.next(), "b", new double[] {10, 2, 11, 3},        "walking", 2);
             assertFeatureEquals(it.next(), "a", new double[] {12, 3, 10, 3},        "walking", 2);



Mime
View raw message