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 }