ddf.test.itests.catalog.TestSpatial.java Source code

Java tutorial

Introduction

Here is the source code for ddf.test.itests.catalog.TestSpatial.java

Source

/**
 * Copyright (c) Codice Foundation
 *
 * <p>This 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 3 of
 * the License, or any later version.
 *
 * <p>This program 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. A copy of the GNU Lesser General Public
 * License is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 */
package ddf.test.itests.catalog;

import static com.jayway.restassured.RestAssured.given;
import static com.jayway.restassured.RestAssured.when;
import static com.xebialabs.restito.builder.stub.StubHttp.whenHttp;
import static com.xebialabs.restito.semantics.Condition.get;
import static com.xebialabs.restito.semantics.Condition.parameter;
import static com.xebialabs.restito.semantics.Condition.post;
import static com.xebialabs.restito.semantics.Condition.withPostBodyContaining;
import static ddf.catalog.Constants.DEFAULT_PAGE_SIZE;
import static org.codice.ddf.itests.common.catalog.CatalogTestCommons.ingest;
import static org.codice.ddf.itests.common.catalog.CatalogTestCommons.ingestCswRecord;
import static org.codice.ddf.itests.common.catalog.CatalogTestCommons.ingestGeoJson;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasXPath;
import static org.junit.Assert.assertTrue;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.jayway.restassured.path.xml.XmlPath;
import com.jayway.restassured.specification.RequestSpecification;
import com.xebialabs.restito.semantics.Action;
import com.xebialabs.restito.semantics.Condition;
import com.xebialabs.restito.server.StubServer;
import ddf.catalog.data.types.Location;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import org.apache.commons.lang.text.StrSubstitutor;
import org.apache.http.HttpStatus;
import org.codice.ddf.itests.common.AbstractIntegrationTest;
import org.codice.ddf.itests.common.XmlSearch;
import org.codice.ddf.itests.common.annotations.ConditionalIgnoreRule;
import org.codice.ddf.itests.common.annotations.ConditionalIgnoreRule.ConditionalIgnore;
import org.codice.ddf.itests.common.annotations.SkipUnstableTest;
import org.codice.ddf.test.common.LoggingUtils;
import org.codice.ddf.test.common.annotations.BeforeExam;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.ops4j.pax.exam.junit.PaxExam;
import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
import org.ops4j.pax.exam.spi.reactors.PerSuite;
import org.osgi.framework.FrameworkUtil;

@RunWith(PaxExam.class)
@ExamReactorStrategy(PerSuite.class)
public class TestSpatial extends AbstractIntegrationTest {

    private static final String CSW_RESPONSE_COUNTRY_CODE = "GBR";

    private static final String CSW_RESOURCE_ROOT = "/TestSpatial/";

    private static final String CSW_QUERY_RESOURCES = CSW_RESOURCE_ROOT + "csw/request/query/";

    private static final String CSW_PAGING_METACARD = CSW_RESOURCE_ROOT + "csw/record/CswPagingRecord.xml";

    private static final String CSW_METACARD = "CswRecord.xml";

    private static final String GEOJSON_NEAR_METACARD = "GeoJson near";

    private static final String GEOJSON_FAR_METACARD = "GeoJson far";

    private static final String PLAINXML_NEAR_METACARD = "PlainXml near";

    private static final String PLAINXML_FAR_METACARD = "PlainXml far";

    private static final String TEXT_XML_UTF_8 = "text/xml;charset=UTF-8";

    private static final String WFS_11_SYMBOLIC_NAME = "spatial-wfs-v1_1_0-source";

    private static final String WFS_11_FACTORY_PID = "Wfs_v110_Federated_Source";

    private static final String WFS_11_SOURCE_ID = "WFS 1.1 Source";

    private static final String WFS_11_CONTEXT = "/mockWfs/11";

    private String restitoStubServerPath;

    private StubServer server;

    private static Map<String, String> savedCswQueries = new HashMap<>();

    private static Map<String, String> metacardIds = new HashMap<>();

    private final ImmutableMap<String, ExpectedResultPair[]> cswExpectedResults = ImmutableMap
            .<String, ExpectedResultPair[]>builder()
            .put("CswAfterDateQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, GEOJSON_NEAR_METACARD) })
            .put("CswBeforeDateQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, PLAINXML_FAR_METACARD) })
            .put("CswContainingWktLineStringQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, GEOJSON_FAR_METACARD) })
            .put("CswContainingWktPolygonQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, PLAINXML_FAR_METACARD) })
            .put("CswDuringDatesQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, GEOJSON_FAR_METACARD) })
            .put("CswEqualToTextQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, CSW_METACARD) })
            .put("CswIntersectingWktLineStringQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, PLAINXML_NEAR_METACARD) })
            .put("CswIntersectingWktPolygonQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, GEOJSON_NEAR_METACARD) })
            .put("CswLikeTextQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, GEOJSON_NEAR_METACARD) })
            .put("CswNearestToWktLineStringQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, PLAINXML_NEAR_METACARD) })
            .put("CswNearestToWktPolygonQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, GEOJSON_NEAR_METACARD),
                            new ExpectedResultPair(ResultType.TITLE, PLAINXML_NEAR_METACARD) })
            .put("CswOverLappingDatesQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, GEOJSON_NEAR_METACARD) })
            .put("CswWithinBufferWktLineStringQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, PLAINXML_NEAR_METACARD) })
            .put("CswWithinBufferWktPolygonQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, GEOJSON_NEAR_METACARD) })
            .put("CswWithinWktPolygonQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, GEOJSON_NEAR_METACARD) })
            .put("CswCompoundLikeTextAndIntersectingWktLineString",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, PLAINXML_NEAR_METACARD) })
            .put("CswCompoundAfterDateAndIntersectingWktPolygon",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, GEOJSON_NEAR_METACARD) })
            .put("CswCompoundBeforeDateAndLikeText",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, PLAINXML_FAR_METACARD) })
            .put("CswCompoundNotBeforeDateAndLikeText",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, CSW_METACARD) })
            .put("CswLogicalOperatorContextualNotQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, PLAINXML_NEAR_METACARD) })
            .put("CswLogicalOperatorContextualOrQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, PLAINXML_NEAR_METACARD),
                            new ExpectedResultPair(ResultType.TITLE, GEOJSON_FAR_METACARD) })
            .put("CswXPathExpressionQueryWithAttributeSelector",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, PLAINXML_FAR_METACARD) })
            .put("CswXPathExpressionQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, CSW_METACARD) })
            .put("CswFuzzyTextQuery",
                    new ExpectedResultPair[] { new ExpectedResultPair(ResultType.TITLE, CSW_METACARD) })
            .put("CswCompoundNot", new ExpectedResultPair[] { new ExpectedResultPair(ResultType.COUNT, "0") })
            .build();

    @Rule
    public ConditionalIgnoreRule rule = new ConditionalIgnoreRule();

    /** Loads the resource queries into memory. */
    public static Map<String, String> loadResourceQueries(String resourcesPath, Map<String, String> savedQueries) {

        // gets a list of resources available within the resource bundle
        Enumeration<URL> queryResourcePaths = FrameworkUtil.getBundle(AbstractIntegrationTest.class)
                .getBundleContext().getBundle().findEntries(resourcesPath, "*", false);

        while (queryResourcePaths.hasMoreElements()) {
            String queryResourcePath = queryResourcePaths.nextElement().getPath();
            if (!queryResourcePath.endsWith("/")) {
                String queryName = queryResourcePath.substring(queryResourcePath.lastIndexOf("/") + 1);
                savedQueries.put(removeFileExtension(queryName), getFileContent(queryResourcePath));
            }
        }
        return savedQueries;
    }

    private static String removeFileExtension(String file) {
        return file.contains(".") ? file.substring(0, file.lastIndexOf(".")) : file;
    }

    public static Map<String, String> ingestMetacards(Map<String, String> metacardsIds) {
        // ingest csw
        String cswRecordId = ingestCswRecord(getFileContent(CSW_RESOURCE_ROOT + "csw/record/CswRecord.xml"));
        metacardsIds.put(CSW_METACARD, cswRecordId);

        // ingest xml
        String plainXmlNearId = ingest(getFileContent(CSW_RESOURCE_ROOT + "xml/PlainXmlNear.xml"),
                MediaType.TEXT_XML);
        String plainXmlFarId = ingest(getFileContent(CSW_RESOURCE_ROOT + "xml/PlainXmlFar.xml"),
                MediaType.TEXT_XML);
        metacardsIds.put(PLAINXML_NEAR_METACARD, plainXmlNearId);
        metacardsIds.put(PLAINXML_FAR_METACARD, plainXmlFarId);

        // ingest json
        String geoJsonNearId = ingestGeoJson(getFileContent(CSW_RESOURCE_ROOT + "json/GeoJsonNear.json"));
        String geoJsonFarId = ingestGeoJson(getFileContent(CSW_RESOURCE_ROOT + "json/GeoJsonFar.json"));
        metacardsIds.put(GEOJSON_NEAR_METACARD, geoJsonNearId);
        metacardsIds.put(GEOJSON_FAR_METACARD, geoJsonFarId);

        return metacardsIds;
    }

    @BeforeExam
    public void beforeExam() throws Exception {
        try {
            waitForSystemReady();
            getSecurityPolicy().configureRestForGuest();
            waitForSystemReady();
            setupMockServer();

            getCatalogBundle().waitForFederatedSource(WFS_11_SOURCE_ID);
            getServiceManager().waitForSourcesToBeAvailable(REST_PATH.getUrl(), WFS_11_SOURCE_ID);

            loadResourceQueries(CSW_QUERY_RESOURCES, savedCswQueries);
            getServiceManager().startFeature(true, "spatial-wps");
            getServiceManager().startFeature(true, "sample-process");

        } catch (Exception e) {
            LoggingUtils.failWithThrowableStacktrace(e, "Failed to start required apps: ");
        }
    }

    @Before
    public void setup() throws Exception {
        clearCatalogAndWait();
    }

    private void setupMockServer() throws IOException {
        DynamicPort restitoStubServerPort = new DynamicPort("org.codice.ddf.system.restito_stub_server_port", 6);
        restitoStubServerPath = DynamicUrl.INSECURE_ROOT + restitoStubServerPort.getPort();
        server = new StubServer(Integer.parseInt(restitoStubServerPort.getPort())).run();
        setupWfs11(restitoStubServerPort.getPort());
    }

    private void setupWfs11(String mockServerPort) throws IOException {
        FederatedSourceProperties wfs11SourceProperties = new FederatedSourceProperties(WFS_11_SOURCE_ID,
                WFS_11_CONTEXT, WFS_11_SYMBOLIC_NAME, WFS_11_FACTORY_PID, "wfsUrl");

        String wfs11GetCapabilities = getFileContent("/TestSpatial/xml/WFS_11_GetCapabilities.xml")
                .replaceAll("\\$\\{PORT}", mockServerPort);
        String wfs11sfRoadsFeatureType = getFileContent("/TestSpatial/xml/WFS_11_sfRoadsFeatureType.xsd");
        String sfRoad1 = getFileContent("/TestSpatial/xml/sfRoad1.xml");
        String sfRoad2 = getFileContent("/TestSpatial/xml/sfRoad2.xml");
        String sfRoad3 = getFileContent("/TestSpatial/xml/sfRoad3.xml");

        setupWfs11GetCapabilities(wfs11GetCapabilities);
        setupWfs11DescribeFeature(wfs11sfRoadsFeatureType);
        // wildcard
        setupWfs11Query(wfsResponse(sfRoad1, sfRoad2, sfRoad3), withPostBodyContaining("Literal>*"));

        // keyword
        setupWfs11Query(wfsResponse(sfRoad1), withPostBodyContaining(">roads.1"));

        // boolean
        setupWfs11Query(wfsResponse(sfRoad2, sfRoad3), withPostBodyContaining("And>"));

        // geometry
        setupWfs11Query(wfsResponse(sfRoad1), withPostBodyContaining("coordinates>"));

        // ID Search
        whenHttp(server).match(post(WFS_11_CONTEXT), withPostBodyContaining("FeatureId")).then(Action.success(),
                Action.stringContent(wfsResponse(sfRoad1)));

        getServiceManager().createManagedService(WFS_11_FACTORY_PID, wfs11SourceProperties);
    }

    private void setupWfs11GetCapabilities(String getCapabilities) {
        whenHttp(server)
                .match(get(WFS_11_CONTEXT), parameter("service", "WFS"), parameter("version", "1.1.0"),
                        parameter("request", "GetCapabilities"))
                .then(Action.success(), Action.stringContent(getCapabilities));
    }

    private void setupWfs11DescribeFeature(String featureDescription) {
        whenHttp(server)
                .match(get(WFS_11_CONTEXT), parameter("service", "WFS"), parameter("version", "1.1.0"),
                        parameter("request", "DescribeFeatureType"))
                .then(Action.success(), Action.stringContent(featureDescription));
    }

    private void setupWfs11Query(String response, Condition... conditions) {
        List<Condition> queryConditions = new ArrayList<>();
        queryConditions.add(post(WFS_11_CONTEXT));
        queryConditions.add(withPostBodyContaining("GetFeature"));
        queryConditions.add(withPostBodyContaining("sf:roads"));
        queryConditions.addAll(Arrays.asList(conditions));

        whenHttp(server).match(queryConditions.toArray(new Condition[0])).then(Action.success(),
                Action.stringContent(response));
    }

    @After
    public void tearDown() throws Exception {
        clearCatalog();
    }

    @Test
    public void testCswPagingQuery() throws Exception {
        // Set to internal paging size
        int pageSize = 500;
        Set<String> ingestIds = ingestPagingRecords(pageSize + 11);
        int ingestCount = ingestIds.size();
        assertThat("Ingest count not equal to expected", ingestCount, is(pageSize + 11));

        ImmutableList<Integer> maxSizes = ImmutableList.of(DEFAULT_PAGE_SIZE - 5, DEFAULT_PAGE_SIZE, pageSize - 5,
                pageSize, pageSize + 7, ingestCount, ingestCount + 1);

        for (Integer maxSize : maxSizes) {
            String query = getPagingMaxRecordsQuery(maxSize);
            String cswResponse = sendCswQuery(query);

            int expectedResults = (maxSize <= ingestCount) ? maxSize : ingestCount;

            assertTrue("The responses contained a different number of matches; expected " + ingestCount,
                    hasExpectedMatchCount(cswResponse, new ExpectedResultPair(ResultType.COUNT, ingestCount + "")));

            assertTrue("The responses contained a different result count; expected " + expectedResults,
                    hasExpectedResultCount(cswResponse,
                            new ExpectedResultPair(ResultType.COUNT, expectedResults + "")));
        }
    }

    @Test
    public void testCswAfterDateQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswAfterDateQuery");
    }

    @Test
    public void testCswBeforeDateQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswBeforeDateQuery");
    }

    @Test
    public void testCswContainingWktLineStringQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswContainingWktLineStringQuery");
    }

    @Test
    public void testCswContainingWktPolygonQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswContainingWktPolygonQuery");
    }

    @Test
    public void testCswDuringDatesQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswDuringDatesQuery");
    }

    @Test
    public void testCswEqualToTextQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswEqualToTextQuery");
    }

    @Test
    public void testCswIntersectingWktLineStringQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswIntersectingWktLineStringQuery");
    }

    @Test
    public void testCswIntersectingWktPolygonQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswIntersectingWktPolygonQuery");
    }

    @Test
    public void testCswLikeTextQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswLikeTextQuery");
    }

    @Test
    public void testCswNearestToWktLineStringQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswNearestToWktLineStringQuery");
    }

    @Test
    public void testCswNearestToWktPolygonQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswNearestToWktPolygonQuery");
    }

    @Test
    public void testCswOverLappingDatesQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswOverLappingDatesQuery");
    }

    @Test
    public void testCswWithinBufferWktLineStringQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswWithinBufferWktLineStringQuery");
    }

    @Test
    public void testCswWithinBufferWktPolygonQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswWithinBufferWktPolygonQuery");
    }

    @Test
    public void testCswWithinWktPolygonQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswWithinWktPolygonQuery");
    }

    @Test
    public void testCswCompoundAfterDateAndIntersectingWktPolygon() throws Exception {

        performQueryAndValidateExpectedResults("CswCompoundAfterDateAndIntersectingWktPolygon");
    }

    @Test
    public void testCswCompoundBeforeDateAndLikeText() throws Exception {

        performQueryAndValidateExpectedResults("CswCompoundBeforeDateAndLikeText");
    }

    @Test
    public void testCswCompoundNotBeforeDateAndLikeText() throws Exception {

        performQueryAndValidateExpectedResults("CswCompoundNotBeforeDateAndLikeText");
    }

    @Test
    public void testCswCompoundLikeTextAndIntersectingWktLineString() throws Exception {

        performQueryAndValidateExpectedResults("CswCompoundLikeTextAndIntersectingWktLineString");
    }

    @Test
    public void testCswLogicalOperatorContextualNotQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswLogicalOperatorContextualNotQuery");
    }

    @Test
    public void testCswLogicalOperatorContextualOrQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswLogicalOperatorContextualOrQuery");
    }

    @Test
    public void testCswXPathExpressionQueryWithAttributeSelector() throws Exception {

        performQueryAndValidateExpectedResults("CswXPathExpressionQueryWithAttributeSelector");
    }

    @Test
    public void testCswXPathExpressionQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswXPathExpressionQuery");
    }

    @Test
    public void testCswCompoundNot() throws Exception {

        performQueryAndValidateExpectedResults("CswCompoundNot");
    }

    @Test
    public void testCswFuzzyTextQuery() throws Exception {

        performQueryAndValidateExpectedResults("CswFuzzyTextQuery");
    }

    @Test
    @ConditionalIgnore(condition = SkipUnstableTest.class) // DDF-3032
    public void testGeoCoderPlugin() throws Exception {
        getServiceManager().startFeature(true, "webservice-gazetteer");
        String id = ingestCswRecord(getFileContent(CSW_RECORD_RESOURCE_PATH + "/CswRecord2"));

        String queryUrl = REST_PATH.getUrl() + "sources/ddf.distribution/" + id + "?format=xml";

        when().get(queryUrl).then().and().assertThat()
                .body(hasXPath("/metacard/string[@name='" + Location.COUNTRY_CODE + "']/value/text()",
                        equalTo(CSW_RESPONSE_COUNTRY_CODE)));
    }

    @Test
    public void testWpsGetCapabilities() throws Exception {
        given().get(SERVICE_ROOT.getUrl() + "/wps?service=WPS&request=GetCapabilities").then().assertThat().body(
                hasXPath(
                        "count(/*[local-name()='Capabilities']/*[local-name()='Contents']/*[local-name()='ProcessSummary'])",
                        Matchers.is("5")),
                hasXPath(
                        "/*[local-name()='Capabilities']/*[local-name()='Contents']/*[local-name()='ProcessSummary']/*[local-name()='Identifier' and text()='geojson']"));
    }

    @Test
    public void testWpsDescribeProcess() throws Exception {
        given().get(SERVICE_ROOT.getUrl() + "/wps?service=WPS&request=DescribeProcess&identifier=geojson").then()
                .assertThat()
                .body(hasXPath("count(/*[local-name()='ProcessOfferings']/*[local-name()='ProcessOffering'])",
                        Matchers.is("1")),
                        hasXPath(
                                "/*[local-name()='ProcessOfferings']/*[local-name()='ProcessOffering']/*[local-name()='Process']/*[local-name()='Identifier' and text()='geojson']"));
    }

    @Test
    public void testWpsExecute() throws Exception {
        ingestMetacards(metacardIds);
        final String requestXml = getFileContent("ExecuteRequest.xml",
                ImmutableMap.of("id", "csw:Record", "metacardId", metacardIds.get(PLAINXML_NEAR_METACARD)),
                AbstractIntegrationTest.class);
        given().header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML).body(requestXml)
                .post(SERVICE_ROOT.getUrl() + "/wps?service=WPS&request=Execute").then().assertThat()
                .body(hasXPath("count(/*[local-name()='Result']/*[local-name()='Output'])", Matchers.is("1")));
    }

    @Test
    public void testWfs11Wildcard() {
        ingestMetacards(metacardIds);
        String queryUrl = OPENSEARCH_PATH + "?q=*&format=xml&src=" + WFS_11_SOURCE_ID;
        String responseXml = given().request().get(queryUrl).andReturn().body().asString();
        assertMetacards(responseXml, 3, WFS_11_SOURCE_ID, "roads");
    }

    @Test
    public void testWfs11Keyword() {
        ingestMetacards(metacardIds);
        String queryUrl = OPENSEARCH_PATH + "?q=roads.1&format=xml&src=" + WFS_11_SOURCE_ID;
        String responseXml = given().request().get(queryUrl).andReturn().body().asString();
        assertMetacards(responseXml, 1, WFS_11_SOURCE_ID, "roads");
    }

    @Test
    public void testWfs11Boolean() {
        ingestMetacards(metacardIds);
        String queryUrl = OPENSEARCH_PATH + "?q=roads.2 AND roads.3&format=xml&src=" + WFS_11_SOURCE_ID;
        String responseXml = given().request().get(queryUrl).andReturn().body().asString();
        assertMetacards(responseXml, 2, WFS_11_SOURCE_ID, "roads");
    }

    @Test
    public void testWfs11Id() {
        ingestMetacards(metacardIds);
        String queryUrl = REST_PATH.getUrl() + "sources/" + WFS_11_SOURCE_ID + "/roads.1";
        String responseXml = given().request().get(queryUrl).andReturn().body().asString();
        assertMetacard(responseXml, WFS_11_SOURCE_ID, "roads");
    }

    @Test
    public void testWfs11Geo() {
        ingestMetacards(metacardIds);
        String queryUrl = OPENSEARCH_PATH + "?lat=90&lon=90&radius=1000&format=xml&src=" + WFS_11_SOURCE_ID;
        String responseXml = given().request().get(queryUrl).andReturn().body().asString();
        assertMetacards(responseXml, 1, WFS_11_SOURCE_ID, "roads");
    }

    private String wfsResponse(String... featureMembers) {
        StringBuilder sb = new StringBuilder();
        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>").append("<wfs:FeatureCollection\n")
                .append("  xmlns:sf=\"http://www.openplans.org/spearfish\"\n")
                .append("  xmlns:wfs=\"http://www.opengis.net/wfs\"\n")
                .append("  xmlns:gml=\"http://www.opengis.net/gml\"\n").append("  numberOfFeatures=\"")
                .append(featureMembers.length).append("\"\n").append("  timeStamp=\"2018-05-11T00:11:44.120Z\">\n")
                .append("  <gml:featureMembers>\n");

        for (String featureMember : featureMembers) {
            sb.append(featureMember).append("\n");
        }

        sb.append("</gml:featureMembers>\n").append("</wfs:FeatureCollection>");

        return sb.toString();
    }

    private void assertMetacard(String responseXml, String expectedSource, String expectedType) {
        XmlPath xmlPath = new XmlPath(responseXml);
        int numMetacards = xmlPath.get("metacard.size()");
        assertThat(numMetacards, equalTo(1));
        assertThat(xmlPath.get("metacard.type"), equalTo(expectedType));
        assertThat(xmlPath.get("metacard.source"), equalTo(expectedSource));
    }

    private void assertMetacards(String responseXml, int expectedNumMetacards, String expectedSource,
            String expectedType) {
        XmlPath xmlPath = new XmlPath(responseXml);
        int numMetacards = xmlPath.get("metacards.metacard.size()");
        assertThat(numMetacards, equalTo(expectedNumMetacards));

        for (int i = 0; i < numMetacards; i++) {
            assertThat(xmlPath.get("metacards.metacard[" + i + "].type"), equalTo(expectedType));
            assertThat(xmlPath.get("metacards.metacard[" + i + "].source"), equalTo(expectedSource));
        }
    }

    /**
     * Ingests data, performs and validates the query returns the correct results.
     *
     * @param queryType - The query that is performed
     */
    private void performQueryAndValidateExpectedResults(String queryType) throws Exception {
        ingestMetacards(metacardIds);

        String cswQuery = savedCswQueries.get(queryType);

        String cswResponse = sendCswQuery(cswQuery);

        hasExpectedResults(cswResponse, cswExpectedResults.get(queryType));
    }

    private String sendCswQuery(String query) {
        RequestSpecification queryRequest = given().body(query);

        queryRequest = queryRequest.header("Content-Type", TEXT_XML_UTF_8);

        return queryRequest.when().post(CSW_PATH.getUrl()).then().assertThat().statusCode(equalTo(HttpStatus.SC_OK))
                .extract().response().getBody().asString();
    }

    /**
     * Validates that the results from the query are correct.
     *
     * @param queryResult - The result obtained from sending the query
     * @param expectedValues - The values expected within the results
     */
    private void hasExpectedResults(String queryResult, ExpectedResultPair[] expectedValues) throws Exception {
        if (expectedValues[0].type == ResultType.COUNT) {
            assertTrue("The responses contained a different count",
                    hasExpectedResultCount(queryResult, expectedValues[0]));
        } else if (expectedValues[0].type == ResultType.TITLE) {
            // assertion done within the method
            hasExpectedMetacardsReturned(queryResult, expectedValues);
        } else {
            assertTrue("The expected values are an invalid type", false);
        }
    }

    /**
     * Validates that the query returned the expected metacards.
     *
     * @param queryResult - The result obtained from sending the query
     * @param expectedValues - The values expected within the result
     */
    private boolean hasExpectedMetacardsReturned(String queryResult, ExpectedResultPair[] expectedValues)
            throws Exception {
        boolean testPassed = false;

        for (int i = 0; i < expectedValues.length; i++) {
            assertTrue("Metacard: " + expectedValues[i].value + " not found in result.",
                    testPassed = queryResult.contains(metacardIds.get(expectedValues[i].value)));
        }
        return testPassed;
    }

    /**
     * Validates that the query matched the expected result count.
     *
     * @param queryResult - The result obtained from sending the query
     * @param expectedValue - The values expected within the result
     * @return true if the {@code numberOfRecordsMatched} matches the expected value
     * @throws Exception if an error occurs parsing the XML response
     */
    private boolean hasExpectedMatchCount(String queryResult, ExpectedResultPair expectedValue) throws Exception {

        String originalCount = XmlSearch.evaluate("//@numberOfRecordsMatched", queryResult);

        return originalCount.equals(expectedValue.value);
    }

    /**
     * Validates that the query returned the expected result count.
     *
     * @param queryResult - The result obtained from sending the query
     * @param expectedValue - The values expected within the result
     * @return true if the {@code numberOfRecordsReturned} matches the expected value
     * @throws Exception if an error occurs parsing the XML response
     */
    private boolean hasExpectedResultCount(String queryResult, ExpectedResultPair expectedValue) throws Exception {

        String originalCount = XmlSearch.evaluate("//@numberOfRecordsReturned", queryResult);

        return originalCount.equals(expectedValue.value);
    }

    private String getPagingMaxRecordsQuery(int maxRecords) {
        String rawCswQuery = savedCswQueries.get("CswPagingTestLikeQuery");
        StrSubstitutor strSubstitutor = new StrSubstitutor(ImmutableMap.of("maxRecords", "" + maxRecords));

        strSubstitutor.setVariablePrefix(RESOURCE_VARIABLE_DELIMETER);
        strSubstitutor.setVariableSuffix(RESOURCE_VARIABLE_DELIMETER);
        return strSubstitutor.replace(rawCswQuery);
    }

    private Set<String> ingestPagingRecords(int number) {
        String rawCswRecord = getFileContent(CSW_PAGING_METACARD);

        Set<String> pagingIds = new HashSet<>();
        for (int i = 1; i <= number; i++) {
            String identifier = UUID.randomUUID().toString().replaceAll("-", "");
            String cswPagingRecord = substitutePagingParams(rawCswRecord, i, identifier);

            String id = ingestCswRecord(cswPagingRecord);

            pagingIds.add(id);
            metacardIds.put(id, id);
        }

        return pagingIds;
    }

    private String substitutePagingParams(String rawCswRecord, int testNum, String identifier) {
        StrSubstitutor strSubstitutor = new StrSubstitutor(
                ImmutableMap.of("identifier", identifier, "testNum", "" + testNum));

        strSubstitutor.setVariablePrefix(RESOURCE_VARIABLE_DELIMETER);
        strSubstitutor.setVariableSuffix(RESOURCE_VARIABLE_DELIMETER);
        return strSubstitutor.replace(rawCswRecord);
    }

    public enum ResultType {
        TITLE, COUNT
    }

    public class ExpectedResultPair {

        String value;

        ResultType type;

        public ExpectedResultPair(ResultType type, String value) {
            this.value = value;
            this.type = type;
        }
    }

    public class FederatedSourceProperties extends HashMap<String, Object> {

        public FederatedSourceProperties(String sourceId, String context, String symbolicName, String factoryPid,
                String urlPropName) {
            this.putAll(getServiceManager().getMetatypeDefaults(symbolicName, factoryPid));

            this.put("id", sourceId);
            this.put(urlPropName, restitoStubServerPath + context);
            this.put("pollInterval", 1);
        }
    }
}