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 }