All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opengis.cite.iso19142.util.WFSMessage Maven / Gradle / Ivy

There is a newer version: 2.0-r18
Show newest version
package org.opengis.cite.iso19142.util;

import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.apache.xerces.xs.XSElementDeclaration;
import org.apache.xerces.xs.XSModel;
import org.opengis.cite.iso19136.util.XMLSchemaModelUtils;
import org.opengis.cite.iso19142.Namespaces;
import org.opengis.cite.iso19142.ProtocolBinding;
import org.opengis.cite.iso19142.WFS2;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Provides various utility methods for constructing, accessing, or manipulating
 * the content of WFS request and response messages.
 */
public class WFSMessage {

    private static final Logger LOGR = Logger.getLogger(WFSMessage.class.getPackage().getName());
    private static final String TNS_PREFIX = "tns";
    private static final DocumentBuilder BUILDER = initDocBuilder();

    private static DocumentBuilder initDocBuilder() {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        DocumentBuilder builder = null;
        try {
            builder = factory.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            TestSuiteLogger.log(Level.WARNING, "Failed to create parser", e);
        }
        return builder;
    }

    /**
     * Transforms the XML representation of a WFS request entity to its
     * corresponding KVP serialization format.
     * 
     * @param xmlSource
     *            A Source representing the XML request entity.
     * @return A String containing the resulting query component.
     */
    public static String transformEntityToKVP(Source xmlSource) {
        Source xsltSource = new StreamSource(WFSMessage.class.getResourceAsStream("xml2kvp.xsl"));
        TransformerFactory factory = TransformerFactory.newInstance();
        StringWriter writer = new StringWriter();
        try {
            Transformer transformer = factory.newTransformer(xsltSource);
            transformer.transform(xmlSource, new StreamResult(writer));
        } catch (Exception e) {
            TestSuiteLogger.log(Level.WARNING, "Failed to generate KVP result from Source " + xmlSource.getSystemId(),
                    e);
        }
        return writer.toString();
    }

    /**
     * Wraps the given XML request entity in the body of a SOAP envelope.
     * 
     * @param xmlSource
     *            The Source providing the XML request entity.
     * @param version
     *            The version of the SOAP protocol (either "1.1" or "1.2"); if
     *            not specified the latest version is assumed.
     * @return A DOM Document node representing a SOAP request message.
     */
    public static Document wrapEntityInSOAPEnvelope( Source xmlSource, String version ) {
        if ( "1.1".equals( version ) )
            return wrapEntityInSOAPEnvelopeWithNS( xmlSource, Namespaces.SOAP11 );
        return wrapEntityInSOAPEnvelopeWithNS( xmlSource, Namespaces.SOAP_ENV );
    }
    
    /**
     * Adds a simple wfs:Query element (without a filter) to the given request
     * entity. The typeNames attribute value is set using the supplied QName
     * objects. Namespace bindings are added if necessary.
     * 
     * @param doc
     *            A Document representing a WFS request entity that accepts
     *            wfs:Query elements as children of the document element
     *            (GetFeature, GetPropertyValue, GetFeatureWithLock,
     *            LockFeature).
     * @param qNames
     *            A sequence of QName objects representing (qualified) feature
     *            type names recognized by the IUT.
     * @return The Element representing the query expression (wfs:Query); it
     *         will be empty.
     */
    public static Element appendSimpleQuery(Document doc, QName... qNames) {
        Element docElement = doc.getDocumentElement();
        Element newQuery = doc.createElementNS(Namespaces.WFS, "wfs:Query");
        StringBuilder typeNames = new StringBuilder();
        for (QName qName : qNames) {
            // look for prefix already bound to this namespace URI
            String nsPrefix = docElement.lookupPrefix(qName.getNamespaceURI());
            if (null == nsPrefix) {
                nsPrefix = "ns" + Integer.toString((int) (Math.random() * 100));
                newQuery.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:" + nsPrefix,
                        qName.getNamespaceURI());
            }
            typeNames.append(nsPrefix).append(':').append(qName.getLocalPart()).append(' ');
        }
        newQuery.setAttribute("typeNames", typeNames.toString().trim());
        docElement.appendChild(newQuery);
        return newQuery;
    }

    /**
     * Adds a wfs:StoredQuery element to the given request entity.
     * 
     * @param doc
     *            A Document representing a WFS request entity that accepts
     *            wfs:StoredQuery elements as children of the document element
     *            (GetFeature, GetPropertyValue, GetFeatureWithLock,
     *            LockFeature).
     * @param queryId
     *            A URI that identifies the stored query to invoke.
     * @param params
     *            A Map containing query parameters (may be empty, e.g.
     *            {@literal Collections..emptyMap()}). A
     *            parameter name is associated with an Object (String or QName)
     *            representing its value.
     */
    public static void appendStoredQuery(Document doc, String queryId, Map params) {
        Element docElement = doc.getDocumentElement();
        Element newQuery = doc.createElementNS(Namespaces.WFS, "wfs:StoredQuery");
        newQuery.setAttribute("id", queryId);
        docElement.appendChild(newQuery);
        for (Map.Entry entry : params.entrySet()) {
            Element param = doc.createElementNS(Namespaces.WFS, WFS2.PARAM_ELEM);
            param.setPrefix("wfs");
            param.setAttribute("name", entry.getKey());
            newQuery.appendChild(param);
            Object value = entry.getValue();
            if (QName.class.isInstance(value)) {
                QName qName = QName.class.cast(value);
                String prefix = (qName.getPrefix().isEmpty()) ? "tns" : qName.getPrefix();
                param.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:" + prefix, qName.getNamespaceURI());
                param.setTextContent(prefix + ":" + qName.getLocalPart());
            } else {
                param.setTextContent(value.toString());
            }
        }
    }

    /**
     * Creates an XML request entity of the specified request type.
     * 
     * @param reqResource
     *            The name of a classpath resource containing an XML request
     *            entity.
     * @param wfsVersion
     *            A WFS version identifier ("2.0.0" if not specified).
     * @return A Document representing a WFS request entity.
     */
    public static Document createRequestEntity(String reqResource, String wfsVersion) {
        String resourceName = reqResource + ".xml";
        Document doc = null;
        try {
            doc = BUILDER.parse(WFSMessage.class.getResourceAsStream(resourceName));
        } catch (Exception e) {
            TestSuiteLogger.log(Level.WARNING, "Failed to parse request entity from classpath: " + resourceName, e);
        }
        updateVersion( doc , wfsVersion);
        return doc;
    }

    /**
     * Sets the @version attribute in the root element of the doc to the specified version
     * 
     * @param doc
     *            the doc to update. never null
     * @param wfsVersion
     *            A WFS version identifier ("2.0.0" if not specified).
     */
    public static void updateVersion( Document doc, String wfsVersion ) {
        Attr verAttr = doc.getDocumentElement().getAttributeNode( "version" );
        if ( null != verAttr && null != wfsVersion && !wfsVersion.isEmpty() ) {
            doc.getDocumentElement().getAttributeNode( "version" ).setValue( wfsVersion );
        }
    }

    /**
     * Sets the value of the typeName attribute on an action element
     * (wfs:Update, wfs:Delete) contained in a Transaction request entity.
     * 
     * @param elem
     *            An action element in a transaction request.
     * @param qName
     *            The qualified name of a feature type.
     */
    public static void setTypeName(Element elem, QName qName) {
        List actions = Arrays.asList(WFS2.UPDATE, WFS2.DELETE);
        if (!actions.contains(elem.getLocalName())) {
            return;
        }
        StringBuilder typeNames = new StringBuilder();
        // look for prefix already bound to this namespace URI
        String nsPrefix = elem.lookupPrefix(qName.getNamespaceURI());
        if (null == nsPrefix) { // check document element
            nsPrefix = elem.getOwnerDocument().lookupPrefix(qName.getNamespaceURI());
        }
        if (null == nsPrefix) {
            nsPrefix = "ns" + Integer.toString((int) (Math.random() * 100));
            elem.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:" + nsPrefix, qName.getNamespaceURI());
        }
        typeNames.append(nsPrefix).append(':').append(qName.getLocalPart());
        elem.setAttribute("typeName", typeNames.toString());
    }

    /**
     * Builds a filter predicate containing a fes:ResourceId element that
     * identifies the feature instance to be modified.
     * 
     * @param id
     *            A String denoting a GML object identifier (gml:id).
     * @return An Element node (fes:Filter).
     */
    public static Element newResourceIdFilter(String id) {
        Element filter = XMLUtils.createElement(new QName(Namespaces.FES, "Filter", "fes"));
        Element resourceId = XMLUtils.createElement(new QName(Namespaces.FES, "ResourceId", "fes"));
        resourceId.setAttribute("rid", id);
        filter.appendChild(filter.getOwnerDocument().adoptNode(resourceId));
        return filter;
    }

    /**
     * Inserts a standard GML property into a given feature instance. If the
     * property node already exists it is replaced.
     * 
     * @param feature
     *            An Element node representing a GML feature
     * @param gmlProperty
     *            An Element node representing a standard (non-deprecated) GML
     *            feature property.
     */
    public static void insertGMLProperty(Element feature, Element gmlProperty) {
        Document doc = feature.getOwnerDocument();
        QName gmlPropName = new QName(gmlProperty.getNamespaceURI(), gmlProperty.getLocalName());
        NodeList existing = feature.getElementsByTagNameNS(gmlPropName.getNamespaceURI(), gmlPropName.getLocalPart());
        if (existing.getLength() > 0) {
            Node oldProp = existing.item(0);
            gmlProperty.setPrefix(oldProp.getPrefix());
            feature.replaceChild(doc.adoptNode(gmlProperty), oldProp);
            return;
        }
        // Create map associating GML property with collection of following
        // siblings to help determine insertion point (before nextSibling).
        Map> followingSiblingsMap = new HashMap>();
        followingSiblingsMap.put(new QName(Namespaces.GML, "description"),
                Arrays.asList("descriptionReference", "identifier", "name", "boundedBy", "location"));
        followingSiblingsMap.put(new QName(Namespaces.GML, "identifier"),
                Arrays.asList("name", "boundedBy", "location"));
        followingSiblingsMap.put(new QName(Namespaces.GML, "name"), Arrays.asList("boundedBy", "location"));
        if (!followingSiblingsMap.containsKey(gmlPropName))
            return; // ignore deprecated properties
        List followingSibs = followingSiblingsMap.get(gmlPropName);
        NodeList properties = feature.getChildNodes();
        Node nextSibling = null;
        for (int i = 0; i < properties.getLength(); i++) {
            Node property = properties.item(i);
            if (property.getNodeType() != Node.ELEMENT_NODE)
                continue;
            String nsURI = property.getNamespaceURI();
            String propName = property.getLocalName();
            // check if application-defined prop or a following GML prop
            if (!nsURI.equals(Namespaces.GML) || (followingSibs.contains(propName) && nsURI.equals(Namespaces.GML))) {
                nextSibling = property;
                break;
            }
        }
        if (nextSibling.getNamespaceURI().equals(Namespaces.GML)) {
            gmlProperty.setPrefix(nextSibling.getPrefix());
        } else {
            gmlProperty.setPrefix("gml");
        }
        feature.insertBefore(doc.adoptNode(gmlProperty), nextSibling);
    }

    /**
     * Creates an Element node (fes:ValueReference) containing an XPath
     * expression derived from a property element declaration.
     * 
     * @param propertyElem
     *            An element declaration that defines some feature property.
     * @return An Element containing an XPath expression and an appropriate
     *         namespace binding.
     */
    public static Element createValueReference(XSElementDeclaration propertyElem) {
        Element valueRef = XMLUtils.createElement(new QName(Namespaces.FES, "ValueReference", "fes"));
        valueRef.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:" + TNS_PREFIX,
                propertyElem.getNamespace());
        valueRef.setTextContent(TNS_PREFIX + ":" + propertyElem.getName());
        return valueRef;
    }

    /**
     * Creates a GML envelope covering the area of use for the "WGS 84" CRS
     * (srsName="urn:ogc:def:crs:EPSG::4326").
     * 
     * @return A Document containing gml:Envelope as the document element.
     */
    public static Document createGMLEnvelope() {
        Document doc;
        try {
            doc = BUILDER.parse(WFSMessage.class.getResourceAsStream("Envelope.xml"));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return doc;
    }

    /**
     * Adds a namespace binding to the document element.
     * 
     * @param doc
     *            A Document representing a request entity.
     * @param qName
     *            A QName containing a namespace URI and prefix; the local part
     *            is ignored.
     */
    public static void addNamespaceBinding(Document doc, QName qName) {
        Element docElem = doc.getDocumentElement();
        docElem.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:" + qName.getPrefix(),
                qName.getNamespaceURI());
    }

    /**
     * Adds a sequence of wfs:Replace statements to the given transaction
     * request entity.
     * 
     * @param trxRequest
     *            A Document node representing a wfs:Transaction request entity.
     * @param replacements
     *            A List containing replacement feature representations (as
     *            GML).
     */
    public static void addReplaceStatements(Document trxRequest, List replacements) {
        Element docElem = trxRequest.getDocumentElement();
        if (!docElem.getLocalName().equals(WFS2.TRANSACTION)) {
            throw new IllegalArgumentException("Document node is not a Transaction request: " + docElem.getNodeName());
        }
        for (Element feature : replacements) {
            Element replace = trxRequest.createElementNS(Namespaces.WFS, "Replace");
            replace.setPrefix("wfs");
            replace.appendChild(trxRequest.importNode(feature, true));
            Element filter = WFSMessage.newResourceIdFilter(feature.getAttributeNS(Namespaces.GML, "id"));
            replace.appendChild(trxRequest.adoptNode(filter));
            docElem.appendChild(replace);
        }
        if (TestSuiteLogger.isLoggable(Level.FINE)) {
            TestSuiteLogger.log(Level.FINE, XMLUtils.writeNodeToString(trxRequest));
        }
    }

    /**
     * Appends a wfs:Insert element to the document element in the given request
     * entity. The wfs:Insert element contains the supplied feature instance.
     * 
     * @param request
     *            A Document node representing a wfs:Transaction request entity.
     * @param feature
     *            A Node representing a GML feature instance.
     */
    public static void addInsertStatement(Document request, Node feature) {
        Element docElem = request.getDocumentElement();
        if (!docElem.getLocalName().equals(WFS2.TRANSACTION)) {
            throw new IllegalArgumentException("Document node is not a Transaction request: " + docElem.getNodeName());
        }
        Element insert = request.createElementNS(Namespaces.WFS, "Insert");
        docElem.appendChild(insert);
        insert.appendChild(request.importNode(feature, true));
    }

    /**
     * Adds a ResourceId predicate to a GetFeature (or GetFeatureWithLock)
     * request entity that contains a simple query expression without a filter.
     * The identifiers should match features of the indicated type.
     * 
     * @param request
     *            The request entity (/wfs:GetFeature/[wfs:Query]).
     * @param idSet
     *            A {@literal Set} of feature identifiers that conform
     *            to the xsd:ID datatype.
     */
    public static void addResourceIdPredicate(Document request, Set idSet) {
        if (idSet.isEmpty())
            return;
        if (!request.getDocumentElement().getLocalName().startsWith(WFS2.GET_FEATURE)) {
            throw new IllegalArgumentException(
                    "Expected a GetFeature(WithLock) request: " + request.getDocumentElement().getNodeName());
        }
        NodeList queryList = request.getElementsByTagNameNS(Namespaces.WFS, WFS2.QUERY_ELEM);
        if (queryList.getLength() == 0) {
            throw new IllegalArgumentException(
                    "No wfs:Query element found in request: " + request.getDocumentElement().getNodeName());
        }
        Element filter = request.createElementNS(Namespaces.FES, "Filter");
        queryList.item(0).appendChild(filter);
        for (String id : idSet) {
            Element resourceId = request.createElementNS(Namespaces.FES, "ResourceId");
            resourceId.setAttribute("rid", id);
            filter.appendChild(resourceId);
        }
    }

    /**
     * Checks the given list of objects for the presence of a
     * {@link ProtocolBinding#GET} object.
     * 
     * @param testParams
     *            A list of objects representing test method parameters.
     * @return true if a {@literal ProtocolBinding#GET} object was found; false
     *         otherwise.
     */
    public static boolean containsGetProtocolBinding(Object[] testParams) {
        if (null == testParams || testParams.length == 0) {
            return false;
        }
        boolean foundGetBinding = false;
        for (Object param : testParams) {
            if (ProtocolBinding.class.isInstance(param)
                    && ProtocolBinding.class.cast(param).equals(ProtocolBinding.GET)) {
                foundGetBinding = true;
                break;
            }
        }
        return foundGetBinding;
    }

    /**
     * Finds elements in a DOM Document that correspond to the given collection
     * of element declarations.
     * 
     * @param doc
     *            A Document node containing an XML entity.
     * @param elemDeclarations
     *            A collection of element declarations.
     * @return A list of matching element nodes (it may be empty).
     */
    public static List findMatchingElements(Document doc, XSElementDeclaration... elemDeclarations) {
        LOGR.log(Level.FINE, String.format("In %s, find %s", doc.getDocumentElement().getNodeName(),
                Arrays.toString(elemDeclarations)));
        List nodes = new ArrayList<>();
        for (XSElementDeclaration decl : elemDeclarations) {
            NodeList matches = doc.getElementsByTagNameNS(decl.getNamespace(), decl.getName());
            LOGR.log(Level.FINE, String.format("Found %d instances of %s", matches.getLength(), decl));
            for (int i = 0; i < matches.getLength(); i++) {
                nodes.add(matches.item(i));
            }
        }
        return nodes;
    }

    /**
     * Returns the set of feature identifiers found in the given WFS response
     * entity.
     * 
     * @param doc
     *            A WFS response entity that may contain feature instances.
     * @param featureType
     *            The feature type of interest.
     * @return A set of feature identifiers (gml:id attribute values); it may be
     *         empty.
     */
    public static Set extractFeatureIdentifiers(Document doc, QName featureType) {
        Set idSet = new HashSet<>();
        if (null == featureType) { // alternative: use XPath if null
            throw new IllegalArgumentException("featureType is null");
        }
        NodeList features = doc.getElementsByTagNameNS(featureType.getNamespaceURI(), featureType.getLocalPart());
        for (int i = 0; i < features.getLength(); i++) {
            Element feature = (Element) features.item(i);
            idSet.add(feature.getAttributeNS(Namespaces.GML, "id"));
        }
        return idSet;
    }

    /**
     * Adds a temporal predicate to a GetFeature request entity. If the given
     * temporal element has no temporal reference (frame) it is assumed to use
     * the default frame (ISO 8601).
     * 
     * @param request
     *            The request entity (wfs:GetFeature).
     * @param temporalOp
     *            The name of a spatial operator.
     * @param gmlTime
     *            A Document containing a GML temporal primitive.
     * @param valueRef
     *            An Element (fes:ValueReference) that specifies the temporal
     *            property to check. If it is {@code null}, the predicate
     *            applies to all temporal properties.
     */
    public static void addTemporalPredicate(Document request, String temporalOp, Document gmlTime, Element valueRef) {
        if (!request.getDocumentElement().getLocalName().equals(WFS2.GET_FEATURE)) {
            throw new IllegalArgumentException(
                    "Not a GetFeature request: " + request.getDocumentElement().getNodeName());
        }
        Element queryElem = (Element) request.getElementsByTagNameNS(Namespaces.WFS, WFS2.QUERY_ELEM).item(0);
        if (null == queryElem) {
            throw new IllegalArgumentException("No Query element found in GetFeature request entity.");
        }
        Element filter = request.createElementNS(Namespaces.FES, "fes:Filter");
        queryElem.appendChild(filter);
        Element predicate = request.createElementNS(Namespaces.FES, "fes:" + temporalOp);
        filter.appendChild(predicate);
        if (null != valueRef) {
            predicate.appendChild(request.importNode(valueRef, true));
        }
        // import temporal element to avoid WRONG_DOCUMENT_ERR
        predicate.appendChild(request.importNode(gmlTime.getDocumentElement(), true));
    }
    
    /**
     * Sets the attribute CreateStoredQuery/StoredQueryDefinition/QueryExpressionText/Query/@typeNames to the passed
     * feature type name.
     * 
     * @param request
     *            request to modify, never null
     * @param featureTypeName
     *            name to set, never null
     */
    public static void setReturnTypesAndTypeNamesAttribute( Document request, QName featureTypeName ) {
        if ( !request.getDocumentElement().getLocalName().equals( WFS2.CREATE_STORED_QRY ) ) {
            throw new IllegalArgumentException( "Not a CreateStoredQuery request: "
                                                + request.getDocumentElement().getNodeName() );
        }
        Element storedQueryDefinition = (Element) request.getElementsByTagNameNS( Namespaces.WFS,
                                                                                  "StoredQueryDefinition" ).item( 0 );
        Element queryExpressionText = (Element) storedQueryDefinition.getElementsByTagNameNS( Namespaces.WFS,
                                                                                              "QueryExpressionText" ).item( 0 );
        Element queryElem = (Element) queryExpressionText.getElementsByTagNameNS( Namespaces.WFS, WFS2.QUERY_ELEM ).item( 0 );

        String prefix = "ns" + Integer.toString( (int) ( Math.random() * 100 ) );
        String typeNamesAttributeValue = prefix + ":" + featureTypeName.getLocalPart();
        queryExpressionText.setAttribute( "xmlns:" + prefix, featureTypeName.getNamespaceURI() );

        queryElem.setAttribute( "typeNames", typeNamesAttributeValue );
        queryExpressionText.setAttribute( "returnFeatureTypes", typeNamesAttributeValue );
    }    private static Document wrapEntityInSOAPEnvelopeWithNS( Source xmlSource, String soapNS ) {
        Document soapDoc = BUILDER.newDocument();
        Element soapEnv = soapDoc.createElementNS( soapNS, "soap:Envelope" );
        soapDoc.appendChild( soapEnv );
        Element soapBody = soapDoc.createElementNS( soapNS, "soap:Body" );
        soapEnv.appendChild( soapBody );
        appendContent( xmlSource, soapDoc, soapBody );
        return soapDoc;
    }

    private static void appendContent( Source xmlSource, Document soapDoc, Element soapBody ) {
        try {
            TransformerFactory tFactory = TransformerFactory.newInstance();
            Transformer idTransformer = tFactory.newTransformer();
            Document wfsReq = BUILDER.newDocument();
            idTransformer.transform( xmlSource, new DOMResult( wfsReq ) );
            soapBody.appendChild( soapDoc.importNode( wfsReq.getDocumentElement(), true ) );
        } catch ( Exception e ) {
            TestSuiteLogger.log( Level.WARNING,
                                 "Failed to create SOAP envelope from Source " + xmlSource.getSystemId(), e );
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy