/* (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.vfny.geoserver.global; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Logger; import org.geoserver.catalog.MetadataMap; import org.geoserver.catalog.ProjectionPolicy; import org.geotools.data.DataSourceException; import org.geotools.data.DataStore; import org.geotools.data.DataUtilities; import org.geotools.data.FeatureListener; import org.geotools.data.FeatureLocking; import org.geotools.data.FeatureSource; import org.geotools.data.FeatureStore; import org.geotools.data.Query; import org.geotools.data.QueryCapabilities; import org.geotools.data.ResourceInfo; import org.geotools.data.crs.ForceCoordinateSystemFeatureResults; import org.geotools.data.crs.ReprojectFeatureResults; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.data.simple.SimpleFeatureSource; import org.geotools.factory.CommonFactoryFinder; import org.geotools.factory.Hints; import org.geotools.factory.Hints.ConfigurationMetadataKey; import org.geotools.feature.FeatureTypes; import org.geotools.feature.SchemaException; import org.geotools.feature.collection.MaxSimpleFeatureCollection; import org.geotools.feature.collection.SortedSimpleFeatureCollection; import org.geotools.filter.spatial.DefaultCRSFilterVisitor; import org.geotools.filter.spatial.ReprojectingFilterVisitor; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.referencing.CRS; import org.opengis.feature.Feature; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.feature.type.Name; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory2; import org.opengis.filter.sort.SortBy; import org.opengis.referencing.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.OperationNotFoundException; import org.opengis.referencing.operation.TransformException; /** * GeoServer wrapper for backend Geotools2 DataStore. * * <p> * Support FeatureSource decorator for FeatureTypeInfo that takes care of * mapping the FeatureTypeInfo's FeatureSource with the schema and definition * query configured for it. * </p> * * <p> * Because GeoServer requires that attributes always be returned in the same * order we need a way to smoothly inforce this. Could we use this class to do * so? * </p> * * @author Gabriel Roldan * @version $Id$ */ public class GeoServerFeatureSource implements SimpleFeatureSource { /** Shared package logger */ private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.vfny.geoserver.global"); /** FeatureSource being served up */ protected SimpleFeatureSource source; /** The single filter factory for this source (grabbing it has a high sync penalty */ static FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(null); /** * GeoTools2 Schema information * * <p> * Is this the same as source.getSchema() or is it used supply the order * that GeoServer requires attributes to be returned in? * </p> */ protected SimpleFeatureType schema; /** Used to constrain the Feature made available to GeoServer. */ protected Filter definitionQuery = Filter.INCLUDE; /** Geometries will be forced to this CRS (or null, if no forcing is needed) */ protected CoordinateReferenceSystem declaredCRS; /** How to handle SRS */ protected ProjectionPolicy srsHandling; /** FeatureTypeInfo metadata to pass to extensions within the Query **/ protected MetadataMap metadata; /** * Distance used for curve linearization tolerance, as an absolute value expressed in the data * native CRS */ protected Double linearizationTolerance; /** * Creates a new GeoServerFeatureSource object. * * @param source GeoTools2 FeatureSource * @param schema SimpleFeatureType returned by this FeatureSource * @param definitionQuery Filter used to limit results * @param declaredCRS Geometries will be forced or projected to this CRS * @param linearizationTolerance Distance used for curve linearization tolerance, as an absolute * value expressed in the data native CRS */ GeoServerFeatureSource(FeatureSource<SimpleFeatureType, SimpleFeature> source, SimpleFeatureType schema, Filter definitionQuery, CoordinateReferenceSystem declaredCRS, int srsHandling, Double linearizationTolerance, MetadataMap metadata) { this(source, new Settings(schema, definitionQuery, declaredCRS, srsHandling, linearizationTolerance, metadata)); } /** * Creates a new GeoServerFeatureSource object. * * @param source GeoTools2 FeatureSource * @param settings Settings for this source */ GeoServerFeatureSource(FeatureSource<SimpleFeatureType, SimpleFeature> source, Settings settings) { this.source = DataUtilities.simple(source); this.schema = settings.schema; this.definitionQuery = settings.definitionQuery; this.declaredCRS = settings.declaredCRS; this.srsHandling = ProjectionPolicy.get( settings.srsHandling ); this.linearizationTolerance = settings.linearizationTolerance; this.metadata = settings.metadata; if (this.definitionQuery == null) { this.definitionQuery = Filter.INCLUDE; } } /** * Returns the same name than the feature type (ie, * {@code getSchema().getName()} to honor the simple feature land common * practice of calling the same both the Features produces and their types * * @since 1.7 * @see FeatureSource#getName() */ public Name getName() { return getSchema().getName(); } /** * Factory that make the correct decorator for the provided featureSource. * * <p> * This factory method is public and will be used to create all required * subclasses. By comparison the constructors for this class have package * visibility. * </p> * * @deprecated * * @param featureSource * @param schema DOCUMENT ME! * @param definitionQuery DOCUMENT ME! * @param declaredCRS * @param linearizationTolerance TODO * @param metadata Feature type metadata * */ public static GeoServerFeatureSource create(FeatureSource <SimpleFeatureType, SimpleFeature> featureSource, SimpleFeatureType schema, Filter definitionQuery, CoordinateReferenceSystem declaredCRS, int srsHandling, Double linearizationTolerance, MetadataMap metadata) { return create(featureSource, new Settings(schema, definitionQuery, declaredCRS, srsHandling, linearizationTolerance, metadata)); } /** * Factory that make the correct decorator for the provided featureSource. * * <p> * This factory method is public and will be used to create all required * subclasses. By comparison the constructors for this class have package * visibility. * </p> * * @param featureSource * @param settings Settings for this store * */ public static GeoServerFeatureSource create(FeatureSource <SimpleFeatureType, SimpleFeature> featureSource, Settings settings) { if (featureSource instanceof FeatureLocking) { return new GeoServerFeatureLocking( (FeatureLocking<SimpleFeatureType, SimpleFeature>) featureSource, settings); } else if (featureSource instanceof FeatureStore) { return new GeoServerFeatureStore( (FeatureStore<SimpleFeatureType, SimpleFeature>) featureSource, settings); } return new GeoServerFeatureSource(featureSource, settings); } /** * Takes a query and adapts it to match re definitionQuery filter * configured for a feature type. * * @param query Query against this DataStore * @param schema TODO * * @return Query restricted to the limits of definitionQuery * * @throws IOException See DataSourceException * @throws DataSourceException If query could not meet the restrictions of * definitionQuery */ protected Query makeDefinitionQuery(Query query, SimpleFeatureType schema) throws IOException { if ((query == Query.ALL) || query.equals(Query.ALL)) { return query; } try { String[] propNames = extractAllowedAttributes(query, schema); Filter filter = query.getFilter(); filter = makeDefinitionFilter(filter); Query defQuery = new Query(query); defQuery.setFilter(filter); defQuery.setPropertyNames(propNames); // set sort by if (query.getSortBy() != null) { defQuery.setSortBy(query.getSortBy()); } // tell the data sources about the default linearization tolerance for curved // geometries they might be reading if (linearizationTolerance != null) { query.getHints().put(Hints.LINEARIZATION_TOLERANCE, linearizationTolerance); } return defQuery; } catch (Exception ex) { throw new DataSourceException( "Could not restrict the query to the definition criteria: " + ex.getMessage(), ex); } } /** * List of allowed attributes. * * <p> * Creates a list of FeatureTypeInfo's attribute names based on the * attributes requested by <code>query</code> and making sure they not * contain any non exposed attribute. * </p> * * <p> * Exposed attributes are those configured in the "attributes" element of * the FeatureTypeInfo's configuration * </p> * * @param query User's origional query * @param schema TODO * * @return List of allowed attribute types */ private String[] extractAllowedAttributes(Query query, SimpleFeatureType schema) { String[] propNames = null; if (query.retrieveAllProperties()) { List<String> props = new ArrayList(); for (int i = 0; i < schema.getAttributeCount(); i++) { AttributeDescriptor att = schema.getDescriptor(i); //if this is a joined attribute, don't include it //TODO: make this a better check, actually verify it vs the query object if (Feature.class.isAssignableFrom(att.getType().getBinding()) && !query.getJoins().isEmpty()) { continue; } props.add(att.getLocalName()); } propNames = props.toArray(new String[props.size()]); } else { String[] queriedAtts = query.getPropertyNames(); int queriedAttCount = queriedAtts.length; List allowedAtts = new LinkedList(); for (int i = 0; i < queriedAttCount; i++) { if (schema.getDescriptor(queriedAtts[i]) != null) { allowedAtts.add(queriedAtts[i]); } else { LOGGER.info("queried a not allowed property: " + queriedAtts[i] + ". Ommitting it from query"); } } propNames = (String[]) allowedAtts.toArray(new String[allowedAtts.size()]); } return propNames; } /** * If a definition query has been configured for the FeatureTypeInfo, makes * and return a new Filter that contains both the query's filter and the * layer's definition one, by logic AND'ing them. * * @param filter Origional user supplied Filter * * @return Filter adjusted to the limitations of definitionQuery * * @throws DataSourceException If the filter could not meet the limitations * of definitionQuery */ protected Filter makeDefinitionFilter(Filter filter) throws DataSourceException { Filter newFilter = filter; try { if (definitionQuery != Filter.INCLUDE) { newFilter = ff.and(definitionQuery, filter); } } catch (Exception ex) { throw new DataSourceException("Can't create the definition filter", ex); } return newFilter; } /** * Implement getDataStore. * * <p> * Description ... * </p> * * * * @see org.geotools.data.FeatureSource#getDataStore() */ public DataStore getDataStore() { return (DataStore) source.getDataStore(); } /** * Implement addFeatureListener. * * <p> * Description ... * </p> * * @param listener * * @see org.geotools.data.FeatureSource#addFeatureListener(org.geotools.data.FeatureListener) */ public void addFeatureListener(FeatureListener listener) { source.addFeatureListener(listener); } /** * Implement removeFeatureListener. * * <p> * Description ... * </p> * * @param listener * * @see org.geotools.data.FeatureSource#removeFeatureListener(org.geotools.data.FeatureListener) */ public void removeFeatureListener(FeatureListener listener) { source.removeFeatureListener(listener); } /** * Implement getFeatures. * * <p> * Description ... * </p> * * @param query * * * * @throws IOException * * @see org.geotools.data.FeatureSource#getFeatures(org.geotools.data.Query) */ public SimpleFeatureCollection getFeatures(Query query) throws IOException { // check for a sort in the query, if the underlying store does not do sorting // then we need to apply it after the fact SortBy[] sortBy = query.getSortBy(); Integer offset = null, maxFeatures = null; if(sortBy != null && sortBy != SortBy.UNSORTED) { if(!source.getQueryCapabilities().supportsSorting(sortBy)) { query.setSortBy(null); // if paging is in and we cannot do sorting natively // we should not let the datastore handle it: we need to sort first, then // page on it offset = query.getStartIndex(); maxFeatures = query.getMaxFeatures() == Integer.MAX_VALUE ? null : query.getMaxFeatures(); query.setStartIndex(null); query.setMaxFeatures(Query.DEFAULT_MAX); } else { sortBy = null; } } //check for an offset in the query, if the underlying store does not do offsets then // we need to apply it after the fact along with max features if (query.getStartIndex() != null) { if (!source.getQueryCapabilities().isOffsetSupported()) { offset = query.getStartIndex(); maxFeatures = query.getMaxFeatures() == Integer.MAX_VALUE ? null : query.getMaxFeatures(); query.setStartIndex(null); query.setMaxFeatures(Query.DEFAULT_MAX); } } Query reprojected = reprojectFilter(query); Query newQuery = adaptQuery(reprojected, schema); // Merge configuration metadata into query hints. This ensures that all // metadata for a particular FeatureType is available in the actual data store. // All String keys in the featuretype.xml metadata will be transformed into // ConfigurationMetadataKey instances for (Entry<String, Serializable> e : metadata.entrySet()) { try { ConfigurationMetadataKey key = ConfigurationMetadataKey.get(e.getKey()); newQuery.getHints().put(key, e.getValue()); } catch (IllegalArgumentException ignore){ LOGGER.fine("Hint "+e.getKey()+": "+ignore); } } CoordinateReferenceSystem targetCRS = query.getCoordinateSystemReproject(); try { //this is the raw "unprojected" feature collection SimpleFeatureCollection fc = source.getFeatures(newQuery); // apply sorting if necessary if(sortBy != null) { fc = new SortedSimpleFeatureCollection(fc, sortBy); } //apply limit offset if necessary if (offset != null || maxFeatures != null) { fc = new MaxSimpleFeatureCollection(fc, offset == null ? 0 : offset, maxFeatures == null ? Integer.MAX_VALUE : maxFeatures); } //apply reprojection return applyProjectionPolicies(targetCRS, fc); } catch (Exception e) { throw new DataSourceException(e); } } private Query reprojectFilter(Query query) throws IOException { SimpleFeatureType nativeFeatureType = source.getSchema(); final GeometryDescriptor geom = nativeFeatureType.getGeometryDescriptor(); // if no geometry involved, no reprojection needed if(geom == null) return query; try { // default CRS: the CRS we can assume geometry and bbox elements in filter are // that is, usually the declared one, but the native one in the leave case CoordinateReferenceSystem defaultCRS = null; // we need to reproject all bbox and geometries to a target crs, which is // the native one usually, but it's the declared on in the force case (since in // that case we completely ignore the native one) CoordinateReferenceSystem targetCRS = null; CoordinateReferenceSystem nativeCRS = geom.getCoordinateReferenceSystem(); if(srsHandling == ProjectionPolicy.FORCE_DECLARED) { defaultCRS = declaredCRS; targetCRS = declaredCRS; nativeFeatureType = FeatureTypes.transform(nativeFeatureType, declaredCRS); } else if(srsHandling == ProjectionPolicy.REPROJECT_TO_DECLARED) { defaultCRS = declaredCRS; targetCRS = nativeCRS; } else { // FeatureTypeInfo.LEAVE defaultCRS = nativeCRS; targetCRS = nativeCRS; } // now we apply a default to all geometries and bbox in the filter DefaultCRSFilterVisitor defaultCRSVisitor = new DefaultCRSFilterVisitor(ff, defaultCRS); Filter filter = query.getFilter() != null ? query.getFilter() : Filter.INCLUDE; Filter defaultedFilter = (Filter) filter.accept(defaultCRSVisitor, null); // and then we reproject all geometries so that the datastore receives // them in the native projection system (or the forced one, in case of force) ReprojectingFilterVisitor reprojectingVisitor = new ReprojectingFilterVisitor(ff, nativeFeatureType); Filter reprojectedFilter = (Filter) defaultedFilter.accept(reprojectingVisitor, null); Query reprojectedQuery = new Query(query); reprojectedQuery.setFilter(reprojectedFilter); return reprojectedQuery; } catch(Exception e) { throw new DataSourceException("Had troubles handling filter reprojection...", e); } } /** * Wraps feature collection as needed in order to respect the current projection policy and the * target CRS, if any (can be null, in that case only the projection policy is applied) */ protected SimpleFeatureCollection applyProjectionPolicies( CoordinateReferenceSystem targetCRS, SimpleFeatureCollection fc) throws IOException, SchemaException, TransformException, OperationNotFoundException, FactoryException { if ( fc.getSchema().getGeometryDescriptor() == null ) { // reprojection and crs forcing do not make sense, bail out return fc; } CoordinateReferenceSystem nativeCRS = fc.getSchema().getGeometryDescriptor().getCoordinateReferenceSystem(); if(nativeCRS == null) { if(declaredCRS != null) { fc = new ForceCoordinateSystemFeatureResults(fc, declaredCRS); nativeCRS = declaredCRS; } } else if(srsHandling == ProjectionPolicy.FORCE_DECLARED && !nativeCRS.equals(declaredCRS)) { fc = new ForceCoordinateSystemFeatureResults(fc, declaredCRS); nativeCRS = declaredCRS; } else if(srsHandling == ProjectionPolicy.REPROJECT_TO_DECLARED && targetCRS == null && !nativeCRS.equals(declaredCRS)) { fc = new ReprojectFeatureResults(fc,declaredCRS); } //was reproject specified as part of the query? if (targetCRS != null ) { //reprojection is occuring if ( nativeCRS == null ) { //we do not know what the native crs which means we can // not be sure if we should reproject or not... so we go // ahead and reproject regardless fc = new ReprojectFeatureResults(fc,targetCRS); } else { //only reproject if native != target if (!CRS.equalsIgnoreMetadata(nativeCRS, targetCRS)) { fc = new ReprojectFeatureResults(fc,targetCRS); } } } return fc; } /** * Transforms the query applying the definition query in this layer, removes reprojection * since data stores cannot be trusted * @param query * @param schema TODO * * @throws IOException */ protected Query adaptQuery(Query query, SimpleFeatureType schema) throws IOException { // if needed, reproject the filter to the native srs Query newQuery = makeDefinitionQuery(query, schema); // // see if the CRS got xfered over // // a. old had a CRS, new doesnt // boolean requireXferCRS = (newQuery.getCoordinateSystem() == null) // && (query.getCoordinateSystem() != null); // // if ((newQuery.getCoordinateSystem() != null) && (query.getCoordinateSystem() != null)) { // //b. both have CRS, but they're different // requireXferCRS = !(newQuery.getCoordinateSystem().equals(query.getCoordinateSystem())); // } // // if (requireXferCRS) { // //carry along the CRS // if (!(newQuery instanceof Query)) { // newQuery = new Query(newQuery); // } // // ((Query) newQuery).setCoordinateSystem(query.getCoordinateSystem()); // } //JD: this is a huge hack... but its the only way to ensure that we // we get what we ask for ... which is not reprojection, since // datastores are unreliable in this aspect we dont know if they will // reproject or not. // AA: added force coordinate system reset as well, since we cannot // trust GT2 datastores there neither. if ( newQuery.getCoordinateSystemReproject() != null ) { newQuery.setCoordinateSystemReproject(null); } if ( newQuery.getCoordinateSystem() != null ) { newQuery.setCoordinateSystem(null); } return newQuery; } public SimpleFeatureCollection getFeatures(Filter filter) throws IOException { return getFeatures(new Query(schema.getTypeName(), filter)); } public SimpleFeatureCollection getFeatures() throws IOException { return getFeatures(Query.ALL); } /** * Implement getSchema. * * <p> * Description ... * </p> * * * * @see org.geotools.data.FeatureSource#getSchema() */ public SimpleFeatureType getSchema() { return schema; } /** * Retrieves the total extent of this FeatureSource. * * <p> * Please note this extent will reflect the provided definitionQuery. * </p> * * @return Extent of this FeatureSource, or <code>null</code> if no * optimizations exist. * * @throws IOException If bounds of definitionQuery */ public ReferencedEnvelope getBounds() throws IOException { // since CRS is at most forced, we don't need to change this code if (definitionQuery == Filter.INCLUDE) { return source.getBounds(); } else { Query query = new Query(getSchema().getTypeName(), definitionQuery); return source.getBounds(query); } } /** * Retrive the extent of the Query. * * <p> * This method provides access to an optimized getBounds opperation. If no * optimized opperation is available <code>null</code> will be returned. * </p> * * <p> * You may still make use of getFeatures( Query ).getCount() which will * return the correct answer (even if it has to itterate through all the * results to do so. * </p> * * @param query User's query * * @return Extend of Query or <code>null</code> if no optimization is * available * * @throws IOException If a problem is encountered with source */ public ReferencedEnvelope getBounds(Query query) throws IOException { // since CRS is at most forced, we don't need to change this code try { query = makeDefinitionQuery(query, schema); } catch (IOException ex) { return null; } return source.getBounds(query); } /** * Adjust query and forward to source. * * <p> * This method provides access to an optimized getCount opperation. If no * optimized opperation is available <code>-1</code> will be returned. * </p> * * <p> * You may still make use of getFeatures( Query ).getCount() which will * return the correct answer (even if it has to itterate through all the * results to do so). * </p> * * @param query User's query. * * @return Number of Features for Query, or -1 if no optimization is * available. */ public int getCount(Query query) { try { query = makeDefinitionQuery(query, schema); } catch (IOException ex) { return -1; } try { return source.getCount(query); } catch (IOException e) { return 0; } } public Set getSupportedHints() { return source.getSupportedHints(); } public ResourceInfo getInfo() { return source.getInfo(); } public QueryCapabilities getQueryCapabilities() { // we can do both sorting and offset locally if necessary return new QueryCapabilitiesDecorator(source.getQueryCapabilities()) { @Override public boolean isOffsetSupported() { return true; } @Override public boolean supportsSorting(SortBy[] sortAttributes) { return true; } }; } protected static class Settings { protected SimpleFeatureType schema; protected Filter definitionQuery; protected CoordinateReferenceSystem declaredCRS; protected int srsHandling; protected Double linearizationTolerance; protected MetadataMap metadata; /** * Constructor parameter for GeoServerFeatureSource. * * @param schema * @param definitionQuery * @param declaredCRS * @param srsHandling * @param linearizationTolerance * @param metadata Feature type metadata */ protected Settings(SimpleFeatureType schema, Filter definitionQuery, CoordinateReferenceSystem declaredCRS, int srsHandling, Double linearizationTolerance, MetadataMap metadata) { this.schema = schema; this.definitionQuery = definitionQuery; this.declaredCRS = declaredCRS; this.srsHandling = srsHandling; this.linearizationTolerance = linearizationTolerance; this.metadata = metadata; } } }