blob: a387f6ac28d11847335de0451ef6b2a02412ba26 [file] [log] [blame]
/*
* 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.internal.storage.csv;
import java.util.Collection;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.concurrent.atomic.AtomicInteger;
import java.time.DateTimeException;
import java.io.IOException;
import org.apache.sis.util.ObjectConverter;
import org.apache.sis.util.ObjectConverters;
import org.apache.sis.util.collection.BackingStoreException;
// Branch-dependent imports
import org.apache.sis.feature.AbstractFeature;
import org.apache.sis.feature.AbstractIdentifiedType;
import org.apache.sis.feature.DefaultAttributeType;
/**
* Base implementation of iterators returned by {@link Store#features(boolean)}. This base class returns one feature
* per line. For example iteration over the following file will produce 4 {@code Feature} instances, even if there is
* actually only three distinct instances because the feature "a" is splitted on 2 lines:
*
* {@preformat text
* a, 10, 150, 11.0 2.0 12.0 3.0
* b, 10, 190, 10.0 2.0 11.0 3.0
* a, 150, 190, 12.0 3.0 10.0 3.0
* c, 10, 190, 12.0 1.0 10.0 2.0 11.0 3.0
* }
*
* <b>Multi-threading:</b> {@code Iter} is not thread-safe.
* However many {@code Iter} instances can be used concurrently for the same {@link Store} instance.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.8
* @since 0.7
* @module
*/
class FeatureIterator implements Spliterator<AbstractFeature> {
/**
* Index of the column containing trajectory coordinates.
* Columns before the trajectory are Moving Feature identifier {@code mfIdRef}, start time and end time.
*/
static final int TRAJECTORY_COLUMN = 3;
/**
* Connection to the CSV file.
*/
final Store store;
/**
* Name of the property where to store a value.
* This array be considered unmodifiable and may be shared between many {@code Iter} instances.
*/
final String[] propertyNames;
/**
* Converters from string representations to the values to store in the {@link #values} array.
* This array be considered unmodifiable and may be shared between many {@code Iter} instances.
*/
final ObjectConverter<String,?>[] converters;
/**
* All values found in a row. We need to remember those values between different executions
* of the {@link #tryAdvance(Consumer)} method because the Moving Feature Specification said:
* "If the value equals the previous value, the text for the value can be omitted."
*/
final Object[] values;
/**
* Number of calls to {@link #trySplit()}. Created only if needed.
*/
private AtomicInteger splitCount;
/**
* Creates a new iterator.
*/
@SuppressWarnings({"unchecked", "rawtypes", "fallthrough"})
FeatureIterator(final Store store) {
this.store = store;
final Collection<? extends AbstractIdentifiedType> properties = store.featureType.getProperties(true);
converters = new ObjectConverter[properties.size()];
values = new Object[converters.length];
propertyNames = new String[converters.length];
int i = -1;
for (final AbstractIdentifiedType p : properties) {
propertyNames[++i] = p.getName().tip().toString();
/*
* 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.
*/
final ObjectConverter<String,?> c;
switch (i) {
case 1: // Fall through
case 2: {
final TimeEncoding timeEncoding = store.timeEncoding();
if (timeEncoding != null) {
c = timeEncoding;
break;
}
/*
* If there is no time columns, then this column may be the trajectory (note that allowing
* CSV files without time is obviously a departure from Moving Features specification.
* The intent is to have a CSV format applicable to other features than moving ones).
* Fall through in order to process trajectory.
*/
}
case TRAJECTORY_COLUMN: {
if (store.hasTrajectories()) {
c = GeometryParser.INSTANCE;
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: {
c = ObjectConverters.find(String.class, ((DefaultAttributeType) p).getValueClass());
break;
}
}
converters[i] = c;
}
}
/**
* Creates a new iterator using the same configuration than the given iterator.
* This constructor is for {@link #trySplit()} implementation only.
*/
private FeatureIterator(final FeatureIterator other) {
store = other.store;
splitCount = other.splitCount;
converters = other.converters;
propertyNames = other.propertyNames;
values = new Object[converters.length];
}
/**
* If this spliterator can be partitioned, returns a {@code Spliterator} covering elements.
* This method does not make any guarantees about iteration order; i.e. the returned iterator
* is not guaranteed to cover a strict prefix of the elements.
*/
@Override
public Spliterator<AbstractFeature> trySplit() {
if (splitCount == null) {
splitCount = new AtomicInteger();
}
if (splitCount.incrementAndGet() < 8) { // Arbitrary limit.
return new FeatureIterator(this);
}
return null;
}
/**
* Executes the given action only on the next feature, if any.
*/
@Override
public boolean tryAdvance(final Consumer<? super AbstractFeature> action) {
try {
return read(action, false);
} catch (IOException | IllegalArgumentException | DateTimeException e) {
throw new BackingStoreException(store.canNotParseFile(), e);
}
}
/**
* Executes the given action on all remaining features.
*/
@Override
public void forEachRemaining(final Consumer<? super AbstractFeature> action) {
try {
read(action, true);
} catch (IOException | IllegalArgumentException | DateTimeException e) {
throw new BackingStoreException(store.canNotParseFile(), e);
}
}
/**
* Executes the given action for the next feature or for all remaining features.
* The features are assumed static, with one feature per line.
* This method is for {@link #tryAdvance(Consumer)} and {@link #forEachRemaining(Consumer)} implementations.
*
* <p><b>Multi-threading:</b>
* {@code Iter} does not need to be thread-safe, so we do not perform synchronization for its {@link #values}.
* Accesses to {@code Store} fields need to be thread-safe, but this method uses only immutable or thread-safe
* objects from {@link Store}, so there is no need for {@code synchronize(Store.this)} statement.
* The only object that need synchronization is {@link Store#source}, which is already synchronized.</p>
*
* @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.
*/
private boolean read(final Consumer<? super AbstractFeature> action, final boolean all) throws IOException {
final FixedSizeList elements = new FixedSizeList(values);
String line;
while ((line = store.readLine()) != null) {
Store.split(line, elements);
final AbstractFeature feature = store.featureType.newInstance();
int i, n = elements.size();
for (i=0; i<n; i++) {
values[i] = converters[i].apply((String) values[i]);
feature.setPropertyValue(propertyNames[i], values[i]);
}
n = values.length;
for (; i<n; i++) {
// For omitted elements, reuse previous value.
feature.setPropertyValue(propertyNames[i], values[i]);
}
action.accept(feature);
if (!all) return true;
elements.clear();
}
return false;
}
/**
* We do not know the number of features.
*/
@Override
public long estimateSize() {
return Long.MAX_VALUE;
}
/**
* Returns the characteristics of the iteration over feature instances.
* The iteration is {@link #NONNULL} (i.e. {@link #tryAdvance(Consumer)} is not allowed
* to return null value) and {@link #IMMUTABLE} (i.e. we do not support modification of
* the CSV file while an iteration is in progress).
* The iteration is not declared {@link #ORDERED} because {@link #trySplit()} does not
* return a strict prefix of the elements.
*
* @return characteristics of iteration over the features in the CSV file.
*/
@Override
public int characteristics() {
return NONNULL | IMMUTABLE;
}
}