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