/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2011, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.data.sfs;
import java.io.IOException;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.data.FeatureReader;
import org.geotools.data.FilteringFeatureReader;
import org.geotools.data.Query;
import org.geotools.data.QueryCapabilities;
import org.geotools.data.ReTypeFeatureReader;
import org.geotools.data.simple.SimpleFeatureSource;
import org.geotools.data.store.ContentEntry;
import org.geotools.data.store.ContentFeatureSource;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.filter.FilterAttributeExtractor;
import org.geotools.filter.visitor.PostPreProcessFilterSplittingVisitor;
import org.geotools.filter.visitor.SimplifyingFilterVisitor;
import org.geotools.geojson.feature.FeatureJSON;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.json.simple.JSONArray;
import org.json.simple.parser.JSONParser;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.Filter;
class SFSFeatureSource extends ContentFeatureSource implements SimpleFeatureSource {
static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.data.simplefeatureservice");
Map<String, String> hint = Collections.emptyMap();
ContentEntry contentEntry;
SFSDataStore ods;
SFSLayer layer;
/**
* Constructor for OpenDataStoreFeatureSource
* @param fnEntry
* @param fnQuery
* @throws IOException
*/
public SFSFeatureSource(ContentEntry fnEntry) throws IOException {
super(fnEntry, Query.ALL);
this.contentEntry = fnEntry;
ods = (SFSDataStore) contentEntry.getDataStore();
this.layer = ods.getLayer(contentEntry.getName());
}
/**
* This method get the Bound of the Layer -- using /mode=bounds
* portions of code has been borrowed from
* https://svn.osgeo.org/geotools/trunk/modules/library/jdbc/src/main/java/org/geotools/jdbc/JDBCFeatureSource.java
* @param fnQuery
* @return bounds in terms of reference Envelope
* @throws IOException
*/
@Override
public ReferencedEnvelope getBoundsInternal(Query fnQuery) throws IOException {
ReferencedEnvelope env = new ReferencedEnvelope(layer.getCoordinateReferenceSystem());
/* Spilt the filter into two parts pre and post */
Filter[] split = splitFilter(fnQuery.getFilter());
Filter preFilter = split[0];
Filter postFilter = split[1];
if ((postFilter != null) && (postFilter != Filter.INCLUDE) || preFilter == Filter.EXCLUDE) {
return null;
} else {
Query tmpPreQ = new Query(fnQuery);
tmpPreQ.setFilter(preFilter);
String strQuery = SFSDataStoreUtil.encodeQuery(tmpPreQ, getSchema());
/* Create the URL */
String bboxString = ods.resourceToString("data/" + layer.getTypeName().getLocalPart(), "mode=bounds&" + strQuery);
JSONArray bbox;
try {
bbox = (JSONArray) new JSONParser().parse(bboxString);
} catch(org.json.simple.parser.ParseException e) {
throw (IOException) new IOException("Failed to parse the bbox JSON array:" + bboxString).initCause(e);
}
if (bbox.size() == 4) {
if (layer.isXYOrder()) {
env.init(((Number) bbox.get(0)).doubleValue(),
((Number) bbox.get(2)).doubleValue(),
((Number) bbox.get(1)).doubleValue(),
((Number) bbox.get(3)).doubleValue());
} else {
env.init(((Number) bbox.get(1)).doubleValue(),
((Number) bbox.get(3)).doubleValue(),
((Number) bbox.get(0)).doubleValue(),
((Number) bbox.get(2)).doubleValue());
}
} else {
throw new IOException("The returned bound was not of size 4 but of size: " + bbox.size());
}
}
return env;
}
/**
* This method get the counts of the feature-set which satisfy the fnQuery
* portions of this code has been borrowed from
* https://svn.osgeo.org/geotools/trunk/modules/library/jdbc/src/main/java/org/geotools/jdbc/JDBCFeatureSource.java
* @param fnQuery
* @return count
* @throws IOException
*/
@Override
protected int getCountInternal(Query fnQuery) throws IOException {
int count = 0;
/* Spilt the filter into two parts pre and post */
Filter[] split = splitFilter(fnQuery.getFilter());
Filter preFilter = split[0];
Filter postFilter = split[1];
if ((postFilter != null) && (postFilter != Filter.INCLUDE)) {
return -1;
} else {
Query tmpPreQ = new Query(fnQuery);
tmpPreQ.setFilter(preFilter);
/**/
String strQuery = SFSDataStoreUtil.encodeQuery(tmpPreQ, getSchema());
/* Create the URL */
String strCount = ods.resourceToString("data/" + layer.getTypeName().getLocalPart(), "mode=count&" + strQuery);
try {
count = Integer.parseInt(strCount);
} catch (NumberFormatException nfe) {
LOGGER.log(Level.SEVERE, "Number format Exception in getCountInternal : FeatureSource -- getCount --" + nfe.getMessage(), nfe);
return 0;
}
}
/* Check if the count greater than maxFeature Limit*/
if (fnQuery.getMaxFeatures() > 0 && count > fnQuery.getMaxFeatures()) {
count = fnQuery.getMaxFeatures();
}
return count;
}
/**
* This method invokes the OpenDataStoreFeatureReader class
* This method has been borrowed from
* https://svn.osgeo.org/geotools/trunk/modules/library/jdbc/src/main/java/org/geotools/jdbc/JDBCFeatureSource.java
* @param fnQuery
* @return FeatureReader
* @throws IOException
*/
@Override
protected FeatureReader<SimpleFeatureType, SimpleFeature> getReaderInternal(Query fnQuery) throws IOException {
// simplify the filter
Filter filter = fnQuery.getFilter();
SimplifyingFilterVisitor simplifier = new SimplifyingFilterVisitor();
filter = (Filter) filter.accept(simplifier, null);
// Split the filter into two parts, pre and post
Filter[] split = splitFilter(filter);
Filter preFilter = split[0];
Filter postFilter = split[1];
/* Getting the preFilter part of the query*/
Query preQuery = new Query(fnQuery);
preQuery.setFilter(preFilter);
/* Check if the schema remains same of do we need to update it*/
SimpleFeatureType returnedSchema;
SimpleFeatureType querySchema;
if (fnQuery.getPropertyNames() == Query.ALL_NAMES) {
returnedSchema = querySchema = getSchema();
} else {
returnedSchema = SimpleFeatureTypeBuilder.retype(getSchema(), fnQuery.getPropertyNames());
FilterAttributeExtractor extractor = new FilterAttributeExtractor(getSchema());
// we need to let all attribute pass as we're going to make a full filter evaluation
// in memory
filter.accept(extractor, null);
String[] extraAttributes = extractor.getAttributeNames();
if (extraAttributes == null || extraAttributes.length == 0) {
// nothing to do
querySchema = returnedSchema;
} else {
List<String> allAttributes = new ArrayList<String>(Arrays.asList(fnQuery.getPropertyNames()));
for (String extraAttribute : extraAttributes) {
if (!allAttributes.contains(extraAttribute)) {
allAttributes.add(extraAttribute);
}
}
String[] allAttributeArray = (String[]) allAttributes.toArray(new String[allAttributes.size()]);
preQuery.setPropertyNames(allAttributeArray);
querySchema = SimpleFeatureTypeBuilder.retype(getSchema(), allAttributeArray);
}
}
/* Get the reader*/
FeatureReader<SimpleFeatureType, SimpleFeature> reader;
/* Do the pre part first */
reader = new SFSFeatureReader(getState(), layer, preQuery, querySchema);
/*
* Normally we should finish off with post filtering, but reality proves that the
* remote service sometimes does not implement the protocol filtering spec fully.
* Don't trust the records sent back and apply the whole filter again on top of them
*/
if(filter != null && !Filter.INCLUDE.equals(filter)) {
reader = new FilteringFeatureReader<SimpleFeatureType, SimpleFeature>(reader, filter);
}
if(querySchema.getAttributeCount() > returnedSchema.getAttributeCount()) {
reader = new ReTypeFeatureReader(reader, returnedSchema);
}
return reader;
}
/**
* This method returns the schema of the layer
* @return SimpleFeatureType
* @throws IOException, MalformedURLException
*/
@Override
protected SimpleFeatureType buildFeatureType() throws IOException, MalformedURLException {
/* Get the layer describing URL*/
String jsonString = ods.resourceToString("describe/" + contentEntry.getName().getLocalPart(), null);
JSONParser parser = new JSONParser();
JSONArray jsonArray = null;
try {
jsonArray = (JSONArray) parser.parse(jsonString);
} catch (org.json.simple.parser.ParseException pe) {
LOGGER.log(Level.SEVERE, "parse Exception : FeatureSource -- buildFeatureType --" + pe.getMessage(), pe);
}
/* Lets build the feature type*/
SimpleFeatureTypeBuilder fbt = new SimpleFeatureTypeBuilder();
fbt.setName(contentEntry.getName());
/* set the CRS*/
fbt.setCRS(layer.getCoordinateReferenceSystem());
/* First Iterator*/
Iterator itr = jsonArray.iterator();
while (itr.hasNext()) {
HashMap<String, String> tmpMap = (HashMap<String, String>) itr.next();
/* Iterate Through the hashMap to extract the key-value pairs*/
for (Map.Entry<String, String> entry : tmpMap.entrySet()) {
//LOGGER.log(Level.INFO, "{0} - {1} - {2}", new Object[]{entry.getKey(), entry.getValue(), OpenDataStoreUtil.getClass(entry.getValue())});
fbt.add(entry.getKey(), SFSDataStoreUtil.getClass(entry.getValue()));
}
}
return fbt.buildFeatureType();
}
/**
*
* @param fnSchema
* @return QueryCapabilities
* @throws IOException
*/
public QueryCapabilities getQueryCapabilities(SimpleFeatureType fnSchema) throws IOException {
SFSQueryCapabilities odsc = new SFSQueryCapabilities(fnSchema);
return odsc;
}
/**
*
* @param fnID
* @return SimpleFeature
* @throws MalformedURLException
* @throws MalformedURLException, IOException, org.json.simple.parser.ParseException
*/
public SimpleFeature getFeatureWithID(String fnID) throws MalformedURLException, IOException, org.json.simple.parser.ParseException {
if (fnID == null) {
throw new IOException("FeatureID cannot be null");
}
/* Get the layer describing URL*/
String jsonString = ods.resourceToString("data/" + contentEntry.getName().getLocalPart() + "/" + fnID, null);
FeatureJSON fjson = new FeatureJSON();
SimpleFeature feature = fjson.readFeature(new StringReader(SFSDataStoreUtil.strip(jsonString)));
return feature;
}
/**
* This method has been borrowed from
* https://svn.osgeo.org/geotools/trunk/modules/library/jdbc/src/main/java/org/geotools/jdbc/JDBCFeatureSource.java
* Helper method for splitting a filter.
*/
Filter[] splitFilter(Filter fnOriginalFilter) {
/* Split filter into 2 parts */
Filter[] split = new Filter[2];
if (fnOriginalFilter != null) {
//create a filter splitter
PostPreProcessFilterSplittingVisitor splitter = new PostPreProcessFilterSplittingVisitor(ods.ODS_FILTER_CAPABILITIES, getSchema(), null);
fnOriginalFilter.accept(splitter, null);
/* Natively supported*/
split[0] = splitter.getFilterPre();
/* Not natively supported*/
split[1] = splitter.getFilterPost();
}
/* Getting simplest possible filter*/
SimplifyingFilterVisitor visitor = new SimplifyingFilterVisitor();
split[0] = (Filter) split[0].accept(visitor, null);
split[1] = (Filter) split[1].accept(visitor, null);
return split;
}
@Override
protected boolean canFilter() {
return true;
}
@Override
protected boolean canLimit() {
return true;
}
@Override
protected boolean canOffset() {
return true;
}
@Override
protected boolean canRetype() {
return true;
}
@Override
protected boolean canSort() {
return true;
}
}