/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wfs;
import static org.geoserver.ows.util.ResponseUtils.*;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import javax.xml.namespace.QName;
import net.opengis.wfs.XlinkPropertyNameType;
import net.opengis.wfs20.ResultTypeType;
import net.opengis.wfs20.StoredQueryType;
import org.geoserver.catalog.AttributeTypeInfo;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.ResourcePool;
import org.geoserver.feature.TypeNameExtractingVisitor;
import org.geoserver.ows.Dispatcher;
import org.geoserver.ows.Request;
import org.geoserver.ows.URLMangler.URLType;
import org.geoserver.ows.util.KvpMap;
import org.geoserver.wfs.request.FeatureCollectionResponse;
import org.geoserver.wfs.request.GetFeatureRequest;
import org.geoserver.wfs.request.Lock;
import org.geoserver.wfs.request.LockFeatureRequest;
import org.geoserver.wfs.request.LockFeatureResponse;
import org.geoserver.wfs.request.Query;
import org.geotools.data.DataUtilities;
import org.geotools.data.FeatureSource;
import org.geotools.data.Join;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.factory.Hints;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.NameImpl;
import org.geotools.feature.SchemaException;
import org.geotools.filter.FilterCapabilities;
import org.geotools.filter.expression.AbstractExpressionVisitor;
import org.geotools.filter.v2_0.FES;
import org.geotools.filter.v2_0.FESConfiguration;
import org.geotools.filter.visitor.AbstractFilterVisitor;
import org.geotools.filter.visitor.PostPreProcessFilterSplittingVisitor;
import org.geotools.filter.visitor.SimplifyingFilterVisitor;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.LiteCoordinateSequenceFactory;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.xml.Encoder;
import org.opengis.feature.Feature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.filter.And;
import org.opengis.filter.ExcludeFilter;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.Id;
import org.opengis.filter.IncludeFilter;
import org.opengis.filter.PropertyIsBetween;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.PropertyIsNull;
import org.opengis.filter.expression.ExpressionVisitor;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.spatial.BBOX;
import org.opengis.filter.spatial.Beyond;
import org.opengis.filter.spatial.BinarySpatialOperator;
import org.opengis.filter.spatial.Contains;
import org.opengis.filter.spatial.Crosses;
import org.opengis.filter.spatial.DWithin;
import org.opengis.filter.spatial.Disjoint;
import org.opengis.filter.spatial.Equals;
import org.opengis.filter.spatial.Intersects;
import org.opengis.filter.spatial.Overlaps;
import org.opengis.filter.spatial.Touches;
import org.opengis.filter.spatial.Within;
import org.opengis.filter.temporal.After;
import org.opengis.filter.temporal.Before;
import org.opengis.filter.temporal.Begins;
import org.opengis.filter.temporal.BegunBy;
import org.opengis.filter.temporal.During;
import org.opengis.filter.temporal.EndedBy;
import org.opengis.filter.temporal.Ends;
import org.opengis.filter.temporal.TContains;
import org.opengis.filter.temporal.TEquals;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.xml.sax.helpers.NamespaceSupport;
/**
* Web Feature Service GetFeature operation.
* <p>
* This operation returns an array of {@link org.geotools.feature.FeatureCollection}
* instances.
* </p>
*
* @author Rob Hranac, TOPP
* @author Justin Deoliveira, The Open Planning Project, jdeolive@openplans.org
*
* @version $Id$
*/
public class GetFeature {
/** Standard logging instance for class */
private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.vfny.geoserver.requests");
/**
* Describes the allowed filters we support for join queries.
*/
private final static FilterCapabilities joinFilterCapabilities;
static {
joinFilterCapabilities = new FilterCapabilities();
//simple comparisons
joinFilterCapabilities.addAll(FilterCapabilities.SIMPLE_COMPARISONS_OPENGIS);
//simple comparisons
joinFilterCapabilities.addType(PropertyIsNull.class);
joinFilterCapabilities.addType(PropertyIsBetween.class);
joinFilterCapabilities.addType(Id.class);
joinFilterCapabilities.addType(IncludeFilter.class);
joinFilterCapabilities.addType(ExcludeFilter.class);
joinFilterCapabilities.addType(PropertyIsLike.class);
//spatial
joinFilterCapabilities.addType(BBOX.class);
joinFilterCapabilities.addType(Contains.class);
joinFilterCapabilities.addType(Crosses.class);
joinFilterCapabilities.addType(Disjoint.class);
joinFilterCapabilities.addType(Equals.class);
joinFilterCapabilities.addType(Intersects.class);
joinFilterCapabilities.addType(Overlaps.class);
joinFilterCapabilities.addType(Touches.class);
joinFilterCapabilities.addType(Within.class);
joinFilterCapabilities.addType(DWithin.class);
joinFilterCapabilities.addType(Beyond.class);
//temporal
joinFilterCapabilities.addType(After.class);
joinFilterCapabilities.addType(Before.class);
joinFilterCapabilities.addType(Begins.class);
joinFilterCapabilities.addType(BegunBy.class);
joinFilterCapabilities.addType(During.class);
joinFilterCapabilities.addType(Ends.class);
joinFilterCapabilities.addType(EndedBy.class);
joinFilterCapabilities.addType(TContains.class);
joinFilterCapabilities.addType(TEquals.class);
//we only support simple filters, and any of them And'ed together.
joinFilterCapabilities.addType(And.class);
}
/** The catalog */
protected Catalog catalog;
/** The wfs configuration */
protected WFSInfo wfs;
/** filter factory */
protected FilterFactory2 filterFactory;
/** stored query provider */
StoredQueryProvider storedQueryProvider;
/**
* Creates the WFS 1.0/1.1 GetFeature operation.
*/
public GetFeature(WFSInfo wfs, Catalog catalog) {
this.wfs = wfs;
this.catalog = catalog;
}
/**
* @return The reference to the GeoServer catalog.
*/
public Catalog getCatalog() {
return catalog;
}
/**
* @return NamespaceSupport from Catalog
*/
public NamespaceSupport getNamespaceSupport() {
return new CatalogNamespaceSupport(catalog);
}
/**
* @return The reference to the WFS configuration.
*/
public WFSInfo getWFS() {
return wfs;
}
/**
* Sets the filter factory to use to create filters.
*
* @param filterFactory
*/
public void setFilterFactory(FilterFactory2 filterFactory) {
this.filterFactory = filterFactory;
}
/**
* Sets the stored query provider
*/
public void setStoredQueryProvider(StoredQueryProvider storedQueryProvider) {
this.storedQueryProvider = storedQueryProvider;
}
public FeatureCollectionResponse run(GetFeatureRequest request)
throws WFSException {
List<Query> queries = request.getQueries();
if (queries.isEmpty()) {
throw new WFSException(request, "No query specified");
}
//stored queries, preprocess compile any stored queries into actual query objects
processStoredQueries(request);
queries = request.getQueries();
if (request.isQueryTypeNamesUnset()) {
//do a check for FeatureId filters in the queries and update the type names for the
// queries accordingly
for (Query q : queries) {
if (!q.getTypeNames().isEmpty()) continue;
if (q.getFilter() != null) {
TypeNameExtractingVisitor v = new TypeNameExtractingVisitor(catalog);
q.getFilter().accept(v, null);
q.getTypeNames().addAll(v.getTypeNames());
}
if (q.getTypeNames().isEmpty()) {
String msg = "No feature types specified";
throw new WFSException(request, msg);
}
}
}
// Optimization Idea
//
// We should be able to reduce this to a two pass opperations.
//
// Pass #1 execute
// - Attempt to Locks Fids during the first pass
// - Also collect Bounds information during the first pass
//
// Pass #2 writeTo
// - Using the Bounds to describe our FeatureCollections
// - Iterate through FeatureResults producing GML
//
// And allways remember to release locks if we are failing:
// - if we fail to aquire all the locks we will need to fail and
// itterate through the the FeatureSources to release the locks
//
BigInteger bi = request.getMaxFeatures();
if (bi == null) {
request.setMaxFeatures(BigInteger.valueOf(Integer.MAX_VALUE));
}
// take into consideration the wfs max features
int maxFeatures = Math.min(request.getMaxFeatures().intValue(), wfs.getMaxFeatures());
// if this is only a HITS request AND the wfs setting flag
// hitsIgnoreMaxFeatures is set, then set the maxFeatures to be the
// maximum supported value from geotools. This is currently
// the maximum value of java.lang.Integer.MAX_VALUE so it is impossible
// to return more then this value even if there are matching values
// without either changing geotools to use a long or paging the results.
if (wfs.isHitsIgnoreMaxFeatures() && request.isResultTypeHits()) {
maxFeatures = org.geotools.data.Query.DEFAULT_MAX;
}
// grab the view params is any
List<Map<String, String>> viewParams = null;
if(request.getViewParams() != null && request.getViewParams().size() > 0) {
viewParams = request.getViewParams();
}
boolean isNumberMatchedSkipped = false;
int count = 0; // should probably be long
int totalCount = 0;
//offset into result set in which to return features
int totalOffset = request.getStartIndex() != null ? request.getStartIndex().intValue() : -1;
if (totalOffset == -1 && request.getVersion().startsWith("2") && wfs.isCiteCompliant()) {
// Strict compliance with the WFS 2.0 spec requires startindex to default to zero.
// This is not enforced because startindex triggers sorting and reduces performance.
// The CITE tests for WFS 2.0 do not yet exist; the CITE compliance setting is taken
// as a request for strict(er) compliance with the WFS 2.0 spec.
// See GEOS-5085.
totalOffset = 0;
}
int offset = totalOffset;
List results = new ArrayList();
List<CountExecutor> totalCountExecutors = new ArrayList<CountExecutor>();
try {
for (int i = 0; (i < queries.size()) && (count < maxFeatures); i++) {
Query query = queries.get(i);
try {
//alias sanity check
if (!query.getAliases().isEmpty()) {
if (query.getAliases().size() != query.getTypeNames().size()) {
throw new WFSException(request, String.format("Query specifies %d type names and %d " +
"aliases, must be equal", query.getTypeNames().size(), query.getAliases().size()));
}
}
List<FeatureTypeInfo> metas = new ArrayList();
for (QName typeName : query.getTypeNames()) {
metas.add(featureTypeInfo(typeName, request));
}
//first is the primary feature type
FeatureTypeInfo meta = metas.get(0);
// parse the requested property names and distribute among requested types
List<List<String>> reqPropertyNames = parsePropertyNames(query, metas);
NamespaceSupport ns = getNamespaceSupport();
List<List<PropertyName>> propNames = new ArrayList();
List<List<PropertyName>> allPropNames = new ArrayList();
for (int j = 0; j < metas.size(); j++) {
List<String> propertyNames = reqPropertyNames.get(j);
List<PropertyName> metaPropNames = null;
List<PropertyName> metaAllPropNames = null;
if (!propertyNames.isEmpty()){
metaPropNames = new ArrayList<PropertyName>();
for (Iterator iter = propertyNames.iterator(); iter.hasNext();) {
PropertyName propName = createPropertyName((String) iter.next(), ns);
if ( propName.evaluate(meta.getFeatureType()) == null) {
String mesg = "Requested property: " + propName + " is " + "not available "
+ "for " + meta.getPrefixedName() + ". ";
if (meta.getFeatureType() instanceof SimpleFeatureType) {
List<AttributeTypeInfo> atts = meta.attributes();
List attNames = new ArrayList( atts.size() );
for ( AttributeTypeInfo att : atts ) {
attNames.add( att.getName() );
}
mesg += "The possible propertyName values are: " + attNames;
}
throw new WFSException(request, mesg, "InvalidParameterValue");
}
metaPropNames.add(propName);
}
// if we need to force feature bounds computation, we have to load
// all of the geometries, but we'll have to remove them in the
// returned feature type
if(wfs.isFeatureBounding()) {
metaAllPropNames = addGeometryProperties(meta, metaPropNames);
} else {
metaAllPropNames = metaPropNames;
}
//we must also include any properties that are mandatory ( even if not requested ),
// ie. those with minOccurs > 0
//only do this for simple features, complex mandatory features are handled by app-schema
if (meta.getFeatureType() instanceof SimpleFeatureType) {
metaAllPropNames =
DataUtilities.addMandatoryProperties((SimpleFeatureType) meta.getFeatureType(), metaAllPropNames);
metaPropNames =
DataUtilities.addMandatoryProperties((SimpleFeatureType) meta.getFeatureType(), metaPropNames);
}
//for complex features, mandatory properties need to be handled by datastore.
}
allPropNames.add(metaAllPropNames);
propNames.add(metaPropNames);
}
//set up joins (if specified)
List<Join> joins = null;
String primaryAlias = null;
QName primaryTypeName = query.getTypeNames().get(0);
FeatureTypeInfo primaryMeta = metas.get(0);
//make sure filters are sane
//
// Validation of filters on non-simple feature types is not yet supported.
// FIXME: Support validation of filters on non-simple feature types:
// need to consider xpath properties and how to configure namespace prefixes in
// GeoTools app-schema FeaturePropertyAccessorFactory.
Filter filter = query.getFilter();
if (filter == null && metas.size() > 1) {
throw new WFSException(request, "Join query must specify a filter");
}
if (filter != null) {
if (meta.getFeatureType() instanceof SimpleFeatureType) {
if (metas.size() > 1) {
//ensure that the filter is allowable
if (!isValidJoinFilter(filter)) {
throw new WFSException(request,
"Unable to preform join with specified filter: " + filter);
}
// join, need to separate the joining filter from other filters
JoinExtractingVisitor extractor =
new JoinExtractingVisitor(metas, query.getAliases());
filter.accept(extractor, null);
primaryAlias = extractor.getPrimaryAlias();
primaryMeta = extractor.getPrimaryFeatureType();
primaryTypeName = new QName(primaryMeta.getNamespace().getURI(),
primaryMeta.getNativeName());
joins = extractor.getJoins();
if (joins.size() != metas.size()-1) {
throw new WFSException(request, String.format("Query specified %d types but %d " +
"join filters were found", metas.size(), extractor.getJoins().size()));
}
//validate the filter for each join
for (int j = 1; j < metas.size(); j++) {
Join join = joins.get(j-1);
if (join.getFilter() != null) {
validateFilter(join.getFilter(), query, metas.get(j), request);
}
}
filter = extractor.getPrimaryFilter();
if (filter != null) {
validateFilter(filter, query, meta, request);
}
}
else {
validateFilter(filter, query, meta, request);
}
} else {
BBOXNamespaceSettingVisitor filterVisitor = new BBOXNamespaceSettingVisitor(ns);
filter.accept(filterVisitor, null);
}
}
// validate sortby if present
List<SortBy> sortBy = query.getSortBy();
if (sortBy != null && !sortBy.isEmpty()
&& meta.getFeatureType() instanceof SimpleFeatureType) {
validateSortBy(sortBy, meta, request);
}
// load primary feature source
Hints hints = null;
if (joins != null) {
hints = new Hints(ResourcePool.JOINS, joins);
}
FeatureSource<? extends FeatureType, ? extends Feature> source =
primaryMeta.getFeatureSource(null, hints);
// handle local maximum
int queryMaxFeatures = maxFeatures - count;
int metaMaxFeatures = maxFeatures(metas);
if (metaMaxFeatures > 0 && metaMaxFeatures < queryMaxFeatures) {
queryMaxFeatures = metaMaxFeatures;
}
Map<String, String> viewParam = viewParams != null ? viewParams.get(i) : null;
org.geotools.data.Query gtQuery = toDataQuery(query, filter, offset,
queryMaxFeatures, source, request, allPropNames.get(0), viewParam,
joins, primaryTypeName, primaryAlias);
LOGGER.fine("Query is " + query + "\n To gt2: " + gtQuery);
FeatureCollection<? extends FeatureType, ? extends Feature> features = getFeatures(request, source, gtQuery);
// For complex features, we need the targetCrs and version in scenario where we have
// a top level feature that does not contain a geometry(therefore no crs) and has a
// nested feature that contains geometry as its property.Furthermore it is possible
// for each nested feature to have different crs hence we need to reproject on each
// feature accordingly.
if (!(meta.getFeatureType() instanceof SimpleFeatureType)) {
features.getSchema().getUserData().put("targetCrs", query.getSrsName());
features.getSchema().getUserData().put("targetVersion", request.getVersion());
}
//feature collection size, we may need to calculate it
boolean calculateSize = true;
// optimization: WFS 1.0 does not require count unless we have multiple query elements
// and we are asked to perform a global limit on the results returned
calculateSize = !(("1.0".equals(request.getVersion()) || "1.0.0".equals(request.getVersion())) &&
(queries.size() == 1 || maxFeatures == Integer.MAX_VALUE));
if (!calculateSize) {
//if offset was specified and we have more queries left in this request then we
// must calculate size in order to adjust the offset
calculateSize = offset > 0 && i < queries.size() - 1;
}
int size = 0;
if (calculateSize) {
size = features.size();
}
//update the count
count += size;
//if offset is present we need to check the size of this returned feature collection
// and adjust the offset for the next feature collection accordingly
if (offset > 0) {
if (size > 0) {
//features returned, offset can be set to zero
offset = 0;
}
else {
//no features might have been because of the offset that was specified, check
// the size of the same query but with no offset
org.geotools.data.Query q2 = toDataQuery(query, filter, 0,
queryMaxFeatures, source, request, allPropNames.get(0),
viewParam, joins, primaryTypeName, primaryAlias);
//int size2 = getFeatures(request, source, q2).size();
int size2 = source.getCount(q2);
if (size2 > 0) {
//adjust the offset for the next query
offset = Math.max(0, offset - size2);
}
}
}
// collect queries required to return numberMatched/totalSize
// check maxFeatures and offset, if they are unset we can use the size we
// calculated above
isNumberMatchedSkipped = meta.getSkipNumberMatched();
if (!isNumberMatchedSkipped) {
if (calculateSize && queryMaxFeatures == Integer.MAX_VALUE && offset == 0) {
totalCountExecutors.add(new CountExecutor(size));
} else {
org.geotools.data.Query qTotal = toDataQuery(query, filter, 0,
Integer.MAX_VALUE, source, request, allPropNames.get(0), viewParam,
joins, primaryTypeName, primaryAlias);
totalCountExecutors.add(new CountExecutor(source, qTotal));
}
}
// we may need to shave off geometries we did load only to make bounds
// computation happy
// TODO: support non-SimpleFeature geometry shaving
List<PropertyName> metaPropNames = propNames.get(0);
if(features.getSchema() instanceof SimpleFeatureType && metaPropNames!=null && metaPropNames.size() < allPropNames.get(0).size()) {
String[] residualNames = new String[metaPropNames.size()];
Iterator<PropertyName> it = metaPropNames.iterator();
int j =0;
while (it.hasNext()) {
residualNames[j] = it.next().getPropertyName();
j++;
}
SimpleFeatureType targetType = DataUtilities.createSubType((SimpleFeatureType) features.getSchema(), residualNames);
features = new FeatureBoundsFeatureCollection((SimpleFeatureCollection) features, targetType);
}
//JD: TODO reoptimize
// if ( i == request.getQuery().size() - 1 ) {
// //DJB: dont calculate feature count if you dont have to. The MaxFeatureReader will take care of the last iteration
// maxFeatures -= features.getCount();
// }
//GR: I don't know if the featuresults should be added here for later
//encoding if it was a lock request. may be after ensuring the lock
//succeed?
results.add(features);
}
catch(WFSException e) {
//intercept and set locator to query handle if one was set, or if it simply set
// to GetFeature, which is the default
if (query.getHandle() != null &&
(e.getLocator() == null || "GetFeature".equalsIgnoreCase(e.getLocator()))) {
e.setLocator(query.getHandle());
}
throw e;
}
}
// total count represents the total count of the features matched for this query in cases
// where the client has limited the result set size, as an optimization we only calculate
// this if the following conditions hold
// 1. the request is wfs 2.0
// 2. maxFeatures != Integer.MAX_VALUE
//TODO: we could actually add a third a optimization that when the count of features is
// less than maxFeatures we don't have to calculate it since it is the same as count, but
// this requires that we do that check post query loop which requires a bit of code
// refactoring
// we need the total count only for WFS 2.0
if (!request.getVersion().startsWith("2")) {
totalCount = -1;
} else {
if (isNumberMatchedSkipped) {
totalCount = -1;
totalOffset = 0;
} else {
// optimization: if count < max features then total count == count
if(count < maxFeatures) {
totalCount = count;
} else {
// ok, in this case we're forced to run the queries to discover the actual total count
for (CountExecutor q : totalCountExecutors) {
int result = q.getCount();
// if the count is unknown for one, we don't know the total, period
if(result == -1) {
totalCount = -1;
break;
} else {
totalCount += result;
}
}
}
}
}
} catch (IOException e) {
throw new WFSException(request, "Error occurred getting features", e, request.getHandle());
} catch (SchemaException e) {
throw new WFSException(request, "Error occurred getting features", e, request.getHandle());
}
//locking
String lockId = null;
if (request.isLockRequest()) {
LockFeatureRequest lockRequest = request.createLockRequest();
lockRequest.setExpiry(request.getExpiry());
lockRequest.setHandle(request.getHandle());
lockRequest.setLockActionAll();
for (int i = 0; i < queries.size(); i++) {
Query query = queries.get(i);
Lock lock = lockRequest.createLock();
lock.setFilter(query.getFilter());
lock.setHandle(query.getHandle());
//TODO: joins?
List<QName> typeNames = query.getTypeNames();
lock.setTypeName(typeNames.get(0));
lockRequest.addLock(lock);
}
LockFeature lockFeature = new LockFeature(wfs, catalog);
lockFeature.setFilterFactory(filterFactory);
LockFeatureResponse response = lockFeature.lockFeature(lockRequest);
lockId = response.getLockId();
}
return buildResults(request, totalOffset, maxFeatures, count, totalCount, results, lockId);
}
protected void processStoredQueries(GetFeatureRequest request) {
List queries = request.getAdaptedQueries();
for (int i = 0; i < queries.size(); i++) {
Object obj = queries.get(i);
if (obj instanceof StoredQueryType) {
if (storedQueryProvider == null) {
throw new WFSException(request, "Stored query not supported");
}
StoredQueryType sq = (StoredQueryType) obj;
//look up the store query
StoredQuery storedQuery = storedQueryProvider.getStoredQuery(sq.getId());
if (storedQuery == null) {
throw new WFSException(request, "Stored query '" + sq.getId() + "' does not exist.");
}
List<net.opengis.wfs20.QueryType> compiled = storedQuery.compile(sq);
queries.remove(i);
queries.addAll(i, compiled);
i += compiled.size();
}
}
}
/**
* Allows subclasses to alter the result generation
*/
protected FeatureCollectionResponse buildResults(GetFeatureRequest request, int offset, int maxFeatures,
int count, int total, List results, String lockId) {
FeatureCollectionResponse result = request.createResponse();
result.setNumberOfFeatures(BigInteger.valueOf(count));
result.setTotalNumberOfFeatures(BigInteger.valueOf(total));
result.setTimeStamp(Calendar.getInstance());
result.setLockId(lockId);
result.getFeature().addAll(results);
if (offset > 0 || count < Integer.MAX_VALUE) {
//paged request, set the values of previous and next
//get the Request thread local since we need to know about the request, whether it is
// GET or POST some kvp information if the former
Request req = Dispatcher.REQUEST.get();
//grab the original kvp params if this is a GET request
//for POST, do nothing, make the client post the same content
//TODO: try to encode the request as best we can in a GET request, only issue should
// be the filter and encoding it property... especially for joins that might be
// tricky, and it also may cause the request to be too large for a get request
//TODO: figure out what the spec says about this...
Map<String,String> kvp = null;
if (req.isGet()) {
kvp = new KvpMap(req.getRawKvp());
}
else {
//generate kvp map from request object
kvp = buildKvpFromRequest(request);
}
if (offset > 0) {
//previous
//previous offset calculated as the current offset - maxFeatures, or 0 if this is a
// negative value
int prevOffset = Math.max(offset - maxFeatures, 0);
kvp.put("startIndex", String.valueOf(prevOffset));
//previous count should be current offset - previousOffset
kvp.put("count", String.valueOf(offset - prevOffset));
result.setPrevious(buildURL(request.getBaseUrl(), "wfs", kvp, URLType.SERVICE));
}
if (count > 0 && offset > -1) {
//next
//calculate the count of the next result set
int nextCount = total - (offset + count);
if (nextCount > 0) {
kvp.put("startIndex", String.valueOf(offset > 0 ? offset + count : count));
//kvp.put("count", String.valueOf(nextCount));
kvp.put("count", String.valueOf(maxFeatures));
result.setNext(buildURL(request.getBaseUrl(), "wfs", kvp, URLType.SERVICE));
}
}
}
return result;
}
KvpMap buildKvpFromRequest(GetFeatureRequest request) {
// FILTER_LANGUAGE
// RESOURCEID
// BBOX
// STOREDQUERY_ID
KvpMap kvp = new KvpMap();
// SERVICE
// VERSION
// REQUEST
kvp.put("SERVICE", "WFS");
kvp.put("REQUEST", "GetFeature");
kvp.put("VERSION", request.getVersion());
// OUTPUTFORMAT
// RESULTTYPE
kvp.put("OUTPUTFORMAT", request.getOutputFormat());
kvp.put("RESULTTYPE", request.isResultTypeHits()
? ResultTypeType.HITS.name() : ResultTypeType.RESULTS.name());
// TYPENAMES
// PROPERTYNAME
// ALIASES
// SRSNAME
// FILTER
// SORTBY
List<Query> queries = request.getQueries();
Query q = queries.get(0);
if (q.getSrsName() != null) {
kvp.put("SRSNAME", q.getSrsName().toString());
}
StringBuilder typeNames = new StringBuilder();
StringBuilder propertyName = !q.getPropertyNames().isEmpty() ? new StringBuilder() : null;
StringBuilder aliases = !q.getAliases().isEmpty() ? new StringBuilder() : null;
StringBuilder filter = q.getFilter() != null && q.getFilter() != Filter.INCLUDE ?
new StringBuilder() : null;
encodeQueryAsKvp(q, typeNames, propertyName, aliases, filter, true);
if (queries.size() > 1) {
for (int i = 1; i < queries.size(); i++) {
encodeQueryAsKvp(queries.get(i), typeNames, propertyName, aliases, filter, true);
}
}
kvp.put("TYPENAMES", typeNames.toString());
if (propertyName != null) {
kvp.put("PROPERTYNAME", propertyName.toString());
}
if (aliases != null) {
kvp.put("ALIASES", aliases.toString());
}
if (filter != null) {
kvp.put("FILTER", filter.toString());
}
return kvp;
}
void encodeQueryAsKvp(Query q, StringBuilder typeNames, StringBuilder propertyName,
StringBuilder aliases, StringBuilder filter, boolean useDelim) {
//typenames
if (useDelim) {
typeNames.append("(");
}
for (QName qName : q.getTypeNames()) {
typeNames.append(qName.getPrefix()).append(":").append(qName.getLocalPart()).append(",");
}
typeNames.setLength(typeNames.length()-1);
if (useDelim) {
typeNames.append(")");
}
//propertynames
if (propertyName != null) {
if (useDelim) {
propertyName.append("(");
}
for (String pName : q.getPropertyNames()) {
propertyName.append(pName).append(",");
}
propertyName.setLength(propertyName.length()-1);
if (useDelim) {
propertyName.append(")");
}
}
//aliases
if (aliases != null) {
if (useDelim) {
aliases.append("(");
}
for (String alias : q.getAliases()) {
aliases.append(alias).append(",");
}
aliases.setLength(aliases.length()-1);
if (useDelim) {
aliases.append(")");
}
}
//filter
if (filter != null) {
//TODO: check the length of the encoded filter and ensure it does not put us over the
// edge of the limit for a GET request
Filter f = q.getFilter();
if (useDelim) {
filter.append("(");
}
try {
Encoder e = new Encoder(new FESConfiguration());
e.setOmitXMLDeclaration(true);
filter.append(e.encodeAsString(q.getFilter(), FES.Filter));
}
catch (Exception e) {
throw new RuntimeException("Unable to encode filter " + f, e);
}
if (useDelim) {
filter.append(")");
}
}
}
/**
* Allows subclasses to poke with the feature collection extraction
* @param source
* @param gtQuery
* @return
* @throws IOException
*/
protected FeatureCollection<? extends FeatureType, ? extends Feature> getFeatures(
Object request, FeatureSource<? extends FeatureType, ? extends Feature> source,
org.geotools.data.Query gtQuery)
throws IOException {
return source.getFeatures(gtQuery);
}
/**
* Get this query as a geotools Query.
*
* <p>
* if maxFeatures is a not positive value Query.DEFAULT_MAX will be
* used.
* </p>
*
* <p>
* The method name is changed to toDataQuery since this is a one way
* conversion.
* </p>
*
* @param maxFeatures number of features, or 0 for Query.DEFAULT_MAX
*
* @return A Query for use with the FeatureSource interface
*
*/
public org.geotools.data.Query toDataQuery(Query query, Filter filter, int offset,
int maxFeatures, FeatureSource<? extends FeatureType, ? extends Feature> source,
GetFeatureRequest request, List<PropertyName> props, Map<String, String> viewParams,
List<Join> joins, QName primaryTypeName, String primaryAlias) throws WFSException {
String wfsVersion = request.getVersion();
if (maxFeatures <= 0) {
maxFeatures = org.geotools.data.Query.DEFAULT_MAX;
}
if (filter == null) {
filter = Filter.INCLUDE;
} else {
// Gentlemen, we can rebuild it. We have the technology!
SimplifyingFilterVisitor visitor = new SimplifyingFilterVisitor();
filter = (Filter) filter.accept(visitor, null);
}
//figure out the crs the data is in
CoordinateReferenceSystem crs = source.getSchema().getCoordinateReferenceSystem();
// gather declared CRS
CoordinateReferenceSystem declaredCRS = WFSReprojectionUtil.getDeclaredCrs(crs, wfsVersion);
// make sure every bbox and geometry that does not have an attached crs will use
// the declared crs, and then reproject it to the native crs
Filter transformedFilter = filter;
if(declaredCRS != null)
transformedFilter = WFSReprojectionUtil.normalizeFilterCRS(filter, source.getSchema(), declaredCRS);
//only handle non-joins for now
QName typeName = primaryTypeName;
org.geotools.data.Query dataQuery = new org.geotools.data.Query(typeName.getLocalPart(),
transformedFilter, maxFeatures, props, query.getHandle());
if (primaryAlias != null) {
dataQuery.setAlias(primaryAlias);
}
//handle reprojection
CoordinateReferenceSystem target;
URI srsName = query.getSrsName();
if (srsName != null) {
try {
target = CRS.decode(srsName.toString());
} catch (Exception e) {
String msg = "Unable to support srsName: " + srsName;
throw new WFSException(request, msg, e, "InvalidParameterValue").locator("srsName");
}
} else {
target = declaredCRS;
}
//if the crs are not equal, then reproject
if (target != null && declaredCRS != null && !CRS.equalsIgnoreMetadata(crs, target)) {
dataQuery.setCoordinateSystemReproject(target);
}
//handle sorting
List<SortBy> sortBy = query.getSortBy();
if (sortBy != null) {
dataQuery.setSortBy(sortBy.toArray(new SortBy[sortBy.size()]));
}
//handle version, datastore may be able to use it
String featureVersion = query.getFeatureVersion();
if (featureVersion != null) {
dataQuery.setVersion(featureVersion);
}
//handle offset / start index
if (offset > -1) {
dataQuery.setStartIndex(offset);
}
//create the Hints to set at the end
final Hints hints = new Hints();
//handle xlink traversal depth
String traverseXlinkDepth = request.getTraverseXlinkDepth();
if (traverseXlinkDepth != null) {
//TODO: make this an integer in the model, and have hte NumericKvpParser
// handle '*' as max value
Integer depth = traverseXlinkDepth( traverseXlinkDepth );
//set the depth as a hint on the query
hints.put(Hints.ASSOCIATION_TRAVERSAL_DEPTH, depth);
}
//handle resolve parameters
hints.put(Hints.RESOLVE, request.getResolve());
BigInteger resolveTimeOut = request.getResolveTimeOut();
if (resolveTimeOut != null) {
hints.put(Hints.RESOLVE_TIMEOUT, resolveTimeOut.intValue());
}
//handle xlink properties
List<XlinkPropertyNameType> xlinkProperties = query.getXlinkPropertyNames();
if (!xlinkProperties.isEmpty() ) {
for ( Iterator x = xlinkProperties.iterator(); x.hasNext(); ) {
XlinkPropertyNameType xlinkProperty = (XlinkPropertyNameType) x.next();
Integer xlinkDepth = traverseXlinkDepth( xlinkProperty.getTraverseXlinkDepth() );
//set the depth and property as hints on the query
hints.put(Hints.ASSOCIATION_TRAVERSAL_DEPTH, xlinkDepth );
PropertyName xlinkPropertyName = filterFactory.property( xlinkProperty.getValue() );
hints.put(Hints.ASSOCIATION_PROPERTY, xlinkPropertyName );
dataQuery.setHints( hints );
//TODO: support multiple properties
break;
}
}
//tell the datastore to use a lite coordinate sequence factory, if possible
hints.put(Hints.JTS_COORDINATE_SEQUENCE_FACTORY, new LiteCoordinateSequenceFactory());
// check for sql view parameters
if(viewParams != null) {
hints.put(Hints.VIRTUAL_TABLE_PARAMETERS, viewParams);
}
//currently only used by app-schema, produce mandatory properties
hints.put(org.geotools.data.Query.INCLUDE_MANDATORY_PROPS, true);
//add the joins, if specified
if (joins != null) {
dataQuery.getJoins().addAll(joins);
}
//finally, set the hints
dataQuery.setHints(hints);
return dataQuery;
}
static Integer traverseXlinkDepth( String raw ) {
Integer traverseXlinkDepth = null;
try {
traverseXlinkDepth = new Integer( raw );
}
catch( NumberFormatException nfe ) {
//try handling *
if ( "*".equals( raw ) ) {
//TODO: JD: not sure what this value should be? i think it
// might be reported in teh acapabilitis document, using
// INteger.MAX_VALUE will result in stack overflow... for now
// we just use 10
traverseXlinkDepth = new Integer( 2 );
}
else {
//not wildcard case, throw original exception
throw nfe;
}
}
return traverseXlinkDepth;
}
boolean isValidJoinFilter(Filter filter) {
PostPreProcessFilterSplittingVisitor visitor =
new PostPreProcessFilterSplittingVisitor(joinFilterCapabilities, null, null);
filter.accept(visitor, null);
return visitor.getFilterPost() == null || visitor.getFilterPost() == Filter.INCLUDE;
}
FeatureTypeInfo featureTypeInfo(QName name, GetFeatureRequest request) throws WFSException, IOException {
FeatureTypeInfo meta = catalog.getFeatureTypeByName(name.getNamespaceURI(), name.getLocalPart());
if (meta == null) {
String msg = "Could not locate " + name + " in catalog.";
throw new WFSException(request, msg, "InvalidParameterValue").locator("typeName");
}
return meta;
}
List<List<String>> parsePropertyNames(Query query, List<FeatureTypeInfo> featureTypes) {
List<List<String>> propNames = new ArrayList();
for (FeatureTypeInfo featureType: featureTypes) {
propNames.add(new ArrayList());
}
if (featureTypes.size() == 1) {
//non join
propNames.get(0).addAll(query.getPropertyNames());
return propNames;
}
//go through all property names and distribute based on prefix accordingly
O: for (String propName : query.getPropertyNames()) {
//check for a full typename prefix
for (int j = 0; j < featureTypes.size(); j++) {
FeatureTypeInfo featureType = featureTypes.get(j);
if (propName.startsWith(featureType.getPrefixedName()+"/")) {
propNames.get(j).add(propName.substring((featureType.getPrefixedName()+"/").length()));
continue O;
}
if (propName.startsWith(featureType.getName()+"/")) {
propNames.get(j).add(propName.substring((featureType.getName()+"/").length()));
continue O;
}
}
if (query.getAliases().isEmpty()) {
//check for aliases
for (int j = 0; j < query.getAliases().size(); j++) {
String alias = query.getAliases().get(j);
if (propName.startsWith(alias+"/")) {
propNames.get(j).add(propName.substring((alias+"/").length()));
continue O;
}
}
}
//fallback on first
propNames.get(0).add(propName);
}
return propNames;
}
void validateSortBy(List<SortBy> sortBys, FeatureTypeInfo meta, final GetFeatureRequest request)
throws IOException {
FeatureType featureType = meta.getFeatureType();
for (SortBy sortBy : sortBys) {
PropertyName name = sortBy.getPropertyName();
if (name.evaluate(featureType) == null) {
throw new WFSException(request, "Illegal property name: " + name.getPropertyName()
+ " for feature type " + meta.prefixedName(), "InvalidParameterValue");
}
}
}
void validateFilter(Filter filter, Query query, final FeatureTypeInfo meta,
final GetFeatureRequest request)
throws IOException {
//1. ensure any property name refers to a property that
// actually exists
final FeatureType featureType = meta.getFeatureType();
ExpressionVisitor visitor = new AbstractExpressionVisitor() {
public Object visit(PropertyName name, Object data) {
// case of multiple geometries being returned
if (name.evaluate(featureType) == null) {
throw new WFSException(request, "Illegal property name: "
+ name.getPropertyName() + " for feature type " + meta.prefixedName(),
"InvalidParameterValue");
}
return name;
}
;
};
filter.accept(new AbstractFilterVisitor(visitor), null);
//2. ensure any spatial predicate is made against a property
// that is actually spatial
AbstractFilterVisitor fvisitor = new AbstractFilterVisitor() {
protected Object visit( BinarySpatialOperator filter, Object data ) {
PropertyName name = null;
if ( filter.getExpression1() instanceof PropertyName ) {
name = (PropertyName) filter.getExpression1();
}
else if ( filter.getExpression2() instanceof PropertyName ) {
name = (PropertyName) filter.getExpression2();
}
if ( name != null ) {
// check against feataure type to make sure its
// a geometric type
AttributeDescriptor att = (AttributeDescriptor) name.evaluate(featureType);
if ( !( att instanceof GeometryDescriptor ) ) {
throw new WFSException(request, "Property " + name
+ " is not geometric in feature type " + meta.prefixedName(),
"InvalidParameterValue");
}
}
return filter;
}
};
filter.accept(fvisitor, null);
//3. ensure that any bounds specified as part of the query
// are valid with respect to the srs defined on the query
if ( wfs.isCiteCompliant() ) {
if ( query.getSrsName() != null ) {
final Query fquery = query;
fvisitor = new AbstractFilterVisitor() {
public Object visit(BBOX filter, Object data) {
if ( filter.getSRS() != null &&
!fquery.getSrsName().toString().equals( filter.getSRS() ) ) {
//back project bounding box into geographic coordinates
CoordinateReferenceSystem geo = DefaultGeographicCRS.WGS84;
GeneralEnvelope e = new GeneralEnvelope(
new double[] { filter.getMinX(), filter.getMinY()},
new double[] { filter.getMaxX(), filter.getMaxY()}
);
CoordinateReferenceSystem crs = null;
try {
crs = CRS.decode( filter.getSRS() );
e = CRS.transform(e, geo);
}
catch( Exception ex ) {
throw new WFSException( request, ex );
}
//ensure within bounds defined by srs specified on
// query
try {
crs = CRS.decode( fquery.getSrsName().toString() );
}
catch( Exception ex ) {
throw new WFSException( request, ex );
}
GeographicBoundingBox valid =
(GeographicBoundingBox) crs.getDomainOfValidity()
.getGeographicElements().iterator().next();
if ( e.getMinimum(0) < valid.getWestBoundLongitude() ||
e.getMinimum(0) > valid.getEastBoundLongitude() ||
e.getMaximum(0) < valid.getWestBoundLongitude() ||
e.getMaximum(0) > valid.getEastBoundLongitude() ||
e.getMinimum(1) < valid.getSouthBoundLatitude() ||
e.getMinimum(1) > valid.getNorthBoundLatitude() ||
e.getMaximum(1) < valid.getSouthBoundLatitude() ||
e.getMaximum(1) > valid.getNorthBoundLatitude() ) {
throw new WFSException(request, "bounding box out of valid range of crs", "InvalidParameterValue");
}
}
return data;
}
};
filter.accept(fvisitor, null);
}
}
}
int maxFeatures(List<FeatureTypeInfo> metas) {
int maxFeatures = Integer.MAX_VALUE;
for (FeatureTypeInfo meta : metas) {
if (meta.getMaxFeatures() > 0) {
maxFeatures = Math.min(maxFeatures, meta.getMaxFeatures());
}
}
return maxFeatures;
}
protected PropertyName createPropertyName (String path, NamespaceSupport namespaceContext) {
if (path.contains("/")) {
return filterFactory.property(path, namespaceContext);
} else {
if (path.contains(":")) {
int i = path.indexOf(":");
return filterFactory.property(new NameImpl(namespaceContext.getURI(path.substring(0, i)), path.substring(i+1) ));
} else {
return filterFactory.property(path);
}
}
}
protected List<PropertyName> addGeometryProperties (FeatureTypeInfo meta, List<PropertyName> oldProperties) throws IOException {
List<AttributeTypeInfo> atts = meta.attributes();
Iterator ii = atts.iterator();
List<PropertyName> properties = new ArrayList<PropertyName>(oldProperties);
while (ii.hasNext()) {
AttributeTypeInfo ati = (AttributeTypeInfo) ii.next();
PropertyName propName = filterFactory.property (ati.getName());
if(meta.getFeatureType().getDescriptor(ati.getName()) instanceof GeometryDescriptor
&& !properties.contains(propName) ) {
properties.add(propName);
}
}
return properties;
}
}