Package org.vfny.geoserver.global

Source Code of org.vfny.geoserver.global.GeoServerFeatureSource

/* (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.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

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.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;

    /**
     * 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) {
        this.source = DataUtilities.simple(source);
        this.schema = schema;
        this.definitionQuery = definitionQuery;
        this.declaredCRS = declaredCRS;
        this.srsHandling = ProjectionPolicy.get( srsHandling );
        this.linearizationTolerance = linearizationTolerance;

        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
     * visibiliy.
     * </p>
     *
     * @param featureSource
     * @param schema DOCUMENT ME!
     * @param definitionQuery DOCUMENT ME!
     * @param declaredCRS
     * @param linearizationTolerance TODO
     * @return
     */
    public static GeoServerFeatureSource create(FeatureSource <SimpleFeatureType, SimpleFeature> featureSource, SimpleFeatureType schema,
        Filter definitionQuery, CoordinateReferenceSystem declaredCRS, int srsHandling, Double linearizationTolerance) {
        if (featureSource instanceof FeatureLocking) {
            return new GeoServerFeatureLocking(
                    (FeatureLocking<SimpleFeatureType, SimpleFeature>) featureSource, schema,
                    definitionQuery, declaredCRS, srsHandling, linearizationTolerance);
        } else if (featureSource instanceof FeatureStore) {
            return new GeoServerFeatureStore(
                    (FeatureStore<SimpleFeatureType, SimpleFeature>) featureSource, schema,
                    definitionQuery, declaredCRS, srsHandling, linearizationTolerance);
        }

        return new GeoServerFeatureSource(featureSource, schema, definitionQuery, declaredCRS, srsHandling, null);
    }

    /**
     * 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>
     *
     * @return
     *
     * @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
     *
     * @return
     *
     * @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);
       
        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
     * @return
     * @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>
     *
     * @return
     *
     * @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;
            }
        };
       
    }  
}
TOP

Related Classes of org.vfny.geoserver.global.GeoServerFeatureSource

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.