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 }