//$Header: /home/deegree/jail/deegreerepository/deegree/src/org/deegree/ogcwebservices/wfs/operation/GetFeature.java,v 1.61 2006/11/16 08:53:21 mschneider Exp $
/*---------------- FILE HEADER ------------------------------------------
This file is part of deegree.
Copyright (C) 2001-2006 by:
EXSE, Department of Geography, University of Bonn
http://www.giub.uni-bonn.de/deegree/
lat/lon GmbH
http://www.lat-lon.de
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Contact:
Andreas Poth
lat/lon GmbH
Aennchenstraße 19
53177 Bonn
Germany
E-Mail: poth@lat-lon.de
Prof. Dr. Klaus Greve
Department of Geography
University of Bonn
Meckenheimer Allee 166
53115 Bonn
Germany
E-Mail: greve@giub.uni-bonn.de
---------------------------------------------------------------------------*/
package org.deegree.ogcwebservices.wfs.operation;
import java.io.StringReader;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.deegree.datatypes.QualifiedName;
import org.deegree.framework.log.ILogger;
import org.deegree.framework.log.LoggerFactory;
import org.deegree.framework.util.KVP2Map;
import org.deegree.framework.xml.NamespaceContext;
import org.deegree.framework.xml.XMLTools;
import org.deegree.i18n.Messages;
import org.deegree.model.filterencoding.AbstractFilter;
import org.deegree.model.filterencoding.ComplexFilter;
import org.deegree.model.filterencoding.Filter;
import org.deegree.ogcbase.PropertyPath;
import org.deegree.ogcbase.PropertyPathFactory;
import org.deegree.ogcbase.PropertyPathStep;
import org.deegree.ogcbase.SortProperty;
import org.deegree.ogcwebservices.InconsistentRequestException;
import org.deegree.ogcwebservices.InvalidParameterValueException;
import org.deegree.ogcwebservices.OGCWebServiceException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* Represents a <code>GetFeature</code> request to a web feature service.
* <p>
* The GetFeature operation allows the retrieval of features from a web feature service.
* A GetFeature request is processed by a WFS and when the value of the outputFormat attribute is
* set to text/gml; subtype=gml/3.1.1, a GML instance document, containing the result set, is
* returned to the client.
*
* @author <a href="mailto:poth@lat-lon.de">Andreas Poth </a>
* @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
* @author last edited by: $Author: mschneider $
*
* @version $Revision: 1.61 $, $Date: 2006/11/16 08:53:21 $
*/
public class GetFeature extends AbstractWFSRequest {
private static final ILogger LOG = LoggerFactory.getLogger( GetFeature.class );
private static final long serialVersionUID = 8885456550385433051L;
/** Serialized java object format (deegree specific extension) **/
public static final String FORMAT_FEATURECOLLECTION = "FEATURECOLLECTION";
/**
* Known result types.
*/
public static enum RESULT_TYPE {
/** A full response should be generated. */
RESULTS,
/** Only a count of the number of features should be returned. */
HITS
}
private RESULT_TYPE resultType = RESULT_TYPE.RESULTS;
private String outputFormat;
private int maxFeatures;
private int traverseXLinkDepth;
private int traverseXLinkExpiry;
private List<Query> queries;
// deegree specific extension, default: 1 (start at first feature)
private int startPosition;
/**
* Creates a new <code>GetFeature</code> instance.
*
* @param version
* request version
* @param id
* id of the request
* @param handle
* @param resultType
* desired result type (results | hits)
* @param outputFormat
* requested result format
* @param maxFeatures
* @param startPosition
* deegree specific parameter defining where to start considering features
* @param traverseXLinkDepth
* @param traverseXLinkExpiry
* @param queries
* @param vendorSpecificParam
*/
GetFeature( String version, String id, String handle, RESULT_TYPE resultType,
String outputFormat, int maxFeatures, int startPosition, int traverseXLinkDepth,
int traverseXLinkExpiry, Query[] queries, Map<String, String> vendorSpecificParam ) {
super( version, id, handle, vendorSpecificParam );
this.setQueries( queries );
this.outputFormat = outputFormat;
this.maxFeatures = maxFeatures;
this.startPosition = startPosition;
this.resultType = resultType;
this.traverseXLinkDepth = traverseXLinkDepth;
this.traverseXLinkExpiry = traverseXLinkExpiry;
}
/**
* Creates a new <code>GetFeature</code> instance from the given parameters.
*
* @param version
* request version
* @param id
* id of the request
* @param resultType
* desired result type (results | hits)
* @param outputFormat
* requested result format
* @param handle
* @param maxFeatures
* default = -1 (all features)
* @param startPosition
* default = 0 (starting at the first feature)
* @param traverseXLinkDepth
* @param traverseXLinkExpiry
* @param queries
* a set of Query objects that describes the query to perform
* @return new <code>GetFeature</code> request
*/
public static GetFeature create( String version, String id, RESULT_TYPE resultType,
String outputFormat, String handle, int maxFeatures,
int startPosition, int traverseXLinkDepth,
int traverseXLinkExpiry, Query[] queries ) {
return new GetFeature( version, id, handle, resultType, outputFormat, maxFeatures,
startPosition, traverseXLinkDepth, traverseXLinkExpiry, queries,
null );
}
/**
* Creates a new <code>GetFeature</code> instance from a document that contains the DOM
* representation of the request.
*
* @param id
* of the request
* @param root
* element that contains the DOM representation of the request
* @return new <code>GetFeature</code> request
* @throws OGCWebServiceException
*/
public static GetFeature create( String id, Element root )
throws OGCWebServiceException {
GetFeatureDocument doc = new GetFeatureDocument();
doc.setRootElement( root );
GetFeature request;
try {
request = doc.parse( id );
} catch ( Exception e ) {
LOG.logError( e.getMessage(), e );
throw new OGCWebServiceException( "GetFeature", e.getMessage() );
}
return request;
}
/**
* Creates a new <code>GetFeature</code> instance from the given key-value pair encoded
* request.
*
* @param id
* request identifier
* @param request
* @return new <code>GetFeature</code> request
* @throws InvalidParameterValueException
* @throws InconsistentRequestException
*/
public static GetFeature create( String id, String request )
throws InconsistentRequestException, InvalidParameterValueException {
Map<String, String> map = KVP2Map.toMap( request );
map.put( "ID", id );
return create( map );
}
/**
* Creates a new <code>GetFeature</code> request from the given map.
*
* @param kvp
* key-value pairs, keys have to be uppercase
* @return new <code>GetFeature</code> request
* @throws InconsistentRequestException
* @throws InvalidParameterValueException
*/
public static GetFeature create( Map<String, String> kvp )
throws InconsistentRequestException, InvalidParameterValueException {
// SERVICE
checkServiceParameter( kvp );
// ID (deegree specific)
String id = kvp.get( "ID" );
// VERSION
String version = checkVersionParameter( kvp );
// OUTPUTFORMAT
String outputFormat = getParam( "OUTPUTFORMAT", kvp, FORMAT_GML3 );
// RESULTTYPE
RESULT_TYPE resultType = RESULT_TYPE.RESULTS;
String resultTypeString = kvp.get( "RESULTTYPE" );
if ( "hits".equals( resultTypeString ) ) {
resultType = RESULT_TYPE.HITS;
}
// FEATUREVERSION
String featureVersion = kvp.get( "FEATUREVERSION" );
// MAXFEATURES
String maxFeaturesString = kvp.get( "MAXFEATURES" );
// -1: fetch all features
int maxFeatures = -1;
if ( maxFeaturesString != null ) {
try {
maxFeatures = Integer.parseInt( maxFeaturesString );
if ( maxFeatures < 1 ) {
throw new NumberFormatException();
}
} catch ( NumberFormatException e ) {
LOG.logError( e.getMessage(), e );
String msg = Messages.getMessage( "WFS_PARAMETER_INVALID_INT", maxFeaturesString,
"MAXFEATURES" );
throw new InvalidParameterValueException( msg );
}
}
// STARTPOSITION (deegree specific)
String startPosString = getParam( "STARTPOSITION", kvp, "1" );
int startPosition = 1;
try {
startPosition = Integer.parseInt( startPosString );
if ( startPosition < 1 ) {
throw new NumberFormatException();
}
} catch ( NumberFormatException e ) {
LOG.logError( e.getMessage(), e );
String msg = Messages.getMessage( "WFS_PARAMETER_INVALID_INT", startPosString,
"STARTPOSITION" );
throw new InvalidParameterValueException( msg );
}
// SRSNAME
String srsName = kvp.get( "SRSNAME" );
// TYPENAME
QualifiedName[] typeNames = extractTypeNames( kvp );
if ( typeNames == null ) {
// no TYPENAME parameter -> FEATUREID must be present
String featureId = kvp.get( "FEATUREID" );
if ( featureId != null ) {
String msg = Messages.getMessage( "WFS_FEATUREID_PARAM_UNSUPPORTED" );
throw new InvalidParameterValueException( msg );
}
String msg = Messages.getMessage( "WFS_TYPENAME+FID_PARAMS_MISSING" );
throw new InvalidParameterValueException( msg );
}
// BBOX
Filter bboxFilter = extractBBOXFilter( kvp );
// FILTER (prequisite: TYPENAME)
Map<QualifiedName, Filter> filterMap = extractFilters( kvp, typeNames );
if ( bboxFilter != null && filterMap.size() > 0 ) {
String msg = Messages.getMessage( "WFS_BBOX+FILTER_INVALID" );
throw new InvalidParameterValueException( msg );
}
// PROPERTYNAME
Map<QualifiedName, PropertyPath[]> propertyNameMap = extractPropNames( kvp, typeNames );
// SORTBY
SortProperty[] sortProperties = null;
// TRAVERSEXLINKDEPTH
int traverseXLinkDepth = -1;
// TRAVERSEXLINKEXPIRY
int traverseXLinkExpiry = -1;
// build a Query instance for each requested feature type (later also for each featureid...)
Query[] queries = new Query[typeNames.length];
for ( int i = 0; i < queries.length; i++ ) {
QualifiedName ftName = typeNames[i];
PropertyPath[] properties = propertyNameMap.get( ftName );
Filter filter;
if ( bboxFilter != null ) {
filter = bboxFilter;
} else {
filter = filterMap.get( ftName );
}
QualifiedName[] ftNames = new QualifiedName[] { ftName };
queries[i] = new Query( properties, null, sortProperties, null, featureVersion,
ftNames, srsName, filter, resultType, maxFeatures,
startPosition );
}
// build a GetFeature request that contains all queries
GetFeature request = new GetFeature( version, id, null, resultType, outputFormat,
maxFeatures, startPosition, traverseXLinkDepth,
traverseXLinkExpiry, queries, null );
return request;
}
/**
* Extracts a <code>Filter</code> from the BBOX parameter.
*
* TODO handle other dimension count and crs
*
* @param model
* @return filter representing the BBOX parameter (null, if no BBOX parameter specified)
* @throws InvalidParameterValueException
*/
private static Filter extractBBOXFilter( Map<String, String> model )
throws InvalidParameterValueException {
ComplexFilter filter = null;
String bboxString = model.get( "BBOX" );
if ( bboxString != null ) {
String msg = "Parameter 'BBOX' is currently not supported. Please use the 'FILTER' parameter instead.";
throw new InvalidParameterValueException( msg );
// String[] parts = bboxString.split( "," );
// double[] coords = new double[4];
//
// if ( parts.length > 5 ) {
// String msg = Messages.getString( "WFS_BBOX_PARAM_WRONG_COORD_COUNT" );
// throw new InvalidParameterValueException( msg );
// }
//
// for ( int i = 0; i < coords.length; i++ ) {
// try {
// coords[i] = Double.parseDouble( parts[i] );
// } catch ( NumberFormatException e ) {
// String msg = Messages.getMessage( "WFS_BBOX_PARAM_COORD_INVALID", coords[i] );
// throw new InvalidParameterValueException( msg );
// }
// }
//
// // build filter
// Envelope bbox = GeometryFactory.createEnvelope( coords[0], coords[1], coords[2],
// coords[3], null );
// Surface surface;
// try {
// surface = GeometryFactory.createSurface( bbox, null );
// } catch ( GeometryException e ) {
// String msg = Messages.getMessage( "WFS_BBOX_PARAM_BBOX_INVALID", e.getMessage() );
// throw new InvalidParameterValueException( msg );
// }
// Operation op = new SpatialOperation( OperationDefines.BBOX, null, surface );
// filter = new ComplexFilter( op );
}
return filter;
}
/**
* Extracts the FILTER parameter and assigns them to the requested type names.
* <p>
* This is necessary, because it is allowed to specify a filter for each requested feature
* type.
*
* @param kvp
* @param typeNames
* @return map with the assignments of type names to filters
* @throws InvalidParameterValueException
*/
private static Map<QualifiedName, Filter> extractFilters( Map<String, String> kvp,
QualifiedName[] typeNames )
throws InvalidParameterValueException {
Map<QualifiedName, Filter> filterMap = new HashMap<QualifiedName, Filter>();
String filterString = kvp.get( "FILTER" );
if ( filterString != null ) {
String[] filterStrings = filterString.split( "\\)" );
if ( filterStrings.length != typeNames.length ) {
String msg = Messages.getMessage( "WFS_FILTER_PARAM_WRONG_COUNT",
filterStrings.length, typeNames.length );
throw new InvalidParameterValueException( msg );
}
for ( int i = 0; i < filterStrings.length; i++ ) {
// remove possible leading parenthesis
if ( filterStrings[i].startsWith( "(" ) ) {
filterStrings[i] = filterStrings[i].substring( 1 );
}
Document doc;
try {
doc = XMLTools.parse( new StringReader( filterStrings[i] ) );
Filter filter = AbstractFilter.buildFromDOM( doc.getDocumentElement() );
filterMap.put( typeNames[i], filter );
} catch ( Exception e ) {
LOG.logError( e.getMessage(), e );
String msg = Messages.getMessage( "WFS_FILTER_PARAM_PARSING", e.getMessage() );
throw new InvalidParameterValueException( msg );
}
}
}
return filterMap;
}
/**
* Extracts the PROPERTYNAME parameter and assigns them to the requested type names.
*
* @param kvp
* @param typeNames
* @return map with the assignments of type names to property names
* @throws InvalidParameterValueException
*/
private static Map<QualifiedName, PropertyPath[]> extractPropNames( Map<String, String> kvp,
QualifiedName[] typeNames )
throws InvalidParameterValueException {
Map<QualifiedName, PropertyPath[]> propMap = new HashMap<QualifiedName, PropertyPath[]>();
String propNameString = kvp.get( "PROPERTYNAME" );
if ( propNameString != null ) {
String[] propNameLists = propNameString.split( "\\)" );
if ( propNameLists.length != typeNames.length ) {
String msg = Messages.getMessage( "WFS_PROPNAME_PARAM_WRONG_COUNT",
propNameLists.length, typeNames.length );
throw new InvalidParameterValueException( msg );
}
NamespaceContext nsContext = extractNamespaceParameter( kvp );
for ( int i = 0; i < propNameLists.length; i++ ) {
String propNameList = propNameLists[i];
if ( propNameList.startsWith( "(" ) ) {
propNameList = propNameList.substring( 1 );
}
String[] propNames = propNameList.split( "," );
PropertyPath[] paths = new PropertyPath[propNames.length];
for ( int j = 0; j < propNames.length; j++ ) {
PropertyPath path = transformToPropertyPath( propNames[j], nsContext );
paths[j] = ( path );
}
propMap.put( typeNames[i], paths );
}
}
return propMap;
}
/**
* Transforms the given property name to a (qualified) <code>PropertyPath</code> object by
* using the specified namespace bindings.
*
* @param propName
* @param nsContext
* @return (qualified) <code>PropertyPath</code> object
* @throws InvalidParameterValueException
*/
private static PropertyPath transformToPropertyPath( String propName, NamespaceContext nsContext )
throws InvalidParameterValueException {
String[] steps = propName.split( "/" );
List<PropertyPathStep> propertyPathSteps = new ArrayList<PropertyPathStep>( steps.length );
for ( int i = 0; i < steps.length; i++ ) {
PropertyPathStep propertyStep = null;
QualifiedName propertyName = null;
String step = steps[i];
boolean isAttribute = false;
boolean isIndexed = false;
int selectedIndex = -1;
// check if step begins with '@' -> must be the final step then
if ( step.startsWith( "@" ) ) {
if ( i != steps.length - 1 ) {
String msg = "PropertyName '" + propName
+ "' is illegal: the attribute specifier may only "
+ "be used for the final step.";
throw new InvalidParameterValueException( msg );
}
step = step.substring( 1 );
isAttribute = true;
}
// check if the step ends with brackets ([...])
if ( step.endsWith( "]" ) ) {
if ( isAttribute ) {
String msg = "PropertyName '" + propName
+ "' is illegal: if the attribute specifier ('@') is used, "
+ "index selection ('[...']) is not possible.";
throw new InvalidParameterValueException( msg );
}
int bracketPos = step.indexOf( '[' );
if ( bracketPos < 0 ) {
String msg = "PropertyName '" + propName
+ "' is illegal. No opening brackets found for step '" + step
+ "'.";
throw new InvalidParameterValueException( msg );
}
try {
selectedIndex = Integer.parseInt( step.substring( bracketPos + 1,
step.length() - 1 ) );
} catch ( NumberFormatException e ) {
LOG.logError( e.getMessage(), e );
String msg = "PropertyName '" + propName + "' is illegal. Specified index '"
+ step.substring( bracketPos + 1, step.length() - 1 )
+ "' is not a number.";
throw new InvalidParameterValueException( msg );
}
step = step.substring( 0, bracketPos );
isIndexed = true;
}
// determine namespace prefix and binding (if any)
int colonPos = step.indexOf( ':' );
String prefix = "";
String localName = step;
if ( colonPos > 0 ) {
prefix = step.substring( 0, colonPos );
localName = step.substring( colonPos + 1 );
}
URI nsURI = nsContext.getURI( prefix );
if ( nsURI == null && prefix.length() > 0 ) {
String msg = "PropertyName '" + propName + "' uses an unbound namespace prefix: "
+ prefix;
throw new InvalidParameterValueException( msg );
}
propertyName = new QualifiedName( prefix, localName, nsURI );
if ( isAttribute ) {
propertyStep = PropertyPathFactory.createAttributePropertyPathStep( propertyName );
} else if ( isIndexed ) {
propertyStep = PropertyPathFactory.createPropertyPathStep( propertyName,
selectedIndex );
} else {
propertyStep = PropertyPathFactory.createPropertyPathStep( propertyName );
}
propertyPathSteps.add( propertyStep );
}
return PropertyPathFactory.createPropertyPath( propertyPathSteps );
}
/**
* Returns the output format.
* <p>
* The outputFormat attribute defines the format to use to generate the result set. Vendor
* specific formats, declared in the capabilities document are possible. The WFS-specs implies
* GML as default output format.
*
* @return the output format.
*/
public String getOutputFormat() {
return this.outputFormat;
}
/**
* The query defines which feature type to query, what properties to retrieve and what
* constraints (spatial and non-spatial) to apply to those properties.
* <p>
* only used for xml-coded requests
*
* @return contained queries
*/
public Query[] getQuery() {
return queries.toArray( new Query[queries.size()] );
}
/**
* sets the <Query>
*/
private void setQueries( Query[] query ) {
this.queries = new ArrayList<Query>( query.length );
if ( query != null ) {
for ( int i = 0; i < query.length; i++ ) {
this.queries.add( query[i] );
}
}
}
/**
* The optional maxFeatures attribute can be used to limit the number of features that a
* GetFeature request retrieves. Once the maxFeatures limit is reached, the result set is
* truncated at that point. If not limit is set -1 will be returned.
*
* @return number of feature to fetch, -1 if no limit is set
*/
public int getMaxFeatures() {
return maxFeatures;
}
/**
* @see #getMaxFeatures()
* @param max
*/
public void setMaxFeatures( int max ) {
this.maxFeatures = max;
}
/**
* The startPosition parameter identifies the first result set entry to be returned specified
* the default is the first record. If not startposition is set 0 will be returned
*
* @return the first result set entry to be returned
*/
public int getStartPosition() {
return startPosition;
}
/**
* Returns the desired result type of the GetFeature operation. Possible values are 'results'
* and 'hits'.
*
* @return the desired result type
*/
public RESULT_TYPE getResultType() {
return this.resultType;
}
/**
* The optional traverseXLinkDepth attribute indicates the depth to which nested property XLink
* linking element locator attribute (href) XLinks in all properties of the selected feature(s)
* are traversed and resolved if possible. A value of "1" indicates that one linking element
* locator attribute (href) XLink will be traversed and the referenced element returned if
* possible, but nested property XLink linking element locator attribute (href) XLinks in the
* returned element are not traversed. A value of "*" indicates that all nested property XLink
* linking element locator attribute (href) XLinks will be traversed and the referenced elements
* returned if possible. The range of valid values for this attribute consists of positive
* integers plus "*".
*
* @return the depth to which nested property XLinks are traversed and resolved
*/
public int getTraverseXLinkDepth() {
return traverseXLinkDepth;
}
/**
* The traverseXLinkExpiry attribute is specified in minutes. It indicates how long a Web
* Feature Service should wait to receive a response to a nested GetGmlObject request. If no
* traverseXLinkExpiry attribute is present for a GetGmlObject request, the WFS wait time is
* implementation dependent.
*
* @return how long to wait to receive a response to a nested GetGmlObject request
*/
public int getTraverseXLinkExpiry() {
return traverseXLinkExpiry;
}
/**
* Returns a string representation of the object.
*
* @return a string representation of the object
*/
@Override
public String toString() {
String ret = null;
ret = "WFSGetFeatureRequest: { \n ";
ret += "outputFormat = " + outputFormat + "\n";
ret += ( "handle = " + getHandle() + "\n" );
ret += ( "query = " + queries + "\n" );
ret += "}\n";
return ret;
}
}
/***************************************************************************************************
* Changes to this class. What the people haven been up to:
*
* Revision 1.25.2.4 2005/11/10 15:24:44 mschneider Refactoring: use "PropertyPath" in
* "org.deegree.model.filterencoding.PropertyName".
*
* Revision 1.25.2.3 2005/11/09 18:02:29 mschneider More refactoring. Revision 1.25.2.2 2005/11/07
* 16:45:08 deshmukh NodeList to List
*
* Revision 1.25.2.1 2005/11/07 15:38:04 mschneider Refactoring: use NamespaceContext instead of
* Node for namespace bindings. Revision 1.25 2005/10/04 15:55:05 poth no message
*
* Revision 1.24 2005/09/27 19:53:19 poth no message
*
* Revision 1.23 2005/09/22 12:05:03 poth no message
*
* Revision 1.22 2005/08/29 17:19:10 mschneider PropertyNames are represented by Strings instead of
* QualifiedNames (actually they are XPath-expressions).
*
* Revision 1.21 2005/08/26 21:11:29 poth no message
*
* Revision 1.5 2005/08/24 16:12:55 mschneider Renamed GenericName to QualifiedName.
*
* Revision 1.4 2005/07/22 15:17:54 mschneider Added constants for output formats.
*
* Revision 1.3 2005/04/23 15:32:05 poth no message
*
* Revision 1.2 2005/04/21 12:41:00 mschneider Changed error message in case of broken filter
* expressions.
*
* Revision 1.1 2005/04/05 08:03:28 poth no message
*
* Revision 1.19 2005/03/09 11:52:59 mschneider *** empty log message ***
*
* Revision 1.17 2005/03/01 16:20:15 poth no message
*
* Revision 1.16 2005/03/01 14:39:08 mschneider *** empty log message ***
*
* Revision 1.1 2004/06/07 13:38:34 tf code adapted to wfs1 refactoring
*
* Revision 1.11 2004/02/23 07:47:51 poth no message
*
**************************************************************************************************/