/* Copyright (c) 2013-2014 Boundless and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/edl-v10.html
*
* Contributors:
* Gabriel Roldan (Boundless) - initial implementation
*/
package org.locationtech.geogig.geotools.data;
import java.io.IOException;
import java.util.AbstractList;
import java.util.Iterator;
import java.util.List;
import org.geotools.data.EmptyFeatureReader;
import org.geotools.data.FeatureReader;
import org.geotools.data.FeatureWriter;
import org.geotools.data.Query;
import org.geotools.data.QueryCapabilities;
import org.geotools.data.ResourceInfo;
import org.geotools.data.Transaction;
import org.geotools.data.store.ContentEntry;
import org.geotools.data.store.ContentFeatureStore;
import org.geotools.data.store.ContentState;
import org.geotools.data.store.FeatureIteratorIterator;
import org.geotools.factory.Hints;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.FeatureReaderIterator;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.filter.identity.FeatureIdVersionedImpl;
import org.geotools.filter.visitor.SimplifyingFilterVisitor;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.locationtech.geogig.api.DefaultProgressListener;
import org.locationtech.geogig.api.Node;
import org.locationtech.geogig.api.NodeRef;
import org.locationtech.geogig.api.ProgressListener;
import org.locationtech.geogig.repository.WorkingTree;
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureVisitor;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.Name;
import org.opengis.filter.Filter;
import org.opengis.filter.identity.FeatureId;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
/**
*
*/
class GeogigFeatureStore extends ContentFeatureStore {
/**
* geogig feature source to delegate to, we do this b/c we can't inherit from both
* ContentFeatureStore and {@link GeogigFeatureSource} at the same time
*/
private GeogigFeatureSource delegate;
/**
* @param entry
* @param query
*/
public GeogigFeatureStore(ContentEntry entry) {
super(entry, (Query) null);
delegate = new GeogigFeatureSource(entry, query) {
@Override
public void setTransaction(Transaction transaction) {
super.setTransaction(transaction);
// keep this feature store in sync
GeogigFeatureStore.this.setTransaction(transaction);
}
};
super.hints = delegate.getSupportedHints();
}
/** We handle events internally */
protected boolean canEvent() {
return true;
}
@Override
public GeoGigDataStore getDataStore() {
return delegate.getDataStore();
}
public GeogigFeatureSource getFeatureSource() {
return delegate;
}
@Override
public ContentEntry getEntry() {
return delegate.getEntry();
}
@Override
public ResourceInfo getInfo() {
return delegate.getInfo();
}
@Override
public Name getName() {
return delegate.getName();
}
@Override
public QueryCapabilities getQueryCapabilities() {
return delegate.getQueryCapabilities();
}
@Override
public ContentState getState() {
return delegate.getState();
}
@Override
public synchronized Transaction getTransaction() {
return delegate.getTransaction();
}
@Override
public synchronized void setTransaction(Transaction transaction) {
// we need to set both super and delegate transactions.
super.setTransaction(transaction);
// this guard ensures that a recursive loop will not form
if (delegate.getTransaction() != transaction) {
delegate.setTransaction(transaction);
}
if (!Transaction.AUTO_COMMIT.equals(transaction)) {
GeogigTransactionState geogigTx;
geogigTx = (GeogigTransactionState) transaction.getState(GeogigTransactionState.class);
if (geogigTx == null) {
geogigTx = new GeogigTransactionState(getEntry());
transaction.putState(GeogigTransactionState.class, geogigTx);
}
}
}
@Override
protected SimpleFeatureType buildFeatureType() throws IOException {
return delegate.buildFeatureType();
}
@Override
protected int getCountInternal(Query query) throws IOException {
return delegate.getCount(query);
}
@Override
protected ReferencedEnvelope getBoundsInternal(Query query) throws IOException {
return delegate.getBoundsInternal(query);
}
@Override
protected boolean canFilter() {
return delegate.canFilter();
}
@Override
protected boolean canSort() {
return delegate.canSort();
}
@Override
protected boolean canRetype() {
return delegate.canRetype();
}
@Override
protected boolean canLimit() {
return delegate.canLimit();
}
@Override
protected boolean canOffset() {
return delegate.canOffset();
}
@Override
protected boolean canTransact() {
return delegate.canTransact();
}
@Override
protected FeatureReader<SimpleFeatureType, SimpleFeature> getReaderInternal(Query query)
throws IOException {
return delegate.getReaderInternal(query);
}
@Override
protected boolean handleVisitor(Query query, FeatureVisitor visitor) throws IOException {
return delegate.handleVisitor(query, visitor);
}
@Override
protected FeatureWriter<SimpleFeatureType, SimpleFeature> getWriterInternal(Query query,
final int flags) throws IOException {
Preconditions.checkArgument(flags != 0, "no write flags set");
Preconditions.checkState(getDataStore().isAllowTransactions(),
"Transactions not supported; head is not a local branch");
FeatureReader<SimpleFeatureType, SimpleFeature> features;
if ((flags | WRITER_UPDATE) == WRITER_UPDATE) {
features = delegate.getReader(query);
} else {
features = new EmptyFeatureReader<SimpleFeatureType, SimpleFeature>(getSchema());
}
String path = delegate.getTypeTreePath();
WorkingTree wtree = getFeatureSource().getWorkingTree();
GeoGigFeatureWriter writer;
if ((flags | WRITER_ADD) == WRITER_ADD) {
writer = GeoGigFeatureWriter.createAppendable(features, path, wtree);
} else {
writer = GeoGigFeatureWriter.create(features, path, wtree);
}
return writer;
}
@Override
public final List<FeatureId> addFeatures(
FeatureCollection<SimpleFeatureType, SimpleFeature> featureCollection)
throws IOException {
if (Transaction.AUTO_COMMIT.equals(getTransaction())) {
throw new UnsupportedOperationException("GeoGIG does not support AUTO_COMMIT");
}
Preconditions.checkState(getDataStore().isAllowTransactions(),
"Transactions not supported; head is not a local branch");
final WorkingTree workingTree = delegate.getWorkingTree();
final String path = delegate.getTypeTreePath();
ProgressListener listener = new DefaultProgressListener();
final List<FeatureId> insertedFids = Lists.newArrayList();
List<Node> deferringTarget = new AbstractList<Node>() {
@Override
public boolean add(Node node) {
String fid = node.getName();
String version = node.getObjectId().toString();
insertedFids.add(new FeatureIdVersionedImpl(fid, version));
return true;
}
@Override
public Node get(int index) {
throw new UnsupportedOperationException();
}
@Override
public int size() {
return 0;
}
};
Integer count = (Integer) null;
FeatureIterator<SimpleFeature> featureIterator = featureCollection.features();
try {
Iterator<SimpleFeature> features;
features = new FeatureIteratorIterator<SimpleFeature>(featureIterator);
/*
* Make sure to transform the incoming features to the native schema to avoid situations
* where geogig would change the metadataId of the RevFeature nodes due to small
* differences in the default and incoming schema such as namespace or missing
* properties
*/
final SimpleFeatureType nativeSchema = delegate.getNativeType();
features = Iterators.transform(features, new SchemaInforcer(nativeSchema));
workingTree.insert(path, features, listener, deferringTarget, count);
} catch (Exception e) {
throw new IOException(e);
} finally {
featureIterator.close();
}
return insertedFids;
}
/**
* Function used when inserted to check whether the {@link Hints#USE_PROVIDED_FID} in a Feature
* {@link Feature#getUserData() user data} map is set to {@code Boolean.TRUE}, and only if so
* let the feature unchanged, otherwise return a feature with the exact same contents but a
* newly generaged feature id.
*/
private static class SchemaInforcer implements Function<SimpleFeature, SimpleFeature> {
private SimpleFeatureBuilder builder;
public SchemaInforcer(final SimpleFeatureType targetSchema) {
this.builder = new SimpleFeatureBuilder(targetSchema);
}
@Override
public SimpleFeature apply(SimpleFeature input) {
builder.reset();
for (int i = 0; i < input.getType().getAttributeCount(); i++) {
String name = input.getType().getDescriptor(i).getLocalName();
builder.set(name, input.getAttribute(name));
}
String id;
if (Boolean.TRUE.equals(input.getUserData().get(Hints.USE_PROVIDED_FID))) {
id = input.getID();
} else {
id = null;
}
SimpleFeature feature = builder.buildFeature(id);
if (!input.getUserData().isEmpty()) {
feature.getUserData().putAll(input.getUserData());
}
return feature;
}
};
@Override
public void modifyFeatures(Name[] names, Object[] values, Filter filter) throws IOException {
Preconditions.checkState(getDataStore().isAllowTransactions(),
"Transactions not supported; head is not a local branch");
final WorkingTree workingTree = delegate.getWorkingTree();
final String path = delegate.getTypeTreePath();
Iterator<SimpleFeature> features = modifyingFeatureIterator(names, values, filter);
/*
* Make sure to transform the incoming features to the native schema to avoid situations
* where geogig would change the metadataId of the RevFeature nodes due to small differences
* in the default and incoming schema such as namespace or missing properties
*/
final SimpleFeatureType nativeSchema = delegate.getNativeType();
features = Iterators.transform(features, new SchemaInforcer(nativeSchema));
try {
ProgressListener listener = new DefaultProgressListener();
Integer count = (Integer) null;
List<Node> target = (List<Node>) null;
workingTree.insert(path, features, listener, target, count);
} catch (Exception e) {
throw new IOException(e);
}
}
/**
* @param names
* @param values
* @param filter
* @return
* @throws IOException
*/
private Iterator<SimpleFeature> modifyingFeatureIterator(final Name[] names,
final Object[] values, final Filter filter) throws IOException {
Iterator<SimpleFeature> iterator = featureIterator(filter);
Function<SimpleFeature, SimpleFeature> modifyingFunction = new ModifyingFunction(names,
values);
Iterator<SimpleFeature> modifyingIterator = Iterators
.transform(iterator, modifyingFunction);
return modifyingIterator;
}
private Iterator<SimpleFeature> featureIterator(final Filter filter) throws IOException {
FeatureReader<SimpleFeatureType, SimpleFeature> unchanged = getReader(filter);
Iterator<SimpleFeature> iterator = new FeatureReaderIterator<SimpleFeature>(unchanged);
return iterator;
}
@Override
public void removeFeatures(Filter filter) throws IOException {
Preconditions.checkState(getDataStore().isAllowTransactions(),
"Transactions not supported; head is not a local branch");
final WorkingTree workingTree = delegate.getWorkingTree();
final String typeTreePath = delegate.getTypeTreePath();
filter = (Filter) filter.accept(new SimplifyingFilterVisitor(), null);
if (Filter.INCLUDE.equals(filter)) {
workingTree.delete(typeTreePath);
return;
}
if (Filter.EXCLUDE.equals(filter)) {
return;
}
Iterator<SimpleFeature> featureIterator = featureIterator(filter);
Iterator<String> affectedFeaturePaths = Iterators.transform(featureIterator,
new Function<SimpleFeature, String>() {
@Override
public String apply(SimpleFeature input) {
String fid = input.getID();
return NodeRef.appendChild(typeTreePath, fid);
}
});
workingTree.delete(affectedFeaturePaths);
}
/**
*
*/
private static final class ModifyingFunction implements Function<SimpleFeature, SimpleFeature> {
private Name[] names;
private Object[] values;
/**
* @param names
* @param values
*/
public ModifyingFunction(Name[] names, Object[] values) {
this.names = names;
this.values = values;
}
@Override
public SimpleFeature apply(SimpleFeature input) {
for (int i = 0; i < names.length; i++) {
Name attName = names[i];
Object attValue = values[i];
input.setAttribute(attName, attValue);
}
input.getUserData().put(Hints.USE_PROVIDED_FID, Boolean.TRUE);
return input;
}
}
}