001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/tags/2.1/src/org/deegree/ogcwebservices/wfs/operation/GetFeature.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2006 by:
006     EXSE, Department of Geography, University of Bonn
007     http://www.giub.uni-bonn.de/deegree/
008     lat/lon GmbH
009     http://www.lat-lon.de
010    
011     This library is free software; you can redistribute it and/or
012     modify it under the terms of the GNU Lesser General Public
013     License as published by the Free Software Foundation; either
014     version 2.1 of the License, or (at your option) any later version.
015    
016     This library is distributed in the hope that it will be useful,
017     but WITHOUT ANY WARRANTY; without even the implied warranty of
018     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
019     Lesser General Public License for more details.
020    
021     You should have received a copy of the GNU Lesser General Public
022     License along with this library; if not, write to the Free Software
023     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
024    
025     Contact:
026    
027     Andreas Poth
028     lat/lon GmbH
029     Aennchenstraße 19
030     53177 Bonn
031     Germany
032     E-Mail: poth@lat-lon.de
033    
034     Prof. Dr. Klaus Greve
035     Department of Geography
036     University of Bonn
037     Meckenheimer Allee 166
038     53115 Bonn
039     Germany
040     E-Mail: greve@giub.uni-bonn.de
041     
042     ---------------------------------------------------------------------------*/
043    package org.deegree.ogcwebservices.wfs.operation;
044    
045    import java.net.URI;
046    import java.util.ArrayList;
047    import java.util.HashMap;
048    import java.util.List;
049    import java.util.Map;
050    
051    import org.deegree.datatypes.QualifiedName;
052    import org.deegree.framework.log.ILogger;
053    import org.deegree.framework.log.LoggerFactory;
054    import org.deegree.framework.util.KVP2Map;
055    import org.deegree.framework.xml.NamespaceContext;
056    import org.deegree.i18n.Messages;
057    import org.deegree.model.filterencoding.Filter;
058    import org.deegree.ogcbase.PropertyPath;
059    import org.deegree.ogcbase.PropertyPathFactory;
060    import org.deegree.ogcbase.PropertyPathStep;
061    import org.deegree.ogcbase.SortProperty;
062    import org.deegree.ogcwebservices.InconsistentRequestException;
063    import org.deegree.ogcwebservices.InvalidParameterValueException;
064    import org.deegree.ogcwebservices.OGCWebServiceException;
065    import org.w3c.dom.Element;
066    
067    /**
068     * Represents a <code>GetFeature</code> request to a web feature service.
069     * <p>
070     * The GetFeature operation allows the retrieval of features from a web feature service. A
071     * GetFeature request is processed by a WFS and when the value of the outputFormat attribute is set
072     * to text/gml; subtype=gml/3.1.1, a GML instance document, containing the result set, is returned
073     * to the client.
074     * 
075     * @author <a href="mailto:poth@lat-lon.de">Andreas Poth </a>
076     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
077     * @author last edited by: $Author: rbezema $
078     * 
079     * @version $Revision: 7923 $, $Date: 2007-08-06 14:34:52 +0200 (Mo, 06 Aug 2007) $
080     */
081    public class GetFeature extends AbstractWFSRequest {
082    
083        private static final ILogger LOG = LoggerFactory.getLogger( GetFeature.class );
084    
085        private static final long serialVersionUID = 8885456550385433051L;
086    
087        /** Serialized java object format (deegree specific extension) * */
088        public static final String FORMAT_FEATURECOLLECTION = "FEATURECOLLECTION";
089    
090        /**
091         * Known result types.
092         */
093        public static enum RESULT_TYPE {
094    
095            /** A full response should be generated. */
096            RESULTS,
097    
098            /** Only a count of the number of features should be returned. */
099            HITS
100        }
101    
102        private RESULT_TYPE resultType = RESULT_TYPE.RESULTS;
103    
104        private String outputFormat;
105    
106        private int maxFeatures;
107    
108        private int traverseXLinkDepth;
109    
110        private int traverseXLinkExpiry;
111    
112        private List<Query> queries;
113    
114        // deegree specific extension, default: 1 (start at first feature)
115        private int startPosition;
116    
117        /**
118         * Creates a new <code>GetFeature</code> instance.
119         * 
120         * @param version
121         *            request version
122         * @param id
123         *            id of the request
124         * @param handle
125         * @param resultType
126         *            desired result type (results | hits)
127         * @param outputFormat
128         *            requested result format
129         * @param maxFeatures
130         * @param startPosition
131         *            deegree specific parameter defining where to start considering features
132         * @param traverseXLinkDepth
133         * @param traverseXLinkExpiry
134         * @param queries
135         * @param vendorSpecificParam
136         */
137        GetFeature( String version, String id, String handle, RESULT_TYPE resultType, String outputFormat, int maxFeatures,
138                    int startPosition, int traverseXLinkDepth, int traverseXLinkExpiry, Query[] queries,
139                    Map<String, String> vendorSpecificParam ) {
140            super( version, id, handle, vendorSpecificParam );
141            this.setQueries( queries );
142            this.outputFormat = outputFormat;
143            this.maxFeatures = maxFeatures;
144            this.startPosition = startPosition;
145            this.resultType = resultType;
146            this.traverseXLinkDepth = traverseXLinkDepth;
147            this.traverseXLinkExpiry = traverseXLinkExpiry;
148        }
149    
150        /**
151         * Creates a new <code>GetFeature</code> instance from the given parameters.
152         * 
153         * @param version
154         *            request version
155         * @param id
156         *            id of the request
157         * @param resultType
158         *            desired result type (results | hits)
159         * @param outputFormat
160         *            requested result format
161         * @param handle
162         * @param maxFeatures
163         *            default = -1 (all features)
164         * @param startPosition
165         *            default = 0 (starting at the first feature)
166         * @param traverseXLinkDepth
167         * @param traverseXLinkExpiry
168         * @param queries
169         *            a set of Query objects that describes the query to perform
170         * @return new <code>GetFeature</code> request
171         */
172        public static GetFeature create( String version, String id, RESULT_TYPE resultType, String outputFormat,
173                                         String handle, int maxFeatures, int startPosition, int traverseXLinkDepth,
174                                         int traverseXLinkExpiry, Query[] queries ) {
175            return new GetFeature( version, id, handle, resultType, outputFormat, maxFeatures, startPosition,
176                                   traverseXLinkDepth, traverseXLinkExpiry, queries, null );
177        }
178    
179        /**
180         * Creates a new <code>GetFeature</code> instance from a document that contains the DOM
181         * representation of the request.
182         * 
183         * @param id
184         *            of the request
185         * @param root
186         *            element that contains the DOM representation of the request
187         * @return new <code>GetFeature</code> request
188         * @throws OGCWebServiceException
189         */
190        public static GetFeature create( String id, Element root )
191                                throws OGCWebServiceException {
192            GetFeatureDocument doc = new GetFeatureDocument();
193            doc.setRootElement( root );
194            GetFeature request;
195            try {
196                request = doc.parse( id );
197            } catch ( Exception e ) {
198                LOG.logError( e.getMessage(), e );
199                throw new OGCWebServiceException( "GetFeature", e.getMessage() );
200            }
201            return request;
202        }
203    
204        /**
205         * Creates a new <code>GetFeature</code> instance from the given key-value pair encoded
206         * request.
207         * 
208         * @param id
209         *            request identifier
210         * @param request
211         * @return new <code>GetFeature</code> request
212         * @throws InvalidParameterValueException
213         * @throws InconsistentRequestException
214         */
215        public static GetFeature create( String id, String request )
216                                throws InconsistentRequestException, InvalidParameterValueException {
217            Map<String, String> map = KVP2Map.toMap( request );
218            map.put( "ID", id );
219            return create( map );
220        }
221    
222        /**
223         * Creates a new <code>GetFeature</code> request from the given map.
224         * 
225         * @param kvp
226         *            key-value pairs, keys have to be uppercase
227         * @return new <code>GetFeature</code> request
228         * @throws InconsistentRequestException
229         * @throws InvalidParameterValueException
230         */
231        public static GetFeature create( Map<String, String> kvp )
232                                throws InconsistentRequestException, InvalidParameterValueException {
233    
234            // SERVICE
235            checkServiceParameter( kvp );
236    
237            // ID (deegree specific)
238            String id = kvp.get( "ID" );
239    
240            // VERSION
241            String version = checkVersionParameter( kvp );
242    
243            // OUTPUTFORMAT
244            String outputFormat = getParam( "OUTPUTFORMAT", kvp, FORMAT_GML3 );
245    
246            // RESULTTYPE
247            RESULT_TYPE resultType = RESULT_TYPE.RESULTS;
248            String resultTypeString = kvp.get( "RESULTTYPE" );
249            if ( "hits".equals( resultTypeString ) ) {
250                resultType = RESULT_TYPE.HITS;
251            }
252    
253            // FEATUREVERSION
254            String featureVersion = kvp.get( "FEATUREVERSION" );
255    
256            // MAXFEATURES
257            String maxFeaturesString = kvp.get( "MAXFEATURES" );
258            // -1: fetch all features
259            int maxFeatures = -1;
260            if ( maxFeaturesString != null ) {
261                try {
262                    maxFeatures = Integer.parseInt( maxFeaturesString );
263                    if ( maxFeatures < 1 ) {
264                        throw new NumberFormatException();
265                    }
266                } catch ( NumberFormatException e ) {
267                    LOG.logError( e.getMessage(), e );
268                    String msg = Messages.getMessage( "WFS_PARAMETER_INVALID_INT", maxFeaturesString, "MAXFEATURES" );
269                    throw new InvalidParameterValueException( msg );
270                }
271            }
272    
273            // STARTPOSITION (deegree specific)
274            String startPosString = getParam( "STARTPOSITION", kvp, "1" );
275            int startPosition = 1;
276            try {
277                startPosition = Integer.parseInt( startPosString );
278                if ( startPosition < 1 ) {
279                    throw new NumberFormatException();
280                }
281            } catch ( NumberFormatException e ) {
282                LOG.logError( e.getMessage(), e );
283                String msg = Messages.getMessage( "WFS_PARAMETER_INVALID_INT", startPosString, "STARTPOSITION" );
284                throw new InvalidParameterValueException( msg );
285            }
286    
287            // SRSNAME
288            String srsName = kvp.get( "SRSNAME" );
289    
290            // TYPENAME
291            QualifiedName[] typeNames = extractTypeNames( kvp );
292            if ( typeNames == null ) {
293                // no TYPENAME parameter -> FEATUREID must be present
294                String featureId = kvp.get( "FEATUREID" );
295                if ( featureId != null ) {
296                    String msg = Messages.getMessage( "WFS_FEATUREID_PARAM_UNSUPPORTED" );
297                    throw new InvalidParameterValueException( msg );
298                }
299                String msg = Messages.getMessage( "WFS_TYPENAME+FID_PARAMS_MISSING" );
300                throw new InvalidParameterValueException( msg );
301            }
302    
303            // BBOX
304            Filter bboxFilter = extractBBOXFilter( kvp );
305    
306            // FILTER (prequisite: TYPENAME)
307            Map<QualifiedName, Filter> filterMap = extractFilters( kvp, typeNames );
308            if ( bboxFilter != null && filterMap.size() > 0 ) {
309                String msg = Messages.getMessage( "WFS_BBOX_FILTER_INVALID" );
310                throw new InvalidParameterValueException( msg );
311            }
312    
313            // PROPERTYNAME
314            Map<QualifiedName, PropertyPath[]> propertyNameMap = extractPropNames( kvp, typeNames );
315    
316            // SORTBY
317            SortProperty[] sortProperties = null;
318    
319            // TRAVERSEXLINKDEPTH
320            int traverseXLinkDepth = -1;
321    
322            // TRAVERSEXLINKEXPIRY
323            int traverseXLinkExpiry = -1;
324    
325            // build a Query instance for each requested feature type (later also for each featureid...)
326            Query[] queries = new Query[typeNames.length];
327            for ( int i = 0; i < queries.length; i++ ) {
328                QualifiedName ftName = typeNames[i];
329                PropertyPath[] properties = propertyNameMap.get( ftName );
330                Filter filter;
331                if ( bboxFilter != null ) {
332                    filter = bboxFilter;
333                } else {
334                    filter = filterMap.get( ftName );
335                }
336                QualifiedName[] ftNames = new QualifiedName[] { ftName };
337                queries[i] = new Query( properties, null, sortProperties, null, featureVersion, ftNames, null, srsName,
338                                        filter, resultType, maxFeatures, startPosition );
339            }
340    
341            // build a GetFeature request that contains all queries
342            GetFeature request = new GetFeature( version, id, null, resultType, outputFormat, maxFeatures, startPosition,
343                                                 traverseXLinkDepth, traverseXLinkExpiry, queries, kvp );
344            return request;
345        }
346    
347        /**
348         * Extracts the PROPERTYNAME parameter and assigns them to the requested type names.
349         * 
350         * @param kvp
351         * @param typeNames
352         * @return map with the assignments of type names to property names
353         * @throws InvalidParameterValueException
354         */
355        protected static Map<QualifiedName, PropertyPath[]> extractPropNames( Map<String, String> kvp,
356                                                                              QualifiedName[] typeNames )
357                                throws InvalidParameterValueException {
358            Map<QualifiedName, PropertyPath[]> propMap = new HashMap<QualifiedName, PropertyPath[]>();
359            String propNameString = kvp.get( "PROPERTYNAME" );
360            if ( propNameString != null ) {
361                String[] propNameLists = propNameString.split( "\\)" );
362                if ( propNameLists.length != typeNames.length ) {
363                    String msg = Messages.getMessage( "WFS_PROPNAME_PARAM_WRONG_COUNT", Integer.toString( propNameLists.length ),
364                                                      Integer.toString( typeNames.length ) );
365                    throw new InvalidParameterValueException( msg );
366                }
367                NamespaceContext nsContext = extractNamespaceParameter( kvp );
368                for ( int i = 0; i < propNameLists.length; i++ ) {
369                    String propNameList = propNameLists[i];
370                    if ( propNameList.startsWith( "(" ) ) {
371                        propNameList = propNameList.substring( 1 );
372                    }
373                    String[] propNames = propNameList.split( "," );
374                    PropertyPath[] paths = new PropertyPath[propNames.length];
375                    for ( int j = 0; j < propNames.length; j++ ) {
376                        PropertyPath path = transformToPropertyPath( propNames[j], nsContext );
377                        paths[j] = ( path );
378                    }
379                    propMap.put( typeNames[i], paths );
380                }
381            }
382            return propMap;
383        }
384    
385        /**
386         * Transforms the given property name to a (qualified) <code>PropertyPath</code> object by
387         * using the specified namespace bindings.
388         * 
389         * @param propName
390         * @param nsContext
391         * @return (qualified) <code>PropertyPath</code> object
392         * @throws InvalidParameterValueException
393         */
394        private static PropertyPath transformToPropertyPath( String propName, NamespaceContext nsContext )
395                                throws InvalidParameterValueException {
396            String[] steps = propName.split( "/" );
397            List<PropertyPathStep> propertyPathSteps = new ArrayList<PropertyPathStep>( steps.length );
398    
399            for ( int i = 0; i < steps.length; i++ ) {
400                PropertyPathStep propertyStep = null;
401                QualifiedName propertyName = null;
402                String step = steps[i];
403                boolean isAttribute = false;
404                boolean isIndexed = false;
405                int selectedIndex = -1;
406    
407                // check if step begins with '@' -> must be the final step then
408                if ( step.startsWith( "@" ) ) {
409                    if ( i != steps.length - 1 ) {
410                        String msg = "PropertyName '" + propName + "' is illegal: the attribute specifier may only "
411                                     + "be used for the final step.";
412                        throw new InvalidParameterValueException( msg );
413                    }
414                    step = step.substring( 1 );
415                    isAttribute = true;
416                }
417    
418                // check if the step ends with brackets ([...])
419                if ( step.endsWith( "]" ) ) {
420                    if ( isAttribute ) {
421                        String msg = "PropertyName '" + propName
422                                     + "' is illegal: if the attribute specifier ('@') is used, "
423                                     + "index selection ('[...']) is not possible.";
424                        throw new InvalidParameterValueException( msg );
425                    }
426                    int bracketPos = step.indexOf( '[' );
427                    if ( bracketPos < 0 ) {
428                        String msg = "PropertyName '" + propName + "' is illegal. No opening brackets found for step '"
429                                     + step + "'.";
430                        throw new InvalidParameterValueException( msg );
431                    }
432                    try {
433                        selectedIndex = Integer.parseInt( step.substring( bracketPos + 1, step.length() - 1 ) );
434                    } catch ( NumberFormatException e ) {
435                        LOG.logError( e.getMessage(), e );
436                        String msg = "PropertyName '" + propName + "' is illegal. Specified index '"
437                                     + step.substring( bracketPos + 1, step.length() - 1 ) + "' is not a number.";
438                        throw new InvalidParameterValueException( msg );
439                    }
440                    step = step.substring( 0, bracketPos );
441                    isIndexed = true;
442                }
443    
444                // determine namespace prefix and binding (if any)
445                int colonPos = step.indexOf( ':' );
446                String prefix = "";
447                String localName = step;
448                if ( colonPos > 0 ) {
449                    prefix = step.substring( 0, colonPos );
450                    localName = step.substring( colonPos + 1 );
451                }
452                URI nsURI = nsContext.getURI( prefix );
453                if ( nsURI == null && prefix.length() > 0 ) {
454                    String msg = "PropertyName '" + propName + "' uses an unbound namespace prefix: " + prefix;
455                    throw new InvalidParameterValueException( msg );
456                }
457                propertyName = new QualifiedName( prefix, localName, nsURI );
458    
459                if ( isAttribute ) {
460                    propertyStep = PropertyPathFactory.createAttributePropertyPathStep( propertyName );
461                } else if ( isIndexed ) {
462                    propertyStep = PropertyPathFactory.createPropertyPathStep( propertyName, selectedIndex );
463                } else {
464                    propertyStep = PropertyPathFactory.createPropertyPathStep( propertyName );
465                }
466                propertyPathSteps.add( propertyStep );
467            }
468            return PropertyPathFactory.createPropertyPath( propertyPathSteps );
469        }
470    
471        /**
472         * Returns the output format.
473         * <p>
474         * The outputFormat attribute defines the format to use to generate the result set. Vendor
475         * specific formats, declared in the capabilities document are possible. The WFS-specs implies
476         * GML as default output format.
477         * 
478         * @return the output format.
479         */
480        public String getOutputFormat() {
481            return this.outputFormat;
482        }
483    
484        /**
485         * The query defines which feature type to query, what properties to retrieve and what
486         * constraints (spatial and non-spatial) to apply to those properties.
487         * <p>
488         * only used for xml-coded requests
489         * 
490         * @return contained queries
491         */
492        public Query[] getQuery() {
493            return queries.toArray( new Query[queries.size()] );
494        }
495    
496        /**
497         * sets the <Query>
498         * 
499         * @param query
500         */
501        public void setQueries( Query[] query ) {
502            this.queries = new ArrayList<Query>( query.length );
503            if ( query != null ) {
504                for ( int i = 0; i < query.length; i++ ) {
505                    this.queries.add( query[i] );
506                }
507            }
508        }
509    
510        /**
511         * The optional maxFeatures attribute can be used to limit the number of features that a
512         * GetFeature request retrieves. Once the maxFeatures limit is reached, the result set is
513         * truncated at that point. If not limit is set -1 will be returned.
514         * 
515         * @return number of feature to fetch, -1 if no limit is set
516         */
517        public int getMaxFeatures() {
518            return maxFeatures;
519        }
520    
521        /**
522         * @see #getMaxFeatures()
523         * @param max
524         */
525        public void setMaxFeatures( int max ) {
526            this.maxFeatures = max;
527            for ( int i = 0; i < queries.size(); i++ ) {
528                queries.get( i ).setMaxFeatures( max );
529            }
530        }
531    
532        /**
533         * The startPosition parameter identifies the first result set entry to be returned specified
534         * the default is the first record. If not startposition is set 0 will be returned
535         * 
536         * @return the first result set entry to be returned
537         */
538        public int getStartPosition() {
539            return startPosition;
540        }
541    
542        /**
543         * Returns the desired result type of the GetFeature operation. Possible values are 'results'
544         * and 'hits'.
545         * 
546         * @return the desired result type
547         */
548        public RESULT_TYPE getResultType() {
549            return this.resultType;
550        }
551    
552        /**
553         * The optional traverseXLinkDepth attribute indicates the depth to which nested property XLink
554         * linking element locator attribute (href) XLinks in all properties of the selected feature(s)
555         * are traversed and resolved if possible. A value of "1" indicates that one linking element
556         * locator attribute (href) XLink will be traversed and the referenced element returned if
557         * possible, but nested property XLink linking element locator attribute (href) XLinks in the
558         * returned element are not traversed. A value of "*" indicates that all nested property XLink
559         * linking element locator attribute (href) XLinks will be traversed and the referenced elements
560         * returned if possible. The range of valid values for this attribute consists of positive
561         * integers plus "*".
562         * 
563         * @return the depth to which nested property XLinks are traversed and resolved
564         */
565        public int getTraverseXLinkDepth() {
566            return traverseXLinkDepth;
567        }
568    
569        /**
570         * The traverseXLinkExpiry attribute is specified in minutes. It indicates how long a Web
571         * Feature Service should wait to receive a response to a nested GetGmlObject request. If no
572         * traverseXLinkExpiry attribute is present for a GetGmlObject request, the WFS wait time is
573         * implementation dependent.
574         * 
575         * @return how long to wait to receive a response to a nested GetGmlObject request
576         */
577        public int getTraverseXLinkExpiry() {
578            return traverseXLinkExpiry;
579        }
580    
581        @Override
582        public String toString() {
583            String ret = null;
584            ret = "WFSGetFeatureRequest: { \n ";
585            ret += "outputFormat = " + outputFormat + "\n";
586            ret += ( "handle = " + getHandle() + "\n" );
587            ret += ( "query = " + queries + "\n" );
588            ret += "}\n";
589            return ret;
590        }
591    }