/* Copyright (c) 2001 - 2009 TOPP - www.openplans.org. All rights reserved.
* This code is licensed under the GPL 2.0 license, availible at the root
* application directory.
*/
package org.geoserver.sfs;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import net.sf.json.JSONArray;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.catalog.LayerInfo;
import org.geoserver.rest.RestletException;
import org.geoserver.rest.util.RESTUtils;
import org.geotools.data.Query;
import org.geotools.data.QueryCapabilities;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.filter.spatial.DefaultCRSFilterVisitor;
import org.geotools.filter.visitor.SimplifyingFilterVisitor;
import org.geotools.geojson.geom.GeometryJSON;
import org.geotools.referencing.CRS;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.Id;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.restlet.Finder;
import org.restlet.data.Form;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.data.Status;
import org.restlet.resource.Resource;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;
/**
* Looks up the describe object
*
* @author Andrea Aime - GeoSolutions
*/
public class FeatureCollectionFinder extends Finder {
static final FilterFactory2 FF = CommonFactoryFinder.getFilterFactory2(null);
Catalog catalog;
public FeatureCollectionFinder(Catalog catalog) {
this.catalog = catalog;
}
@Override
public Resource findTarget(Request request, Response response) {
String layerName = RESTUtils.getAttribute(request, "layer");
LayerInfo layer = catalog.getLayerByName(layerName);
// any of these conditions mean the layer is not currently
// advertised in the capabilities document
if (layer == null || !layer.isEnabled()
|| !(layer.getResource() instanceof FeatureTypeInfo)) {
throw new RestletException("No such layer: " + layerName, Status.CLIENT_ERROR_NOT_FOUND);
}
// build the feature collection and wrap it in a resource
final FeatureTypeInfo resource = (FeatureTypeInfo) layer.getResource();
try {
SimpleFeatureSource featureSource = (SimpleFeatureSource) resource.getFeatureSource(
null, null);
Query query = buildQuery(request, featureSource.getSchema());
// couple sanity checks
final QueryCapabilities queryCapabilities = featureSource.getQueryCapabilities();
if(!queryCapabilities.isOffsetSupported() && (query.getStartIndex() != null && query.getStartIndex() > 1)) {
throw new RestletException("Offset is not supported on this data source", Status.SERVER_ERROR_INTERNAL);
}
SimpleFeatureCollection features = featureSource.getFeatures(query);
Form form = request.getResourceRef().getQueryAsForm();
String mode = form.getFirstValue("mode");
if (mode == null || "features".equals(mode)) {
return new FeatureCollectionResource(getContext(), request, response, features);
} else if ("bounds".equals(mode)) {
return new BoundsResource(getContext(), request, response, features.getBounds());
} else if ("count".equals(mode)) {
return new CountResource(getContext(), request, response, features.size());
} else {
throw new RestletException("Uknown mode '" + mode + "'",
Status.SERVER_ERROR_INTERNAL);
}
} catch (IOException e) {
throw new RestletException("Internal error occurred while "
+ "retrieving the features to be returned", Status.SERVER_ERROR_INTERNAL, e);
}
}
/**
* Build a query based on the
*
* @param request
* @return
*/
private Query buildQuery(Request request, SimpleFeatureType schema) {
// get the query string params as a form
Form form = request.getResourceRef().getQueryAsForm();
Query query = new Query();
applyFilter(request, schema, form, query);
applyAttributeSelection(schema, form, query);
// the following apply only in feature collection mode
String fid = RESTUtils.getAttribute(request, "fid");
if (fid == null) {
applyMaxFeatures(form, query);
applyOffset(form, query);
applyOrderBy(schema, form, query);
}
return query;
}
private void applyAttributeSelection(SimpleFeatureType schema, Form form, Query query) {
Set<String> attributes = Collections.emptySet();
String attrs = form.getFirstValue("attrs");
if (attrs != null) {
String[] parsedAttributes = attrs.split("\\s*,\\s*");
attributes = new HashSet<String>(Arrays.asList(parsedAttributes));
}
// build the output property list, if any
List<String> properties = new ArrayList<String>();
boolean skipGeom = "true".equals(form.getFirstValue("no_geom"));
boolean filterAttributes = attributes.size() > 0;
if (skipGeom || attributes.size() > 0) {
SimpleFeatureTypeBuilder tb = new SimpleFeatureTypeBuilder();
tb.setName(schema.getName());
for (AttributeDescriptor attribute : schema.getAttributeDescriptors()) {
// skip geometric attributes if so requested
if (attribute instanceof GeometryDescriptor && skipGeom) {
continue;
}
// skip unselected attributes (but keep the geometry, that has to be excluded explicitly using nogeom)
final String name = attribute.getLocalName();
if (filterAttributes && !attributes.contains(name)
&& !attribute.equals(schema.getGeometryDescriptor())) {
continue;
}
properties.add(name);
attributes.remove(name);
}
if (properties.size() > 0) {
query.setPropertyNames(properties);
}
// check if we have residual, unknown attributes
if (attributes.size() > 0) {
throw new RestletException(
"The following attributes are not known to this service: " + attributes,
Status.CLIENT_ERROR_BAD_REQUEST);
}
}
}
private void applyOrderBy(SimpleFeatureType schema, Form form, Query query) {
String orderBy = form.getFirstValue("order_by");
if (orderBy != null) {
String[] orderByAtts = orderBy.split("\\s*,\\s*");
String dir = form.getFirstValue("dir");
String[] directions = null;
if (dir != null) {
directions = dir.split("\\s*,\\s*");
}
// check directions and attributes are matched
if (directions != null && directions.length != orderByAtts.length) {
if (directions.length < orderByAtts.length) {
throw new RestletException("dir list has less entries than order_by",
Status.CLIENT_ERROR_BAD_REQUEST);
} else {
throw new RestletException("dir list has more entries than order_by",
Status.CLIENT_ERROR_BAD_REQUEST);
}
}
SortBy[] sortBy = new SortBy[orderByAtts.length];
for (int i = 0; i < orderByAtts.length; i++) {
String name = orderByAtts[i];
SortOrder order = getSortOrder(directions, i);
AttributeDescriptor descriptor = schema.getDescriptor(name);
if (descriptor == null) {
throw new RestletException("Uknown order_by attribute " + name,
Status.CLIENT_ERROR_BAD_REQUEST);
} else if (descriptor instanceof GeometryDescriptor) {
throw new RestletException("order_by attribute " + name + " is a geometry, "
+ "cannot sort on it", Status.CLIENT_ERROR_BAD_REQUEST);
}
sortBy[i] = FF.sort(name, order);
}
query.setSortBy(sortBy);
}
}
private void applyOffset(Form form, Query query) {
String offset = form.getFirstValue("offset");
if (offset != null) {
try {
query.setStartIndex(Integer.parseInt(offset));
} catch (NumberFormatException e) {
throw new RestletException("Invalid offset expression: " + offset,
Status.CLIENT_ERROR_BAD_REQUEST);
}
}
}
private void applyMaxFeatures(Form form, Query query) {
String limit = form.getFirstValue("limit");
if (limit == null) {
limit = form.getFirstValue("maxfeatures");
}
if (limit != null) {
try {
query.setMaxFeatures(Integer.parseInt(limit));
} catch (NumberFormatException e) {
throw new RestletException("Invalid limit expression: " + limit,
Status.CLIENT_ERROR_BAD_REQUEST);
}
}
}
private void applyFilter(Request request, SimpleFeatureType schema, Form form, Query query) {
String fid = RESTUtils.getAttribute(request, "fid");
if (fid != null) {
final Id fidFilter = FF.id(Collections.singleton(FF.featureId(fid)));
query.setFilter(fidFilter);
} else {
List<Filter> filters = new ArrayList<Filter>();
// build the geometry filters
filters.add(buildGeometryFilter(schema, form));
filters.add(buildBBoxFilter(schema, form));
filters.add(buildXYToleranceFilter(schema, form));
// see if we have any non geometric one
String queryable = form.getFirstValue("queryable");
if (queryable != null) {
String[] attributes = queryable.split("\\s*,\\s*");
for (String name : attributes) {
AttributeDescriptor ad = schema.getDescriptor(name);
if (ad == null) {
throw new RestletException("Uknown queryable attribute " + name,
Status.CLIENT_ERROR_BAD_REQUEST);
} else if (ad instanceof GeometryDescriptor) {
throw new RestletException("queryable attribute " + name
+ " is a geometry, " + "cannot perform non spatial filters on it",
Status.CLIENT_ERROR_BAD_REQUEST);
}
final PropertyName property = FF.property(name);
final String prefix = name + "__";
for (String paramName : form.getNames()) {
if (paramName.startsWith(prefix)) {
Literal value = FF.literal(form.getFirstValue(paramName));
String op = paramName.substring(prefix.length());
if ("eq".equals(op)) {
filters.add(FF.equals(property, value));
} else if ("ne".equals(op)) {
filters.add(FF.notEqual(property, value));
} else if ("lt".equals(op)) {
filters.add(FF.less(property, value));
} else if ("lte".equals(op)) {
filters.add(FF.lessOrEqual(property, value));
} else if ("ge".equals(op)) {
filters.add(FF.greater(property, value));
} else if ("gte".equals(op)) {
filters.add(FF.greaterOrEqual(property, value));
} else if ("like".equals(op)) {
String pattern = form.getFirstValue(paramName);
filters.add(FF.like(property, pattern, "%", "_", "\\", true));
} else if ("ilike".equals(op)) {
String pattern = form.getFirstValue(paramName);
filters.add(FF.like(property, pattern, "%", "_", "\\", false));
} else {
throw new RestletException("Uknown query operand '" + op + "'",
Status.CLIENT_ERROR_BAD_REQUEST);
}
}
}
}
}
if (filters.size() > 0) {
// summarize all the filters
Filter result = FF.and(filters);
SimplifyingFilterVisitor simplifier = new SimplifyingFilterVisitor();
result = (Filter) result.accept(simplifier, null);
// if necessary, reproject the filters
String crs = form.getFirstValue("crs");
if (crs == null) {
crs = form.getFirstValue("epsg");
}
if (crs != null) {
try {
// apply the default srs into the spatial filters
CoordinateReferenceSystem sourceCrs = CRS.decode("EPSG:" + crs, true);
DefaultCRSFilterVisitor crsForcer = new DefaultCRSFilterVisitor(FF,
sourceCrs);
result = (Filter) result.accept(crsForcer, null);
} catch (Exception e) {
throw new RestletException("Uknown EPSG code '" + crs + "'",
Status.CLIENT_ERROR_BAD_REQUEST);
}
}
query.setFilter(result);
}
}
}
private double getTolerance(Form form) {
String tolerance = form.getFirstValue("tolerance");
if(tolerance != null) {
double tolValue = parseDouble(tolerance, "tolerance");
if(tolValue < 0) {
throw new RestletException("Invalid tolerance, it should be zero or positive: " + tolValue ,
Status.CLIENT_ERROR_BAD_REQUEST);
}
return tolValue;
} else {
return 0d;
}
}
private Filter buildXYToleranceFilter(SimpleFeatureType schema, Form form) {
String x = form.getFirstValue("lon");
String y = form.getFirstValue("lat");
if (x == null && y == null) {
return Filter.INCLUDE;
}
if (x == null || y == null) {
throw new RestletException(
"Incomplete x/y specification, must provide both values",
Status.CLIENT_ERROR_BAD_REQUEST);
}
double ordx = parseDouble(x, "x");
double ordy = parseDouble(y, "y");
final Point centerPoint = new GeometryFactory().createPoint(new Coordinate(ordx, ordy));
final double tolerance = getTolerance(form);
return geometryFilter(schema, centerPoint, tolerance);
}
private Filter geometryFilter(SimpleFeatureType schema, Geometry geometry, double tolerance) {
PropertyName defaultGeometry = FF.property(schema.getGeometryDescriptor().getLocalName());
Literal center = FF.literal(geometry);
if(tolerance == 0) {
return FF.intersects(defaultGeometry, center);
} else {
return FF.dwithin(defaultGeometry, center, tolerance, null);
}
}
double parseDouble(String value, String name) {
try {
return Double.parseDouble(value);
} catch (NumberFormatException e) {
throw new RestletException("Expected a number for " + name + " but got " + value,
Status.CLIENT_ERROR_BAD_REQUEST);
}
}
private Filter buildBBoxFilter(SimpleFeatureType schema, Form form) {
String bbox = form.getFirstValue("bbox");
if (bbox == null) {
return Filter.INCLUDE;
} else {
try {
JSONArray ordinates = JSONArray.fromObject("[" + bbox + "]");
String defaultGeomName = schema.getGeometryDescriptor().getLocalName();
final double tolerance = getTolerance(form);
double minx = ordinates.getDouble(0);
double miny = ordinates.getDouble(1);
double maxx = ordinates.getDouble(2);
double maxy = ordinates.getDouble(3);
if(tolerance > 0) {
minx -= tolerance;
miny -= tolerance;
maxx += tolerance;
maxy += tolerance;
}
return FF.bbox(defaultGeomName, minx, miny, maxx, maxy, null);
} catch (Exception e) {
throw new RestletException("Could not parse the bbox: " + e.getMessage(),
Status.CLIENT_ERROR_BAD_REQUEST);
}
}
}
private Filter buildGeometryFilter(SimpleFeatureType schema, Form form) {
String geometry = form.getFirstValue("geometry");
if (geometry == null) {
return Filter.INCLUDE;
} else {
try {
Geometry geom = new GeometryJSON().read(geometry);
final double tolerance = getTolerance(form);
return geometryFilter(schema, geom, tolerance);
} catch (IOException e) {
throw new RestletException("Could not parse the geometry geojson: "
+ e.getMessage(), Status.CLIENT_ERROR_BAD_REQUEST);
}
}
}
SortOrder getSortOrder(String[] orders, int idx) {
if (orders == null) {
return SortOrder.ASCENDING;
}
String order = orders[idx];
if (order == null) {
return SortOrder.ASCENDING;
} else if ("DESC".equals(order)) {
return SortOrder.DESCENDING;
} else if ("ASC".equals(order)) {
return SortOrder.ASCENDING;
} else {
throw new RestletException("Unknown ordering direction: " + order,
Status.CLIENT_ERROR_BAD_REQUEST);
}
}
}