Java tutorial
//$HeadURL$ /*---------------------------------------------------------------------------- This file is part of deegree, http://deegree.org/ Copyright (C) 2001-2009 by: Department of Geography, University of Bonn and lat/lon GmbH 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 information: lat/lon GmbH Aennchenstr. 19, 53177 Bonn Germany http://lat-lon.de/ Department of Geography, University of Bonn Prof. Dr. Klaus Greve Postfach 1147, 53001 Bonn Germany http://www.geographie.uni-bonn.de/deegree/ e-mail: info@deegree.org ----------------------------------------------------------------------------*/ package org.deegree.services.wfs.query; import static javax.xml.XMLConstants.DEFAULT_NS_PREFIX; import static org.deegree.commons.ows.exception.OWSException.INVALID_PARAMETER_VALUE; import static org.deegree.commons.ows.exception.OWSException.MISSING_PARAMETER_VALUE; import static org.deegree.services.wfs.query.StoredQueryHandler.GET_FEATURE_BY_ID; import static org.deegree.services.wfs.query.StoredQueryHandler.GET_FEATURE_BY_TYPE; import java.io.IOException; import java.io.StringReader; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.regex.Pattern; import javax.xml.namespace.QName; import org.apache.axiom.om.OMElement; import org.apache.commons.io.IOUtils; import org.deegree.commons.ows.exception.OWSException; import org.deegree.commons.tom.gml.property.PropertyType; import org.deegree.commons.utils.Pair; import org.deegree.commons.utils.QNameUtils; import org.deegree.commons.xml.NamespaceBindings; import org.deegree.cs.CRSUtils; import org.deegree.cs.coordinatesystems.ICRS; import org.deegree.cs.exceptions.TransformationException; import org.deegree.cs.exceptions.UnknownCRSException; import org.deegree.feature.persistence.FeatureStore; import org.deegree.feature.persistence.query.Query; import org.deegree.feature.types.FeatureType; import org.deegree.filter.Filter; import org.deegree.filter.Filters; import org.deegree.filter.IdFilter; import org.deegree.filter.OperatorFilter; import org.deegree.filter.expression.ValueReference; import org.deegree.filter.projection.ProjectionClause; import org.deegree.filter.projection.PropertyName; import org.deegree.filter.sort.SortProperty; import org.deegree.filter.spatial.BBOX; import org.deegree.geometry.Envelope; import org.deegree.geometry.Geometry; import org.deegree.geometry.GeometryFactory; import org.deegree.geometry.GeometryTransformer; import org.deegree.protocol.wfs.getfeature.GetFeature; import org.deegree.protocol.wfs.getfeature.TypeName; import org.deegree.protocol.wfs.query.AdHocQuery; import org.deegree.protocol.wfs.query.BBoxQuery; import org.deegree.protocol.wfs.query.FeatureIdQuery; import org.deegree.protocol.wfs.query.FilterQuery; import org.deegree.protocol.wfs.query.StoredQuery; import org.deegree.protocol.wfs.query.xml.QueryXMLAdapter; import org.deegree.protocol.wfs.storedquery.QueryExpressionText; import org.deegree.protocol.wfs.storedquery.StoredQueryDefinition; import org.deegree.protocol.wfs.storedquery.xml.StoredQueryDefinitionXMLAdapter; import org.deegree.services.wfs.WebFeatureService; import org.deegree.services.wfs.WfsFeatureStoreManager; import org.jaxen.NamespaceContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Responsible for validating a sequence of queries (e.g from {@link GetFeature} requests) and generating a * corresponding sequence of feature store queries. * <p> * Also performs some normalizing on the values of {@link ValueReference}s. TODO describe strategy * </p> * * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a> * @author last edited by: $Author$ * * @version $Revision$, $Date$ */ public class QueryAnalyzer { private final GeometryFactory geomFac = new GeometryFactory(); private static final Logger LOG = LoggerFactory.getLogger(QueryAnalyzer.class); private final WebFeatureService controller; private final WfsFeatureStoreManager service; private final Set<FeatureType> requestedFts = new HashSet<FeatureType>(); private final Map<Query, org.deegree.protocol.wfs.query.Query> queryToWFSQuery = new HashMap<Query, org.deegree.protocol.wfs.query.Query>(); private final Map<FeatureStore, List<Query>> fsToQueries = new LinkedHashMap<FeatureStore, List<Query>>(); private List<ProjectionClause> projections = null; private ICRS requestedCrs; private boolean allFtsPossible; private final boolean checkAreaOfUse; /** * Creates a new {@link QueryAnalyzer}. * * @param wfsQueries * queries be performed, must not be <code>null</code> * @param service * {@link WfsFeatureStoreManager} to be used, must not be <code>null</code> * @param checkInputDomain * true, if geometries in query constraints should be checked against validity domain of the SRS (needed * for CITE 1.1.0 compliance) * @throws OWSException * if the request cannot be performed, e.g. because it queries feature types that are not served */ public QueryAnalyzer(List<org.deegree.protocol.wfs.query.Query> wfsQueries, WebFeatureService controller, WfsFeatureStoreManager service, boolean checkInputDomain) throws OWSException { this.controller = controller; this.service = service; this.checkAreaOfUse = checkInputDomain; // generate validated feature store queries if (wfsQueries.isEmpty()) { // TODO perform the check here? String msg = "Either the typeName parameter must be present or the query must provide feature ids."; throw new OWSException(msg, INVALID_PARAMETER_VALUE, "typeName"); } List<Pair<AdHocQuery, org.deegree.protocol.wfs.query.Query>> adHocQueries = convertStoredQueries( wfsQueries); Query[] queries = new Query[adHocQueries.size()]; for (int i = 0; i < adHocQueries.size(); i++) { AdHocQuery wfsQuery = adHocQueries.get(i).first; Query query = validateQuery(wfsQuery); queries[i] = query; // yes, use the original WFS query (not necessarily adHoc) queryToWFSQuery.put(query, adHocQueries.get(i).second); // TODO what about queries with different SRS? if (wfsQuery.getSrsName() != null) { requestedCrs = wfsQuery.getSrsName(); } else { requestedCrs = controller.getDefaultQueryCrs(); } // TODO cope with more queries than one if (wfsQuery.getProjectionClauses() != null) { this.projections = Arrays.asList(wfsQuery.getProjectionClauses()); } } // associate queries with feature stores for (Query query : queries) { if (query.getTypeNames().length == 0) { for (FeatureStore fs : service.getStores()) { List<Query> fsQueries = fsToQueries.get(fs); if (fsQueries == null) { fsQueries = new ArrayList<Query>(); fsToQueries.put(fs, fsQueries); } fsQueries.add(query); } } else { FeatureStore fs = service.getStore(query.getTypeNames()[0].getFeatureTypeName()); List<Query> fsQueries = fsToQueries.get(fs); if (fsQueries == null) { fsQueries = new ArrayList<Query>(); fsToQueries.put(fs, fsQueries); } fsQueries.add(query); } } } private List<Pair<AdHocQuery, org.deegree.protocol.wfs.query.Query>> convertTemplateStoredQuery( StoredQuery query) throws OWSException { List<Pair<AdHocQuery, org.deegree.protocol.wfs.query.Query>> list = new ArrayList<Pair<AdHocQuery, org.deegree.protocol.wfs.query.Query>>(); StoredQueryHandler handler = controller.getStoredQueryHandler(); URL u = handler.getStoredQueryTemplate(query.getId()); try { String templ = IOUtils.toString(u.openStream()); for (Entry<String, OMElement> e : query.getParams().entrySet()) { String val = e.getValue().getText(); Pattern p = Pattern.compile("[$][{]" + e.getKey() + "[}]", Pattern.CASE_INSENSITIVE); templ = p.matcher(templ).replaceAll(val); } LOG.debug("Stored query template after replacement: {}", templ); StoredQueryDefinitionXMLAdapter parser = new StoredQueryDefinitionXMLAdapter(); parser.load(new StringReader(templ), "http://www.deegree.org/none"); StoredQueryDefinition def = parser.parse(); for (QueryExpressionText text : def.getQueryExpressionTextEls()) { for (OMElement elem : text.getChildEls()) { org.deegree.protocol.wfs.query.Query q = new QueryXMLAdapter().parseAbstractQuery200(elem); if (q instanceof AdHocQuery) { list.add(new Pair<AdHocQuery, org.deegree.protocol.wfs.query.Query>((AdHocQuery) q, query)); } } } return list; } catch (IOException e) { String msg = "An error occurred when trying to convert stored query with id '" + query.getId() + "': '" + e.getLocalizedMessage() + "'."; throw new OWSException(msg, INVALID_PARAMETER_VALUE, "storedQueryId"); } } private List<Pair<AdHocQuery, org.deegree.protocol.wfs.query.Query>> convertStoredQueries( List<org.deegree.protocol.wfs.query.Query> wfsQueries) throws OWSException { List<Pair<AdHocQuery, org.deegree.protocol.wfs.query.Query>> adHocQueries = new ArrayList<Pair<AdHocQuery, org.deegree.protocol.wfs.query.Query>>(); for (org.deegree.protocol.wfs.query.Query wfsQuery : wfsQueries) { if (wfsQuery instanceof AdHocQuery) { adHocQueries.add(new Pair<AdHocQuery, org.deegree.protocol.wfs.query.Query>((AdHocQuery) wfsQuery, wfsQuery)); } else { StoredQuery storedQuery = (StoredQuery) wfsQuery; if (storedQuery.getId().equals(GET_FEATURE_BY_ID)) { OMElement literalEl = storedQuery.getParams().get("ID"); if (literalEl == null) { String msg = "Stored query '" + storedQuery.getId() + "' requires parameter 'ID'."; throw new OWSException(msg, MISSING_PARAMETER_VALUE, "ID"); } LOG.debug("GetFeatureById query"); String requestedId = literalEl.getText(); FeatureIdQuery q = new FeatureIdQuery(null, null, null, null, null, null, new String[] { requestedId }); adHocQueries.add(new Pair<AdHocQuery, org.deegree.protocol.wfs.query.Query>(q, wfsQuery)); } else if (storedQuery.getId().equals(GET_FEATURE_BY_TYPE)) { // TODO qualify typeName using NAMESPACES parameter for KVP requests OMElement literalEl = storedQuery.getParams().get("TYPENAME"); if (literalEl == null) { String msg = "Stored query '" + storedQuery.getId() + "' requires parameter 'TYPENAME'."; throw new OWSException(msg, MISSING_PARAMETER_VALUE, "TYPENAME"); } String tn = literalEl.getText(); if (tn.contains(":")) { tn = tn.split(":")[1]; } LOG.debug("GetFeatureByType query"); FilterQuery q = new FilterQuery(new QName(tn), null, null, null); adHocQueries.add(new Pair<AdHocQuery, org.deegree.protocol.wfs.query.Query>(q, wfsQuery)); } else if (controller.getStoredQueryHandler().hasStoredQuery(storedQuery.getId())) { List<Pair<AdHocQuery, org.deegree.protocol.wfs.query.Query>> qs = convertTemplateStoredQuery( storedQuery); adHocQueries.addAll(qs); } else { String msg = "Stored query with id '" + storedQuery.getId() + "' is not known."; throw new OWSException(msg, INVALID_PARAMETER_VALUE, "storedQueryId"); } } } return adHocQueries; } /** * Returns all {@link FeatureType}s that may be returned in the response to the request. * * @return list of requested feature types, or <code>null</code> if any of the feature types served by the WFS could * be returned (happens only for KVP-request with feature ids and without typenames) */ public Collection<FeatureType> getFeatureTypes() { return allFtsPossible ? null : requestedFts; } /** * Returns the feature store queries that have to performed for this request. * * @return the feature store queries that have to performed, never <code>null</code> */ public Map<FeatureStore, List<Query>> getQueries() { return fsToQueries; } /** * Returns the original <code>GetFeature</code> query that the given query was derived from. * * @param query * @return */ public org.deegree.protocol.wfs.query.Query getQuery(Query query) { return queryToWFSQuery.get(query); } /** * Returns the crs that the returned geometries should have. * * TODO what about multiple queries with different CRS * * @return the crs, or <code>null</code> (use native crs) */ public ICRS getRequestedCRS() { return requestedCrs; } /** * Returns the specific XLink-behaviour for features properties. * * TODO what about multiple queries that specify different sets of properties * * @return specific XLink-behaviour or <code>null</code> (no specific behaviour) */ public List<ProjectionClause> getProjections() { return projections; } /** * Builds a feature store {@link Query} from the given WFS query and checks if the feature type / property name * references in the given {@link Query} are resolvable against the served application schema. * <p> * Incorrectly or un-qualified feature type or property names are repaired. These often stem from WFS 1.0.0 * KVP-requests (which doesn't have a namespace parameter) or broken clients. * </p> * * @param wfsQuery * query to be validated, must not be <code>null</code> * @return the feature store query, using only correctly fully qualified feature / property names * @throws OWSException * if an unresolvable feature type / property name is used */ private Query validateQuery(org.deegree.protocol.wfs.query.Query wfsQuery) throws OWSException { // requalify query typenames and keep track of them TypeName[] wfsTypeNames = ((AdHocQuery) wfsQuery).getTypeNames(); TypeName[] typeNames = new TypeName[wfsTypeNames.length]; FeatureStore commonFs = null; for (int i = 0; i < wfsTypeNames.length; i++) { String alias = wfsTypeNames[i].getAlias(); FeatureType ft = service.lookupFeatureType(wfsTypeNames[i].getFeatureTypeName()); if (ft == null) { String msg = "Feature type with name '" + wfsTypeNames[i].getFeatureTypeName() + "' is not served by this WFS."; throw new OWSException(msg, INVALID_PARAMETER_VALUE, "typeName"); } FeatureStore fs = service.getStore(ft.getName()); if (commonFs != null) { if (fs != commonFs) { String msg = "Requested join of feature types from different feature stores. This is not supported."; throw new OWSException(msg, INVALID_PARAMETER_VALUE, "typeName"); } } else { commonFs = fs; } requestedFts.add(ft); QName ftName = ft.getName(); typeNames[i] = new TypeName(ftName, alias); } if (wfsTypeNames.length == 0) { allFtsPossible = true; } // check requested / filter property names and geometries Filter filter = null; if (wfsQuery instanceof FilterQuery) { FilterQuery fQuery = ((FilterQuery) wfsQuery); if (fQuery.getProjectionClauses() != null) { for (ProjectionClause projection : fQuery.getProjectionClauses()) { if (projection instanceof PropertyName) { validatePropertyName(((PropertyName) projection).getPropertyName(), typeNames); } } } if (fQuery.getFilter() != null) { for (ValueReference pt : Filters.getPropertyNames(fQuery.getFilter())) { validatePropertyName(pt, typeNames); } if (checkAreaOfUse) { for (Geometry geom : Filters.getGeometries(fQuery.getFilter())) { validateGeometryConstraint(geom, ((AdHocQuery) wfsQuery).getSrsName()); } } } filter = fQuery.getFilter(); } else if (wfsQuery instanceof BBoxQuery) { BBoxQuery bboxQuery = (BBoxQuery) wfsQuery; ProjectionClause[] propNames = bboxQuery.getProjectionClauses(); if (propNames != null) { for (ProjectionClause propertyName : propNames) { if (propertyName instanceof PropertyName) { validatePropertyName(((PropertyName) propertyName).getPropertyName(), typeNames); } } } if (checkAreaOfUse) { validateGeometryConstraint(((BBoxQuery) wfsQuery).getBBox(), ((AdHocQuery) wfsQuery).getSrsName()); } Envelope bbox = bboxQuery.getBBox(); BBOX bboxOperator = new BBOX(bbox); filter = new OperatorFilter(bboxOperator); } else if (wfsQuery instanceof FeatureIdQuery) { FeatureIdQuery fidQuery = (FeatureIdQuery) wfsQuery; ProjectionClause[] propNames = fidQuery.getProjectionClauses(); if (propNames != null) { for (ProjectionClause propertyName : propNames) { if (propertyName instanceof PropertyName) { validatePropertyName(((PropertyName) propertyName).getPropertyName(), typeNames); } } } filter = new IdFilter(fidQuery.getFeatureIds()); } if (wfsTypeNames.length == 0 && (filter == null || !(filter instanceof IdFilter))) { String msg = "Either the typeName parameter must be present or the query must provide feature ids."; throw new OWSException(msg, INVALID_PARAMETER_VALUE, "typeName"); } SortProperty[] sortProps = ((AdHocQuery) wfsQuery).getSortBy(); if (sortProps != null) { for (SortProperty sortProperty : sortProps) { validatePropertyName(sortProperty.getSortProperty(), typeNames); } } // superimpose default query CRS if (filter != null) { Filters.setDefaultCRS(filter, controller.getDefaultQueryCrs()); } return new Query(typeNames, filter, ((AdHocQuery) wfsQuery).getFeatureVersion(), ((AdHocQuery) wfsQuery).getSrsName(), sortProps); } private void validatePropertyName(ValueReference propName, TypeName[] typeNames) throws OWSException { // no check possible if feature type is unknown if (typeNames.length > 0) { if (propName.getAsQName() != null) { if (!isPrefixedAndBound(propName)) { repairSimpleUnqualified(propName, typeNames[0]); } // check that the propName is indeed valid as belonging to serviced features QName name = getPropertyNameAsQName(propName); if (name != null) { if (typeNames.length == 1) { FeatureType ft = service.lookupFeatureType(typeNames[0].getFeatureTypeName()); if (ft.getPropertyDeclaration(name) == null) { // gml:boundedBy currently requires special treatment if (!name.getLocalPart().equals("boundedBy")) { String msg = "Specified PropertyName '" + propName.getAsText() + "' (='" + name + "') does not exist for feature type '" + ft.getName() + "'."; throw new OWSException(msg, INVALID_PARAMETER_VALUE, "PropertyName"); } } } // TODO really skip this check for join queries? } } else { // TODO property name may be an XPath and use aliases... } } } /** * Returns whether the propName has to be considered for re-qualification. * * @param propName * @return */ private boolean isPrefixedAndBound(ValueReference propName) { QName name = propName.getAsQName(); return !name.getPrefix().equals(DEFAULT_NS_PREFIX) && !name.getNamespaceURI().equals(""); } /** * Repairs a {@link ValueReference} that contains the local name of a {@link FeatureType}'s property or a prefixed * name, but without a correct namespace binding. * <p> * This types of propertynames especially occurs in WFS 1.0.0 requests. * </p> * * @param propName * property name to be repaired, must be "simple", i.e. contain only of a QName * @param typeName * feature type specification from the query, must not be <code>null</code> * @throws OWSException * if no match could be found */ private void repairSimpleUnqualified(ValueReference propName, TypeName typeName) throws OWSException { FeatureType ft = service.lookupFeatureType(typeName.getFeatureTypeName()); List<QName> propNames = new ArrayList<QName>(); // TODO which GML version for (PropertyType pt : ft.getPropertyDeclarations()) { propNames.add(pt.getName()); } QName match = QNameUtils.findBestMatch(propName.getAsQName(), propNames); if (match == null) { String msg = "Specified PropertyName '" + propName.getAsText() + "' does not exist for feature type '" + ft.getName() + "'."; throw new OWSException(msg, INVALID_PARAMETER_VALUE, "PropertyName"); } if (!match.equals(propName.getAsQName())) { LOG.debug("Repairing unqualified PropertyName: " + QNameUtils.toString(propName.getAsQName()) + " -> " + QNameUtils.toString(match)); // vague match String text = match.getLocalPart(); if (!match.getPrefix().equals(DEFAULT_NS_PREFIX)) { text = match.getPrefix() + ":" + match.getLocalPart(); } NamespaceBindings nsContext = new NamespaceBindings(); nsContext.addNamespace(match.getPrefix(), match.getNamespaceURI()); propName.set(text, nsContext); } } // TODO do this properly private QName getPropertyNameAsQName(ValueReference propName) { QName name = null; NamespaceContext nsContext = propName.getNsContext(); String s = propName.getAsText(); int colonIdx = s.indexOf(':'); if (!s.contains("/") && colonIdx != -1) { if (Character.isLetterOrDigit(s.charAt(0)) && Character.isLetterOrDigit(s.charAt(s.length() - 1))) { String prefix = s.substring(0, colonIdx); String localName = s.substring(colonIdx + 1, s.length()); String nsUri = null; if (nsContext != null) { nsUri = nsContext.translateNamespacePrefixToUri(prefix); } else { nsUri = service.getPrefixToNs().get(prefix); if (nsUri == null) { nsUri = ""; } } name = new QName(nsUri, localName, prefix); } } else { if (!s.contains("/") && !s.isEmpty() && Character.isLetterOrDigit(s.charAt(0)) && Character.isLetterOrDigit(s.charAt(s.length() - 1))) { name = new QName(s); } } return name; } private void validateGeometryConstraint(Geometry geom, ICRS queriedCrs) throws OWSException { // check if geometry's bbox is inside the domain of its CRS Envelope bbox = geom.getEnvelope(); if (bbox.getCoordinateSystem() != null) { // check if geometry's bbox is valid with respect to the CRS domain try { double[] b = bbox.getCoordinateSystem().getAreaOfUseBBox(); Envelope domainOfValidity = geomFac.createEnvelope(b[0], b[1], b[2], b[3], CRSUtils.EPSG_4326); domainOfValidity = transform(domainOfValidity, bbox.getCoordinateSystem()); if (!bbox.isWithin(domainOfValidity)) { String msg = "Invalid geometry constraint in filter. The envelope of the geometry is not within the domain of validity ('" + domainOfValidity + "') of its CRS ('" + bbox.getCoordinateSystem().getAlias() + "')."; throw new OWSException(msg, INVALID_PARAMETER_VALUE, "filter"); } } catch (UnknownCRSException e) { // could not validate constraint, but let's assume it's met } catch (IllegalArgumentException e) { // could not validate constraint, but let's assume it's met } catch (TransformationException e) { // could not validate constraint, but let's assume it's met } } // check if geometry's bbox is inside the validity domain of the queried CRS if (queriedCrs != null) { try { double[] b = queriedCrs.getAreaOfUseBBox(); Envelope domainOfValidity = geomFac.createEnvelope(b[0], b[1], b[2], b[3], CRSUtils.EPSG_4326); domainOfValidity = transform(domainOfValidity, queriedCrs); Envelope bboxTransformed = transform(bbox, queriedCrs); if (!bboxTransformed.isWithin(domainOfValidity)) { String msg = "Invalid geometry constraint in filter. The envelope of the geometry is not within the domain of validity ('" + domainOfValidity + "') of the queried CRS ('" + queriedCrs.getAlias() + "')."; throw new OWSException(msg, INVALID_PARAMETER_VALUE, "filter"); } } catch (UnknownCRSException e) { // could not validate constraint, but let's assume it's met } catch (IllegalArgumentException e) { // could not validate constraint, but let's assume it's met } catch (TransformationException e) { // could not validate constraint, but let's assume it's met } } } private Envelope transform(Envelope bbox, ICRS targetCrs) throws IllegalArgumentException, TransformationException, UnknownCRSException { if (targetCrs.equals(bbox.getEnvelope().getCoordinateSystem())) { return bbox; } GeometryTransformer transformer = new GeometryTransformer(targetCrs); return transformer.transform(bbox); } }