/* 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.Set; import javax.annotation.Nullable; import org.geotools.data.DataUtilities; import org.geotools.data.FeatureReader; import org.geotools.data.FeatureSource; import org.geotools.data.MaxFeatureReader; import org.geotools.data.Query; import org.geotools.data.QueryCapabilities; import org.geotools.data.Transaction; import org.geotools.data.sort.SortedFeatureReader; import org.geotools.data.store.ContentEntry; import org.geotools.data.store.ContentFeatureSource; import org.geotools.data.store.ContentState; import org.geotools.factory.Hints; import org.geotools.feature.simple.SimpleFeatureTypeBuilder; import org.geotools.filter.Filters; import org.geotools.filter.visitor.SimplifyingFilterVisitor; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.renderer.ScreenMap; import org.locationtech.geogig.api.Context; import org.locationtech.geogig.api.NodeRef; import org.locationtech.geogig.api.ObjectId; import org.locationtech.geogig.api.RevFeatureType; import org.locationtech.geogig.api.RevTree; import org.locationtech.geogig.api.plumbing.RevObjectParse; import org.locationtech.geogig.geotools.data.GeoGigDataStore.ChangeType; 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.sort.SortBy; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Optional; import com.google.common.base.Preconditions; /** * */ class GeogigFeatureSource extends ContentFeatureSource { private static final Logger LOGGER = LoggerFactory.getLogger(GeogigFeatureSource.class); private GeoGigDataStore.ChangeType changeType; private String oldRoot; /** * <b>Precondition</b>: {@code entry.getDataStore() instanceof GeoGigDataStore} * * @param entry */ public GeogigFeatureSource(ContentEntry entry) { this(entry, (Query) null); } /** * <b>Precondition</b>: {@code entry.getDataStore() instanceof GeoGigDataStore} * * @param entry * @param query optional "definition query" making this feature source a "view" */ public GeogigFeatureSource(ContentEntry entry, @Nullable Query query) { super(entry, query); Preconditions.checkArgument(entry.getDataStore() instanceof GeoGigDataStore); } /** * Adds the {@link Hints#FEATURE_DETACHED} hint to the supported hints so the renderer doesn't * clone the geometries */ @Override protected void addHints(Set<Hints.Key> hints) { hints.add(Hints.FEATURE_DETACHED); hints.add(Hints.SCREENMAP); } @Override protected boolean canFilter() { return true; } @Override protected boolean canSort() { return true; } @Override protected boolean canRetype() { return false; } @Override protected boolean canLimit() { return true; } @Override protected boolean canOffset() { return true; } /** * @return {@code true} */ @Override protected boolean canTransact() { return true; } @Override protected boolean handleVisitor(Query query, FeatureVisitor visitor) throws IOException { return false; } @Override public GeoGigDataStore getDataStore() { return (GeoGigDataStore) super.getDataStore(); } @Override public ContentState getState() { return super.getState(); } /** * Overrides {@link ContentFeatureSource#getName()} to restore back the original meaning of * {@link FeatureSource#getName()} */ @Override public Name getName() { return getEntry().getName(); } /** * Creates a {@link QueryCapabilities} that declares support for * {@link QueryCapabilities#isUseProvidedFIDSupported() isUseProvidedFIDSupported}, the * datastore supports using the provided feature id in the data insertion workflow as opposed to * generating a new id, by looking into the user data map ( {@link Feature#getUserData()}) for a * {@link Hints#USE_PROVIDED_FID} key associated to a {@link Boolean#TRUE} value, if the * key/value pair is there an attempt to use the provided id will be made, and the operation * will fail if the key cannot be parsed into a valid storage identifier. */ @Override protected QueryCapabilities buildQueryCapabilities() { return new QueryCapabilities() { /** * @return {@code true} */ @Override public boolean isUseProvidedFIDSupported() { return true; } /** * * @return {@code false} by now, will see how/whether we'll support * {@link Query#getVersion()} later */ @Override public boolean isVersionSupported() { return false; } }; } @Override protected ReferencedEnvelope getBoundsInternal(Query query) throws IOException { final Filter filter = (Filter) query.getFilter().accept(new SimplifyingFilterVisitor(), null); final CoordinateReferenceSystem crs = getSchema().getCoordinateReferenceSystem(); if (Filter.INCLUDE.equals(filter) && oldRoot == null && ChangeType.ADDED.equals(changeType())) { NodeRef typeRef = getTypeRef(); ReferencedEnvelope bounds = new ReferencedEnvelope(crs); typeRef.getNode().expand(bounds); return bounds; } if (Filter.EXCLUDE.equals(filter)) { return ReferencedEnvelope.create(crs); } FeatureReader<SimpleFeatureType, SimpleFeature> features; if (isNaturalOrder(query.getSortBy())) { Integer offset = query.getStartIndex(); Integer maxFeatures = query.getMaxFeatures() == Integer.MAX_VALUE ? null : query .getMaxFeatures(); ScreenMap screenMap = (ScreenMap) query.getHints().get(Hints.SCREENMAP); features = getNativeReader(Query.NO_NAMES, filter, offset, maxFeatures, screenMap); } else { features = getReader(query); } ReferencedEnvelope bounds = new ReferencedEnvelope(crs); try { while (features.hasNext()) { bounds.expandToInclude((ReferencedEnvelope) features.next().getBounds()); } } finally { features.close(); } return bounds; } @Override protected int getCountInternal(Query query) throws IOException { final Filter filter = (Filter) query.getFilter().accept(new SimplifyingFilterVisitor(), null); if (Filter.EXCLUDE.equals(filter)) { return 0; } final Integer offset = query.getStartIndex(); final Integer maxFeatures = query.getMaxFeatures() == Integer.MAX_VALUE ? null : query .getMaxFeatures(); int size; if (Filter.INCLUDE.equals(filter) && oldRoot == null && ChangeType.ADDED.equals(changeType())) { RevTree tree = getTypeTree(); size = (int) tree.size(); if (offset != null) { size = size - offset.intValue(); } if (maxFeatures != null) { size = Math.min(size, maxFeatures.intValue()); } return size; } FeatureReader<SimpleFeatureType, SimpleFeature> features; if (isNaturalOrder(query.getSortBy())) { ScreenMap screenMap = (ScreenMap) query.getHints().get(Hints.SCREENMAP); features = getNativeReader(Query.NO_NAMES, filter, offset, maxFeatures, screenMap); } else { features = getReader(query); } int count = 0; try { while (features.hasNext()) { features.next(); count++; } } finally { features.close(); } return count; } @Override protected FeatureReader<SimpleFeatureType, SimpleFeature> getReaderInternal(final Query query) throws IOException { FeatureReader<SimpleFeatureType, SimpleFeature> reader; final boolean naturalOrder = isNaturalOrder(query.getSortBy()); final int startIndex = Optional.fromNullable(query.getStartIndex()).or(Integer.valueOf(0)); final Integer maxFeatures = query.getMaxFeatures() == Integer.MAX_VALUE ? null : query .getMaxFeatures(); final Filter filter = query.getFilter(); final ScreenMap screenMap = (ScreenMap) query.getHints().get(Hints.SCREENMAP); final String[] propertyNames = query.getPropertyNames(); if (naturalOrder) { reader = getNativeReader(propertyNames, filter, startIndex, maxFeatures, screenMap); } else { reader = getNativeReader(propertyNames, filter, null, null, screenMap); // sorting reader = new SortedFeatureReader(DataUtilities.simple(reader), query); if (startIndex > 0) { // skip the first n records for (int i = 0; i < startIndex && reader.hasNext(); i++) { reader.next(); } } if (maxFeatures != null && maxFeatures > 0) { reader = new MaxFeatureReader<SimpleFeatureType, SimpleFeature>(reader, maxFeatures); } } return reader; } private boolean isNaturalOrder(@Nullable SortBy[] sortBy) { if (sortBy == null || sortBy.length == 0 || (sortBy.length == 1 && SortBy.NATURAL_ORDER.equals(sortBy[0]))) { return true; } return false; } /** * @param propertyNames properties to retrieve, empty array for no properties at all * {@link Query#NO_NAMES}, {@code null} means all properties {@link Query#ALL_NAMES} */ private FeatureReader<SimpleFeatureType, SimpleFeature> getNativeReader( @Nullable String[] propertyNames, Filter filter, @Nullable Integer offset, @Nullable Integer maxFeatures, @Nullable final ScreenMap screenMap) { if (screenMap == null) { LOGGER.trace("GeoGigFeatureSource.getNativeReader: no screenMap provided"); } else { LOGGER.trace("GeoGigFeatureSource.getNativeReader: using screenMap filter"); } LOGGER.trace("Query filter: {}", filter); filter = (Filter) filter.accept(new SimplifyingFilterVisitor(), null); LOGGER.trace("Simplified filter: {}", filter); GeogigFeatureReader<SimpleFeatureType, SimpleFeature> nativeReader; final String rootRef = getRootRef(); final String featureTypeTreePath = getTypeTreePath(); final SimpleFeatureType fullType = getSchema(); boolean ignoreAttributes = false; if (propertyNames != null && propertyNames.length == 0) { String[] inProcessFilteringAttributes = Filters.attributeNames(filter, fullType); ignoreAttributes = inProcessFilteringAttributes.length == 0; } final String compareRootRef = oldRoot(); final GeoGigDataStore.ChangeType changeType = changeType(); final Context context = getCommandLocator(); nativeReader = new GeogigFeatureReader<SimpleFeatureType, SimpleFeature>(context, fullType, filter, featureTypeTreePath, rootRef, compareRootRef, changeType, offset, maxFeatures, screenMap, ignoreAttributes); return nativeReader; } public void setChangeType(GeoGigDataStore.ChangeType changeType) { this.changeType = changeType; } public void setOldRoot(@Nullable String oldRoot) { this.oldRoot = oldRoot; } private String oldRoot() { return oldRoot == null ? ObjectId.NULL.toString() : oldRoot; } private GeoGigDataStore.ChangeType changeType() { return changeType == null ? ChangeType.ADDED : changeType; } @Override protected SimpleFeatureType buildFeatureType() throws IOException { SimpleFeatureType featureType = getNativeType(); final Name name = featureType.getName(); final Name assignedName = getEntry().getName(); if (assignedName.getNamespaceURI() != null && !assignedName.equals(name)) { SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder(); builder.init(featureType); builder.setName(assignedName); featureType = builder.buildFeatureType(); } return featureType; } Context getCommandLocator() { Context commandLocator = getDataStore().getCommandLocator(getTransaction()); return commandLocator; } SimpleFeatureType getNativeType() { final NodeRef typeRef = getTypeRef(); final String treePath = typeRef.path(); final ObjectId metadataId = typeRef.getMetadataId(); Context commandLocator = getCommandLocator(); Optional<RevFeatureType> revType = commandLocator.command(RevObjectParse.class) .setObjectId(metadataId).call(RevFeatureType.class); if (!revType.isPresent()) { throw new IllegalStateException(String.format("Feature type for tree %s not found", treePath)); } SimpleFeatureType featureType = (SimpleFeatureType) revType.get().type(); return featureType; } String getTypeTreePath() { NodeRef typeRef = getTypeRef(); String path = typeRef.path(); return path; } /** * @return */ NodeRef getTypeRef() { GeoGigDataStore dataStore = getDataStore(); Name name = getName(); Transaction transaction = getTransaction(); return dataStore.findTypeRef(name, transaction); } /** * @return */ RevTree getTypeTree() { String refSpec = getRootRef() + ":" + getTypeTreePath(); Context commandLocator = getCommandLocator(); Optional<RevTree> ref = commandLocator.command(RevObjectParse.class).setRefSpec(refSpec) .call(RevTree.class); Preconditions.checkState(ref.isPresent(), "Ref %s not found on working tree", refSpec); return ref.get(); } private String getRootRef() { GeoGigDataStore dataStore = getDataStore(); Transaction transaction = getTransaction(); return dataStore.getRootRef(transaction); } /** * @return */ WorkingTree getWorkingTree() { Context commandLocator = getCommandLocator(); WorkingTree workingTree = commandLocator.workingTree(); return workingTree; } }