Package org.geoserver.wfs

Source Code of org.geoserver.wfs.GetFeature

/* (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;
    }
}
TOP

Related Classes of org.geoserver.wfs.GetFeature

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.