/* See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * Esri Inc. licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.esri.gpt.server.csw.provider.local; import com.esri.gpt.catalog.discovery.Discoverable; import com.esri.gpt.catalog.discovery.DiscoveryClause; import com.esri.gpt.catalog.discovery.DiscoveryFilter; import com.esri.gpt.catalog.discovery.DiscoveryQuery; import com.esri.gpt.catalog.discovery.LogicalClause; import com.esri.gpt.catalog.discovery.PropertyClause; import com.esri.gpt.catalog.discovery.PropertyMeaningType; import com.esri.gpt.catalog.discovery.SpatialClause; import com.esri.gpt.catalog.discovery.LogicalClause.LogicalAnd; import com.esri.gpt.framework.geometry.Envelope; import com.esri.gpt.framework.util.Val; import com.esri.gpt.server.csw.provider.components.CapabilityOptions; import com.esri.gpt.server.csw.provider.components.CswConstants; import com.esri.gpt.server.csw.provider.components.CswNamespaces; import com.esri.gpt.server.csw.provider.components.IFilterParser; import com.esri.gpt.server.csw.provider.components.OperationContext; import com.esri.gpt.server.csw.provider.components.OwsException; import com.esri.gpt.server.csw.provider.components.QueryOptions; import java.util.ArrayList; import java.util.logging.Logger; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * Parses the ogc:Filter portion of a CSW query request (GetRecords). */ public class QueryFilterParser extends DiscoveryAdapter implements IFilterParser { /** class variables ========================================================= */ /** The Logger. */ private static Logger LOGGER = Logger.getLogger(QueryFilterParser.class.getName()); /** instance variables ====================================================== */ private OperationContext opContext; /** constructors ============================================================ */ /** Default constructor */ public QueryFilterParser(OperationContext context) { super(context); this.opContext = context; } /** methods ================================================================= */ /** * Parses the ogc:Filter node for an XML based request. * @param context the operation context * @param filterNode the ogc:Filter node * @param xpath an XPath to enable queries (properly configured with name spaces) * @throws OwsException if validation fails * @throws XPathExpressionException if an XPath related exception occurs */ public void parseFilter(OperationContext context, Node filterNode, XPath xpath) throws OwsException, XPathExpressionException { // initialize the discovery filter QueryOptions qOptions = context.getRequestOptions().getQueryOptions(); DiscoveryQuery query = this.getDiscoveryContext().getDiscoveryQuery(); DiscoveryFilter filter = query.getFilter(); filter.setRootClause(null); filter.setStartRecord(qOptions.getStartRecord()); if (qOptions.getResultType().equals(CswConstants.ResultType_Hits)) { filter.setMaxRecords(0); } else { filter.setMaxRecords(qOptions.getMaxRecords()); } // parse the ogc:Filter if (filterNode != null) { LOGGER.finer("Parsing ogc:Filter...."); filter.setRootClause(new LogicalAnd()); LogicalClause rootClause = filter.getRootClause(); this.parseLogicalClause(filterNode,xpath,rootClause); if (rootClause.getClauses().size() == 1) { DiscoveryClause onlySubClause = rootClause.getClauses().get(0); if (onlySubClause instanceof LogicalClause) { LogicalClause onlyLogicalSubClause = (LogicalClause) onlySubClause; filter.setRootClause(onlyLogicalSubClause); } } } } /** * Parses a parent node for logical, property comparison and spatial sub-clauses. * <br/>Any logical clauses encountered will be recursively parsed. * @param parent the parent node from which sub-clauses will read * @param xpath an XPath to enable queries (properly configured with name spaces) * @param logicalClause the active logical clause to which sub-clauses will be added * @throws OwsException if validation fails * @throws XPathExpressionException if an XPath related exception occurs */ protected void parseLogicalClause(Node parent, XPath xpath, LogicalClause logicalClause) throws OwsException, XPathExpressionException { NodeList children = parent.getChildNodes(); if (children == null) return; int n = children.getLength(); for (int i=0; i<n; i++) { Node subNode = children.item(i); String uri = Val.chkStr(subNode.getNamespaceURI()); if (uri.length() > 0) { String localName = Val.chkStr(subNode.getLocalName()); LOGGER.finer("Parsing node ("+uri+")"+localName); if (uri.equals(CswNamespaces.URI_OGC)) { // logical clauses - add then recurse if (localName.equals("And")) { LogicalClause logical = new LogicalClause.LogicalAnd(); logicalClause.getClauses().add(logical); this.parseLogicalClause(subNode,xpath,logical); } else if (localName.equals("Or")) { LogicalClause logical = new LogicalClause.LogicalOr(); logicalClause.getClauses().add(logical); this.parseLogicalClause(subNode,xpath,logical); } else if (localName.equals("Not")) { LogicalClause logical = new LogicalClause.LogicalNot(); logicalClause.getClauses().add(logical); this.parseLogicalClause(subNode,xpath,logical); // property comparison clauses } else if (localName.equals("PropertyIsBetween")) { this.parsePropertyClause(subNode,xpath,logicalClause, new PropertyClause.PropertyIsBetween()); } else if (localName.equals("PropertyIsEqualTo")) { this.parsePropertyClause(subNode,xpath,logicalClause, new PropertyClause.PropertyIsEqualTo()); } else if (localName.equals("PropertyIsGreaterThan")) { this.parsePropertyClause(subNode,xpath,logicalClause, new PropertyClause.PropertyIsGreaterThan()); } else if (localName.equals("PropertyIsGreaterThanOrEqualTo")) { this.parsePropertyClause(subNode,xpath,logicalClause, new PropertyClause.PropertyIsGreaterThanOrEqualTo()); } else if (localName.equals("PropertyIsLessThan")) { this.parsePropertyClause(subNode,xpath,logicalClause, new PropertyClause.PropertyIsLessThan()); } else if (localName.equals("PropertyIsLessThanOrEqualTo")) { this.parsePropertyClause(subNode,xpath,logicalClause, new PropertyClause.PropertyIsLessThanOrEqualTo()); } else if (localName.equals("PropertyIsLike")) { this.parsePropertyClause(subNode,xpath,logicalClause, new PropertyClause.PropertyIsLike()); } else if (localName.equals("PropertyIsNotEqualTo")) { this.parsePropertyClause(subNode,xpath,logicalClause, new PropertyClause.PropertyIsNotEqualTo()); } else if (localName.equals("PropertyIsNull")) { this.parsePropertyClause(subNode,xpath,logicalClause, new PropertyClause.PropertyIsNull()); // spatial clauses } else if (localName.equals("BBOX")) { this.parseSpatialClause(subNode,xpath,logicalClause, new SpatialClause.GeometryBBOXIntersects()); } else if (localName.equals("Contains")) { this.parseSpatialClause(subNode,xpath,logicalClause, new SpatialClause.GeometryContains()); } else if (localName.equals("Disjoint")) { this.parseSpatialClause(subNode,xpath,logicalClause, new SpatialClause.GeometryIsDisjointTo()); } else if (localName.equals("Equals")) { this.parseSpatialClause(subNode,xpath,logicalClause, new SpatialClause.GeometryIsEqualTo()); } else if (localName.equals("Intersects")) { this.parseSpatialClause(subNode,xpath,logicalClause, new SpatialClause.GeometryIntersects()); } else if (localName.equals("Overlaps")) { this.parseSpatialClause(subNode,xpath,logicalClause, new SpatialClause.GeometryOverlaps()); } else if (localName.equals("Within")) { this.parseSpatialClause(subNode,xpath,logicalClause, new SpatialClause.GeometryIsWithin()); } else if (localName.equals("Beyond") || localName.equals("Crosses") || localName.equals("DWithin") || localName.equals("Touches")) { String locator = subNode.getLocalName(); String msg = "Spatial operator "+subNode.getNodeName()+" is not supported."; throw new OwsException(OwsException.OWSCODE_InvalidParameterValue,locator,msg); } else { String locator = subNode.getLocalName(); String msg = "Operator "+subNode.getNodeName()+" is not supported."; throw new OwsException(OwsException.OWSCODE_InvalidParameterValue,locator,msg); } } } } } /** * Parses the property and literal elements underlying a comparison operator. * <br/>If the resulting property clause is valid, it will be added to the * clause collection of the supplied logical clause. * @param parent the parent node (the node of the property operator) * @param xpath an XPath to enable queries (properly configured with name spaces) * @param logicalClause the logical clause which will contain the comparison clause * @param propertyClause the populate * @throws OwsException OwsException if validation fails * @throws XPathExpressionException if an XPath related exception occurs */ protected void parsePropertyClause(Node parent, XPath xpath, LogicalClause logicalClause, PropertyClause propertyClause) throws OwsException, XPathExpressionException { // TODO what if this is a Geometry property, also need to validate literals // initialize LOGGER.finer("Parsing property clause for "+parent.getNodeName()); String sErr = parent.getNodeName(); Discoverable discoverable = this.parsePropertyName(parent,xpath); propertyClause.setTarget(discoverable); // anytext queries are only supported for PropertyIsLike if (discoverable.getMeaning().getMeaningType().equals(PropertyMeaningType.ANYTEXT)) { if (!(propertyClause instanceof PropertyClause.PropertyIsLike)) { String sPropName = "AnyText"; Node ndPropName = (Node)xpath.evaluate("ogc:PropertyName",parent,XPathConstants.NODE); if (ndPropName != null) { sPropName = Val.chkStr(ndPropName.getTextContent()); } String msg = sErr+" - PropertyIsLike is the only supported operand for PropertyName: "+sPropName; throw new OwsException(OwsException.OWSCODE_InvalidParameterValue,"PropertyName",msg); } } // between comparison - set lower and upper boundaries if (propertyClause instanceof PropertyClause.PropertyIsBetween) { PropertyClause.PropertyIsBetween between; between = (PropertyClause.PropertyIsBetween)propertyClause; Node ndLower = (Node)xpath.evaluate("ogc:LowerBoundary",parent,XPathConstants.NODE); Node ndUpper = (Node)xpath.evaluate("ogc:UpperBoundary",parent,XPathConstants.NODE); String sLower = ""; String sUpper = ""; if ((ndLower == null) && (ndUpper == null)) { String msg = sErr+" - a LowerBoundary or UpperBoundary was not found."; throw new OwsException(OwsException.OWSCODE_MissingParameterValue,"PropertyIsBetween",msg); } if (ndLower != null) { sLower = ndLower.getTextContent(); between.setLowerBoundary(sLower); // TODO validate content } if (ndUpper != null) { sUpper = ndUpper.getTextContent(); between.setUpperBoundary(sUpper); // TODO validate content } if ((sLower == null) || (sLower.length() == 0)) { if ((sUpper == null) || (sUpper.length() == 0)) { String msg = sErr+" - the LowerBoundary and UpperBoundary are empty."; throw new OwsException(OwsException.OWSCODE_InvalidParameterValue,"PropertyIsBetween",msg); } } // null check - no literal required } else if (propertyClause instanceof PropertyClause.PropertyIsNull) { // non-range clauses } else { Node ndLiteral = (Node)xpath.evaluate("ogc:Literal", parent,XPathConstants.NODE); if (ndLiteral == null) { String msg = sErr+" - an ogc:Literal was not found."; throw new OwsException(OwsException.OWSCODE_MissingParameterValue,"Literal",msg); } String sLiteral = ndLiteral.getTextContent(); propertyClause.setLiteral(sLiteral); // TODO validate content if ((sLiteral == null) || (sLiteral.length() == 0)) { String msg = sErr+".ogc:Literal - the supplied literal was empty."; throw new OwsException(OwsException.OWSCODE_InvalidParameterValue,"Literal",msg); } // set like comparison attributes if (propertyClause instanceof PropertyClause.PropertyIsLike) { PropertyClause.PropertyIsLike like; like = (PropertyClause.PropertyIsLike) propertyClause; like.setEscapeChar(xpath.evaluate("@escapeChar", parent)); like.setSingleChar(xpath.evaluate("@singleChar", parent)); like.setWildCard(xpath.evaluate("@wildCard", parent)); } // initialize the language code, (INSPIRE requirement but generally applicable) // INSPIRE requirement, specify the language for exceptions in this manner // doesn't seem to be a good approach if ((this.opContext != null) && (propertyClause instanceof PropertyClause.PropertyIsEqualTo)) { if (discoverable.getMeaning().getName().equals("apiso.Language")) { if ((sLiteral != null) && (sLiteral.length() > 0)) { CapabilityOptions cOptions = this.opContext.getRequestOptions().getCapabilityOptions(); if (cOptions.getLanguageCode() == null) { cOptions.setLanguageCode(sLiteral); } } } } } // add the clause logicalClause.getClauses().add(propertyClause); } /** * Parses the spatial operand underlying a spatial operator. * <br/>The spatial operand is used to populate the bounding envelope * associated with the supplied spatial clause. * <br/>If the envelope is valid, the spatial clause will be added to the * clause collection of the supplied logical clause. * @param parent the parent node (specifies the spatial operator) * @param xpath an XPath to enable queries (properly configured with namespaces) * @param logicalClause the logical clause which will contain the spatial clause * @param spatialClause the spatial clause to populate * @throws OwsException OwsException if validation fails * @throws XPathExpressionException if an XPath related exception occurs */ protected void parseSpatialClause(Node parent, XPath xpath, LogicalClause logicalClause, SpatialClause spatialClause) throws OwsException, XPathExpressionException { // initialize LOGGER.finer("Parsing spatial clause for "+parent.getNodeName()); String sErr = parent.getNodeName(); Envelope envelope = spatialClause.getBoundingEnvelope(); // TODO ensure that the discoverable is a geometry Discoverable discoverable = this.parsePropertyName(parent,xpath); spatialClause.setTarget(discoverable); /* * * gml:Envelope <attribute name="gid" type="ID" use="optional"/> <attribute * name="srsName" type="anyURI" use="optional"/> * * <element name="lowerCorner" type="gml:DirectPositionType"/> <attribute * name="gid" type="ID" use="optional"/> <attribute name="srsName" * type="anyURI" use="optional"/> space separated x y <element * name="upperCorner" type="gml:DirectPositionType"/> <attribute name="gid" * type="ID" use="optional"/> <attribute name="srsName" type="anyURI" * use="optional"/> space separated x y * * <element ref="gml:pos" minOccurs="2" maxOccurs="2"> both are * type="gml:DirectPositionType" (deprecated) * * <element ref="gml:coord" minOccurs="2" maxOccurs="2"/> <element X> <element * Y> * * <element ref="gml:coordinates"/> <attribute name="decimal" type="string" * use="optional" default="."/> <attribute name="cs" type="string" * use="optional" default=","/> <attribute name="ts" type="string" * use="optional" default=" "/> * * gml:Box <attribute name="gid" type="ID" use="optional"/> <attribute * name="srsName" type="anyURI" use="optional"/> <element ref="gml:coord" * minOccurs="2" maxOccurs="2"/> <element X> <element Y> <element * ref="gml:coordinates"/> <attribute name="decimal" type="string" * use="optional" default="."/> <attribute name="cs" type="string" * use="optional" default=","/> <attribute name="ts" type="string" * use="optional" default=" "/> * * gml:Point <attribute name="gid" type="ID" use="optional"/> <attribute * name="srsName" type="anyURI" use="optional"/> <element ref="gml:coord"/> * <element X> <element Y> <element ref="gml:coordinates"/> <attribute * name="decimal" type="string" use="optional" default="."/> <attribute * name="cs" type="string" use="optional" default=","/> <attribute name="ts" * type="string" use="optional" default=" "/> * * gml:LineString <attribute name="gid" type="ID" use="optional"/> <attribute * name="srsName" type="anyURI" use="optional"/> <element ref="gml:coord" * minOccurs="2" maxOccurs="unbounded"/> <element X> <element Y> <element * ref="gml:coordinates"/> <attribute name="decimal" type="string" * use="optional" default="."/> <attribute name="cs" type="string" * use="optional" default=","/> <attribute name="ts" type="string" * use="optional" default=" "/> */ // determine the envelope Node ndSpatial = (Node)xpath.evaluate("gml:Envelope",parent,XPathConstants.NODE); if (ndSpatial == null) { ndSpatial = (Node)xpath.evaluate("gml:Box",parent,XPathConstants.NODE); if (ndSpatial == null) { ndSpatial = (Node)xpath.evaluate("gml:Point",parent,XPathConstants.NODE); } } if (ndSpatial != null) { LOGGER.finest("Parsing "+ndSpatial.getNodeName()+"..."); sErr += "."+ndSpatial.getNodeName(); spatialClause.setSrsName(xpath.evaluate("@srsName",ndSpatial)); Node ndLower = (Node)xpath.evaluate("gml:lowerCorner",ndSpatial,XPathConstants.NODE); Node ndUpper = (Node)xpath.evaluate("gml:upperCorner",ndSpatial,XPathConstants.NODE); Node ndCoords = (Node)xpath.evaluate("gml:coordinates",ndSpatial,XPathConstants.NODE); NodeList nlCoord = (NodeList)xpath.evaluate("gml:coord",ndSpatial,XPathConstants.NODESET); // handle a lower and upper boundary if ((ndLower != null) && (ndUpper != null)) { String sLower = Val.chkStr(ndLower.getTextContent()); String sUpper = Val.chkStr(ndUpper.getTextContent()); LOGGER.finest("Parsing gml:lowerCorner=\""+sLower+"\" gml:upperCorner=\""+sUpper+"\""); String[] xyLower = Val.tokenize(sLower," "); String[] xyUpper = Val.tokenize(sUpper," "); if ((xyLower.length == 2) && (xyUpper.length == 2)) { envelope.put(xyLower[0],xyLower[1],xyUpper[0],xyUpper[1]); } // handle a delimited list of coordinates } else if (ndCoords != null) { // separators: decimal, ts (between coordinate pairs), cs (between x/y values) char cDecSep = '.'; String decSep = xpath.evaluate("@decimal",ndCoords); if ((decSep != null) && (decSep.length() == 1)) cDecSep = decSep.charAt(0); String tsSep = xpath.evaluate("@ts",ndCoords); if ((tsSep == null) || (tsSep.length() != 1)) tsSep = " "; String csSep = xpath.evaluate("@cs", ndCoords); if ((csSep == null) || (csSep.length() != 1)) csSep = ","; String sepMsg = "decimal=\""+cDecSep+"\" ts=\""+tsSep+"\" cs=\""+csSep+"\""; // collect individual string values String sCordinates = Val.chkStr(ndCoords.getTextContent()); LOGGER.finest("Parsing gml:coordinates "+sepMsg+" coordinates=\""+sCordinates+"\""); ArrayList<String> values = new ArrayList<String>(); String[] tsValues = Val.tokenize(sCordinates,tsSep); for (String tsValue : tsValues) { String[] csValues = Val.tokenize(tsValue,csSep); for (String csValue: csValues) { LOGGER.finer("Adding coordinate value: "+csValue); values.add(csValue.replace(cDecSep,'.')); } } // determine the minimum and maximum envelope values from the coordinate list for (int i=0; i<values.size(); i=i+2) { try { LOGGER.finest("Handling coordinate: "+values.get(i)+" "+values.get(i + 1)); double x = Double.parseDouble(values.get(i)); double y = Double.parseDouble(values.get(i+1)); envelope.merge(new Envelope(x,y,x,y)); } catch (NumberFormatException e) { LOGGER.warning(e.getMessage()); } } // handle a collection of coordinate elements } else if (nlCoord.getLength() > 0) { LOGGER.finest("Parsing gml:coord elements..."); for (int i=0; i< nlCoord.getLength(); i++) { Node ndX = (Node)xpath.evaluate("gml:X",nlCoord.item(i),XPathConstants.NODE); Node ndY = (Node)xpath.evaluate("gml:Y",nlCoord.item(i),XPathConstants.NODE); if ((ndX != null) && (ndY != null)) { try { LOGGER.finest("Handling coordinate: "+ndX.getTextContent()+" "+ndY.getTextContent()); double x = Double.parseDouble(ndX.getTextContent()); double y = Double.parseDouble(ndY.getTextContent()); envelope.merge(new Envelope(x,y,x,y)); } catch (NumberFormatException e) { LOGGER.warning(e.getMessage()); } } } } } // add the clause if the envelope is not empty if (!envelope.isEmpty()) { logicalClause.getClauses().add(spatialClause); } else { String msg = sErr+" - the geometry of the spatial operand was not valid."; throw new OwsException(OwsException.OWSCODE_InvalidParameterValue,parent.getLocalName(),msg); } } }