org.geoserver.kml.KMLReflectorTest.java Source code

Java tutorial

Introduction

Here is the source code for org.geoserver.kml.KMLReflectorTest.java

Source

/* Copyright (c) 2001 - 2013 OpenPlans - www.openplans.org. All rights reserved.
 * This code is licensed under the GPL 2.0 license, available at the root
 * application directory.
 */
package org.geoserver.kml;

import static org.custommonkey.xmlunit.XMLAssert.assertXpathEvaluatesTo;
import static org.custommonkey.xmlunit.XMLAssert.assertXpathExists;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;

import javax.xml.namespace.QName;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.custommonkey.xmlunit.XMLAssert;
import org.custommonkey.xmlunit.XMLUnit;
import org.custommonkey.xmlunit.XpathEngine;
import org.geoserver.catalog.Catalog;
import org.geoserver.catalog.FeatureTypeInfo;
import org.geoserver.config.GeoServer;
import org.geoserver.config.GeoServerInfo;
import org.geoserver.data.test.MockData;
import org.geoserver.data.test.SystemTestData;
import org.geoserver.ows.kvp.FormatOptionsKvpParser;
import org.geoserver.ows.util.KvpUtils;
import org.geoserver.wms.GetMapRequest;
import org.geoserver.wms.WMSMapContent;
import org.geoserver.wms.WMSTestSupport;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.junit.Test;
import org.springframework.util.xml.SimpleNamespaceContext;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;

import com.mockrunner.mock.web.MockHttpServletResponse;

/**
 * Some functional tests for kml reflector
 * 
 * @author David Winslow (OpenGeo)
 * @author Gabriel Roldan (OpenGeo)
 * @version $Id$
 */
public class KMLReflectorTest extends WMSTestSupport {

    @Override
    protected void onSetUp(SystemTestData testData) throws Exception {
        super.onSetUp(testData);
        Catalog catalog = getCatalog();
        testData.addStyle("Bridge", "bridge.sld", getClass(), catalog);
        testData.addStyle("allsymbolizers", "allsymbolizers.sld", getClass(), catalog);
        testData.addStyle("labels", "labels.sld", getClass(), catalog);
        testData.addStyle("SingleFeature", "singlefeature.sld", getClass(), catalog);
        testData.addStyle("BridgeSubdir", "bridgesubdir.sld", getClass(), catalog);
        testData.addStyle("dynamicsymbolizer", "dynamicsymbolizer.sld", getClass(), catalog);
        testData.addStyle("relativeds", "relativeds.sld", getClass(), catalog);
        testData.addStyle("big-local-image", "big-local-image.sld", getClass(), catalog);
        testData.addStyle("big-mark", "big-mark.sld", getClass(), catalog);
        testData.copyTo(getClass().getResourceAsStream("bridge.png"), "styles/bridge.png");
        testData.copyTo(getClass().getResourceAsStream("planet-42.png"), "styles/planet-42.png");
        File stylesDir = new File(testData.getDataDirectoryRoot(), "styles");
        new File(stylesDir, "graphics").mkdir();
        testData.copyTo(getClass().getResourceAsStream("bridge.png"), "styles/graphics/bridgesubdir.png");
    }

    /**
     * Verify that NetworkLink's generated by the reflector do not include a BBOX parameter, since
     * that would override the BBOX provided by Google Earth.
     * 
     * @see http://jira.codehaus.org/browse/GEOS-2185
     */
    @Test
    public void testNoBBOXInHREF() throws Exception {
        final String layerName = MockData.BASIC_POLYGONS.getPrefix() + ":" + MockData.BASIC_POLYGONS.getLocalPart();
        final XpathEngine xpath = XMLUnit.newXpathEngine();
        String requestURL = "wms/kml?mode=refresh&layers=" + layerName;
        Document dom = getAsDOM(requestURL);
        // print(dom);
        assertXpathEvaluatesTo("1", "count(kml:kml/kml:Document)", dom);
        assertXpathEvaluatesTo("1", "count(kml:kml/kml:Document/kml:NetworkLink)", dom);
        assertXpathEvaluatesTo("1", "count(kml:kml/kml:Document/kml:LookAt)", dom);

        assertXpathEvaluatesTo(layerName, "kml:kml/kml:Document/kml:NetworkLink[1]/kml:name", dom);
        assertXpathEvaluatesTo("1", "kml:kml/kml:Document/kml:NetworkLink[1]/kml:open", dom);
        assertXpathEvaluatesTo("1", "kml:kml/kml:Document/kml:NetworkLink[1]/kml:visibility", dom);

        assertXpathEvaluatesTo("onStop", "kml:kml/kml:Document/kml:NetworkLink[1]/kml:Url/kml:viewRefreshMode",
                dom);
        assertXpathEvaluatesTo("1.0", "kml:kml/kml:Document/kml:NetworkLink[1]/kml:Url/kml:viewRefreshTime", dom);
        assertXpathEvaluatesTo("1.0", "kml:kml/kml:Document/kml:NetworkLink[1]/kml:Url/kml:viewBoundScale", dom);
        Map<String, Object> expectedKVP = KvpUtils.parseQueryString(
                "http://localhost:80/geoserver/wms?format_options=autofit%3Atrue%3BKMPLACEMARK%3Afalse%3BKMATTR%3Atrue%3BKMSCORE%3A40%3BSUPEROVERLAY%3Afalse%3B&service=wms&srs=EPSG%3A4326&width=2048&styles=BasicPolygons&height=2048&transparent=false&request=GetMap&layers=cite%3ABasicPolygons&format=application%2Fvnd.google-earth.kml+xml&version=1.1.1");
        Map<String, Object> resultedKVP = KvpUtils
                .parseQueryString(xpath.evaluate("kml:kml/kml:Document/kml:NetworkLink[1]/kml:Url/kml:href", dom));

        assertMapsEqual(expectedKVP, resultedKVP);

        String href = xpath.evaluate("kml:kml/kml:Document/kml:NetworkLink/kml:Link/kml:href", dom);
        Pattern badPattern = Pattern.compile("&bbox=", Pattern.CASE_INSENSITIVE);
        assertFalse(badPattern.matcher(href).matches());
    }

    /**
     * Verify that NetworkLink's generated by the reflector do not include a BBOX parameter, since
     * that would override the BBOX provided by Google Earth.
     * 
     * @see http://jira.codehaus.org/browse/GEOS-2185
     */
    @Test
    public void testBBOXInHREF() throws Exception {
        final XpathEngine xpath = XMLUnit.newXpathEngine();
        String requestURL = "wms/kml?layers=" + getLayerId(MockData.BASIC_POLYGONS)
                + "&bbox=-1,-1,-0.5,-0.5&mode=download";

        Document dom = getAsDOM(requestURL);
        // print(dom);

        assertEquals(1, xpath.getMatchingNodes("//kml:Placemark", dom).getLength());
    }

    @Test
    public void testDownloadMultiLayer() throws Exception {
        String requestURL = "wms/kml?&layers=" + getLayerId(MockData.LAKES) + "," + getLayerId(MockData.FORESTS);
        MockHttpServletResponse response = getAsServletResponse(requestURL);
        assertEquals(KMLMapOutputFormat.MIME_TYPE, response.getContentType());
        assertEquals("attachment; filename=cite-Lakes_cite-Forests.kml", response.getHeader("Content-Disposition"));
        Document dom = dom(getBinaryInputStream(response));
        // print(dom);
        assertXpathEvaluatesTo("1", "count(kml:kml/kml:Document)", dom);
        assertXpathEvaluatesTo("2", "count(kml:kml/kml:Document/kml:NetworkLink)", dom);
        assertXpathEvaluatesTo("2", "count(kml:kml/kml:Document/kml:NetworkLink/kml:LookAt)", dom);
    }

    /**
     * Do some spot checks on the KML generated when an overlay hierarchy is requested.
     */
    @Test
    public void testSuperOverlayReflection() throws Exception {
        final String layerName = MockData.BASIC_POLYGONS.getPrefix() + ":" + MockData.BASIC_POLYGONS.getLocalPart();

        final String requestUrl = "wms/kml?layers=" + layerName + "&styles=&mode=superoverlay";
        Document dom = getAsDOM(requestUrl);
        // print(dom);
        assertEquals("kml", dom.getDocumentElement().getLocalName());
        assertXpathExists("kml:kml/kml:Document/kml:Folder/kml:NetworkLink/kml:Link/kml:href", dom);
        assertXpathExists("kml:kml/kml:Document/kml:LookAt/kml:longitude", dom);
    }

    @Test
    public void testWmsRepeatedLayerWithNonStandardStyleAndCqlFiler() throws Exception {
        final String layerName = MockData.BASIC_POLYGONS.getPrefix() + ":" + MockData.BASIC_POLYGONS.getLocalPart();

        String requestUrl = "wms/kml?mode=refresh&layers=" + layerName + "," + layerName
                + "&styles=Default,Default&cql_filter=att1<10;att1>1000";
        Document dom = getAsDOM(requestUrl);

        assertEquals("kml", dom.getDocumentElement().getLocalName());

        assertXpathEvaluatesTo("2", "count(kml:kml/kml:Document/kml:NetworkLink)", dom);
        assertXpathEvaluatesTo(layerName, "kml:kml/kml:Document/kml:NetworkLink[1]/kml:name", dom);
        assertXpathEvaluatesTo(layerName, "kml:kml/kml:Document/kml:NetworkLink[2]/kml:name", dom);

        XpathEngine xpath = XMLUnit.newXpathEngine();

        String url1 = xpath.evaluate("/kml:kml/kml:Document/kml:NetworkLink[1]/kml:Url/kml:href", dom);
        String url2 = xpath.evaluate("/kml:kml/kml:Document/kml:NetworkLink[2]/kml:Url/kml:href", dom);

        assertNotNull(url1);
        assertNotNull(url2);

        Map<String, Object> kvp1 = KvpUtils.parseQueryString(url1);
        Map<String, Object> kvp2 = KvpUtils.parseQueryString(url2);

        assertEquals(layerName, kvp1.get("layers"));
        assertEquals(layerName, kvp2.get("layers"));

        assertEquals("Default", kvp1.get("styles"));
        assertEquals("Default", kvp2.get("styles"));

        assertEquals("att1<10", kvp1.get("cql_filter"));
        assertEquals("att1>1000", kvp2.get("cql_filter"));
    }

    /**
     * @see {@link KMLReflector#organizeFormatOptionsParams(Map, Map)}
     * @throws Exception
     */
    @Test
    public void testKmlFormatOptionsAsKVP() throws Exception {
        final String layerName = MockData.BASIC_POLYGONS.getPrefix() + ":" + MockData.BASIC_POLYGONS.getLocalPart();

        final String baseUrl = "wms/kml?layers=" + layerName + "&styles=&mode=superoverlay";
        final String requestUrl = baseUrl + "&kmltitle=myCustomLayerTitle&kmscore=10&legend=true&kmattr=true";
        Document dom = getAsDOM(requestUrl);
        XpathEngine xpath = XMLUnit.newXpathEngine();

        // print(dom);
        // all the kvp parameters (which should be set as format_options now are correctly parsed)
        String result = xpath.evaluate("//kml:NetworkLink/kml:Link/kml:href", dom);
        Map<String, Object> kvp = KvpUtils.parseQueryString(result);
        String formatOptions = (String) kvp.get("format_options");
        assertEquals(
                "LEGEND:true;SUPEROVERLAY:true;AUTOFIT:true;KMPLACEMARK:false;OVERLAYMODE:auto;KMSCORE:10;KMATTR:true;KMLTITLE:myCustomLayerTitle;",
                formatOptions);
    }

    @Test
    public void testKmlTitleFormatOption() throws Exception {
        final String layerName = MockData.BASIC_POLYGONS.getPrefix() + ":" + MockData.BASIC_POLYGONS.getLocalPart();

        final String requestUrl = "wms/kml?layers=" + layerName
                + "&styles=&mode=superoverlay&format_options=kmltitle:myCustomLayerTitle";
        // System.out.println(getAsServletResponse(requestUrl).getContentType());
        Document dom = getAsDOM(requestUrl);
        // print(dom);
        assertEquals("kml", dom.getDocumentElement().getLocalName());
        assertXpathEvaluatesTo("myCustomLayerTitle", "/kml:kml/kml:Document/kml:name", dom);
    }

    /**
     * See http://jira.codehaus.org/browse/GEOS-1947
     * 
     * @throws Exception
     */
    @Test
    public void testExternalGraphicBackround() throws Exception {
        final String requestUrl = "wms/kml?layers=" + getLayerId(MockData.BRIDGES) + "&styles=Bridge&mode=download";
        Document dom = getAsDOM(requestUrl);
        // print(dom);

        // make sure we are generating icon styles, but that we're not sticking a color onto them
        XMLAssert.assertXpathEvaluatesTo("1", "count(//kml:Style/kml:IconStyle/kml:Icon/kml:href)", dom);
        XMLAssert.assertXpathEvaluatesTo("0", "count(//kml:Style/kml:IconStyle/kml:Icon/kml:color)", dom);
    }

    /**
     * See http://jira.codehaus.org/browse/GEOS-3994
     * 
     * @throws Exception
     */
    @Test
    public void testExternalGraphicSubdir() throws Exception {
        final String requestUrl = "wms/kml?layers=" + getLayerId(MockData.BRIDGES)
                + "&styles=BridgeSubdir&mode=download";
        Document dom = getAsDOM(requestUrl);
        // print(dom);
        // make sure we are generating icon styles with the subdir path
        XMLAssert.assertXpathEvaluatesTo("http://localhost:8080/geoserver/styles/graphics/bridgesubdir.png",
                "//kml:Style[1]/kml:IconStyle/kml:Icon/kml:href", dom);
    }

    /**
     * See http://jira.codehaus.org/browse/GEOS-3965
     * 
     * @throws Exception
     */
    @Test
    public void testProxyBaseURL() throws Exception {
        GeoServer gs = getGeoServer();
        try {
            GeoServerInfo info = gs.getGlobal();
            info.getSettings().setProxyBaseUrl("http://myhost:9999/gs");
            gs.save(info);

            final String requestUrl = "wms/kml?layers=" + getLayerId(MockData.BRIDGES)
                    + "&styles=Bridge&mode=download";
            Document dom = getAsDOM(requestUrl);

            // make sure we are using the proxy base URL
            XMLAssert.assertXpathEvaluatesTo("http://myhost:9999/gs/styles/bridge.png",
                    "//kml:Style/kml:IconStyle/kml:Icon/kml:href", dom);
        } finally {
            GeoServerInfo info = gs.getGlobal();
            info.getSettings().setProxyBaseUrl(null);
            gs.save(info);
        }
    }

    @Test
    public void testFilteredData() throws Exception {
        // the style selects a single feature
        final String requestUrl = "wms/kml?layers=" + getLayerId(MockData.BASIC_POLYGONS)
                + "&styles=SingleFeature&mode=download";
        Document dom = getAsDOM(requestUrl);
        // print(dom);

        // check we have indeed a single feature
        assertXpathEvaluatesTo("1", "count(//kml:Placemark)", dom);
    }

    @Test
    public void testForceRasterKml() throws Exception {
        final String requestUrl = "wms/kml?layers=" + getLayerId(MockData.BASIC_POLYGONS)
                + "&styles=&mode=download&KMSCORE=0";
        Document dom = getAsDOM(requestUrl);
        // print(dom);

        assertXpathEvaluatesTo("1", "count(//kml:Folder/kml:GroundOverlay)", dom);
        String href = XMLUnit.newXpathEngine().evaluate("//kml:Folder/kml:GroundOverlay/kml:Icon/kml:href", dom);
        assertTrue(href.startsWith("http://localhost:8080/geoserver/wms"));
        assertTrue(href.contains("request=GetMap"));
        assertTrue(href.contains("format=image%2Fpng"));
    }

    @Test
    public void testForceRasterKmz() throws Exception {
        final String requestUrl = "wms/kml?layers=" + getLayerId(MockData.BASIC_POLYGONS)
                + "&styles=&mode=download&KMSCORE=0&format=" + KMZMapOutputFormat.MIME_TYPE;
        MockHttpServletResponse response = getAsServletResponse(requestUrl);
        assertEquals(KMZMapOutputFormat.MIME_TYPE, response.getContentType());
        assertEquals("attachment; filename=cite-BasicPolygons.kmz", response.getHeader("Content-Disposition"));

        ZipInputStream zis = new ZipInputStream(getBinaryInputStream(response));
        try {
            // first entry, the kml document itself
            ZipEntry entry = zis.getNextEntry();
            assertEquals("wms.kml", entry.getName());
            // we need to clone the input stream, as dom(is) closes the stream
            byte[] data = IOUtils.toByteArray(zis);
            Document dom = dom(new ByteArrayInputStream(data));
            assertXpathEvaluatesTo("1", "count(//kml:Folder/kml:GroundOverlay)", dom);
            String href = XMLUnit.newXpathEngine().evaluate("//kml:Folder/kml:GroundOverlay/kml:Icon/kml:href",
                    dom);
            assertEquals("images/layers_0.png", href);
            zis.closeEntry();

            // the images folder
            entry = zis.getNextEntry();
            assertEquals("images/", entry.getName());
            zis.closeEntry();

            // the ground overlay for the raster layer
            entry = zis.getNextEntry();
            assertEquals("images/layers_0.png", entry.getName());
            zis.closeEntry();
            assertNull(zis.getNextEntry());
        } finally {
            zis.close();
        }
    }

    @Test
    public void testRasterTransformerSLD() throws Exception {
        URL url = getClass().getResource("allsymbolizers.sld");
        final String requestUrl = "wms/kml?layers=" + getLayerId(MockData.BASIC_POLYGONS) + "&sld="
                + url.toExternalForm() + "&mode=download&KMSCORE=0";
        Document dom = getAsDOM(requestUrl);
        // print(dom);

        assertXpathEvaluatesTo("1", "count(//kml:Folder/kml:GroundOverlay)", dom);
        String href = XMLUnit.newXpathEngine().evaluate("//kml:Folder/kml:GroundOverlay/kml:Icon/kml:href", dom);
        href = URLDecoder.decode(href, "UTF-8");
        assertTrue(href.startsWith("http://localhost:8080/geoserver/wms"));
        assertTrue(href.contains("request=GetMap"));
        assertTrue(href.contains("format=image/png"));
        assertTrue(href.contains("&sld=" + url.toExternalForm()));
    }

    @Test
    public void testRasterPlacemarkTrue() throws Exception {
        doTestRasterPlacemark(true);
    }

    @Test
    public void testRasterPlacemarkFalse() throws Exception {
        doTestRasterPlacemark(false);
    }

    protected void doTestRasterPlacemark(boolean doPlacemarks) throws Exception {
        // the style selects a single feature
        final String requestUrl = "wms/kml?layers=" + getLayerId(MockData.BASIC_POLYGONS)
                + "&styles=&mode=download&kmscore=0&format_options=kmplacemark:" + doPlacemarks + "&format="
                + KMZMapOutputFormat.MIME_TYPE;
        MockHttpServletResponse response = getAsServletResponse(requestUrl);
        assertEquals(KMZMapOutputFormat.MIME_TYPE, response.getContentType());

        ZipFile zipFile = null;
        try {
            // create the kmz
            File tempDir = org.geoserver.data.util.IOUtils.createRandomDirectory("./target", "kmplacemark", "test");
            tempDir.deleteOnExit();

            File zip = new File(tempDir, "kmz.zip");
            zip.deleteOnExit();

            FileOutputStream output = new FileOutputStream(zip);
            FileUtils.writeByteArrayToFile(zip, getBinary(response));

            output.flush();
            output.close();

            assertTrue(zip.exists());

            // unzip and test it
            zipFile = new ZipFile(zip);

            ZipEntry entry = zipFile.getEntry("wms.kml");
            assertNotNull(entry);
            assertNotNull(zipFile.getEntry("images/layers_0.png"));

            // unzip the wms.kml to file
            byte[] buffer = new byte[1024];
            int len;

            InputStream inStream = zipFile.getInputStream(entry);
            File temp = File.createTempFile("test_out", "kmz", tempDir);
            temp.deleteOnExit();
            BufferedOutputStream outStream = new BufferedOutputStream(new FileOutputStream(temp));

            while ((len = inStream.read(buffer)) >= 0)
                outStream.write(buffer, 0, len);
            inStream.close();
            outStream.close();

            // read in the wms.kml and check its contents
            Document document = dom(new BufferedInputStream(new FileInputStream(temp)));
            // print(document);

            assertEquals("kml", document.getDocumentElement().getNodeName());
            if (doPlacemarks) {
                assertEquals(getFeatureSource(MockData.BASIC_POLYGONS).getFeatures().size(),
                        document.getElementsByTagName("Placemark").getLength());
                XMLAssert.assertXpathEvaluatesTo("3", "count(//kml:Placemark//kml:Point)", document);
            } else {
                assertEquals(0, document.getElementsByTagName("Placemark").getLength());
            }

        } finally {
            if (zipFile != null) {
                zipFile.close();
            }
        }
    }

    @Test
    public void testStyleConverter() throws Exception {
        // the style selects a single feature
        final String requestUrl = "wms/kml?layers=" + getLayerId(MockData.BASIC_POLYGONS)
                + "&styles=allsymbolizers&mode=download";
        Document doc = getAsDOM(requestUrl);
        // print(doc);

        XMLAssert.assertXpathEvaluatesTo("1", "count(//kml:Placemark[1]/kml:Style)", doc);
        XMLAssert.assertXpathEvaluatesTo("0",
                "count(//kml:Placemark[1]/kml:Style/kml:IconStyle/kml:Icon/kml:color)", doc);
        XMLAssert.assertXpathEvaluatesTo("http://localhost:8080/geoserver/kml/icon/allsymbolizers?0.0.0=",
                "//kml:Placemark[1]/kml:Style/kml:IconStyle/kml:Icon/kml:href", doc);
        XMLAssert.assertXpathEvaluatesTo("b24d4dff", "//kml:Placemark[1]/kml:Style/kml:PolyStyle/kml:color", doc);
        XMLAssert.assertXpathEvaluatesTo("ffba3e00", "//kml:Placemark[1]/kml:Style/kml:LineStyle/kml:color", doc);
        XMLAssert.assertXpathEvaluatesTo("2.0", "//kml:Placemark[1]/kml:Style/kml:LineStyle/kml:width", doc);
        XMLAssert.assertXpathEvaluatesTo("1.4", "//kml:Placemark[1]/kml:Style/kml:LabelStyle/kml:scale", doc);
    }

    @Test
    public void testLabelFromTextSymbolizer() throws Exception {
        // the style selects a single feature
        final String requestUrl = "wms/kml?layers=" + getLayerId(MockData.NAMED_PLACES)
                + "&styles=labels&mode=download";
        Document doc = getAsDOM(requestUrl);
        // print(doc);

        XMLAssert.assertXpathEvaluatesTo("2", "count(//kml:Placemark)", doc);
        XMLAssert.assertXpathEvaluatesTo("1", "count(//kml:Placemark[kml:name='Ashton'])", doc);
        XMLAssert.assertXpathEvaluatesTo("1", "count(//kml:Placemark[kml:name='Goose Island'])", doc);
    }

    /**
     * See http://jira.codehaus.org/browse/GEOS-2670
     */
    @Test
    public void testDynamicSymbolizer() throws Exception {
        final String requestUrl = "wms/kml?layers=" + getLayerId(MockData.STREAMS)
                + "&styles=dynamicsymbolizer&mode=download";
        Document document = getAsDOM(requestUrl);

        assertEquals("kml", document.getDocumentElement().getNodeName());
        XMLAssert.assertXpathEvaluatesTo("http://example.com/Cam Stream",
                "//kml:Style[1]/kml:IconStyle/kml:Icon/kml:href", document);
    }

    @Test
    public void testRelativeDynamicSymbolizer() throws Exception {
        final String requestUrl = "wms/kml?layers=" + getLayerId(MockData.STREAMS)
                + "&styles=relativeds&mode=download";
        Document document = getAsDOM(requestUrl);

        assertEquals("kml", document.getDocumentElement().getNodeName());
        XMLAssert.assertXpathEvaluatesTo("http://localhost:8080/geoserver/styles/icons/Cam%20Stream",
                "//kml:Style[1]/kml:IconStyle/kml:Icon/kml:href", document);
    }

    @Test
    public void testLegend() throws Exception {
        String layerId = getLayerId(MockData.BASIC_POLYGONS);
        final String requestUrl = "wms/kml?layers=" + layerId
                + "&styles=polygon&mode=download&format_options=legend:true" //
                + "&legend_options=fontStyle:bold;fontColor:ff0000;fontSize:18";
        Document doc = getAsDOM(requestUrl);
        // print(doc);

        assertEquals("kml", doc.getDocumentElement().getNodeName());

        // the icon itself
        XpathEngine xpath = XMLUnit.newXpathEngine();
        String href = xpath.evaluate("//kml:ScreenOverlay/kml:Icon/kml:href", doc);
        assertTrue(href.contains("request=GetLegendGraphic"));
        assertTrue(href.contains("layer=cite%3ABasicPolygons"));
        assertTrue(href.contains("style=polygon"));
        assertTrue(href.contains("LEGEND_OPTIONS=fontStyle%3Abold%3BfontColor%3Aff0000%3BfontSize%3A18"));

        // overlay location
        XMLAssert.assertXpathEvaluatesTo("0.0", "//kml:ScreenOverlay/kml:overlayXY/@x", doc);
        XMLAssert.assertXpathEvaluatesTo("0.0", "//kml:ScreenOverlay/kml:overlayXY/@y", doc);
        XMLAssert.assertXpathEvaluatesTo("pixels", "//kml:ScreenOverlay/kml:overlayXY/@xunits", doc);
        XMLAssert.assertXpathEvaluatesTo("pixels", "//kml:ScreenOverlay/kml:overlayXY/@yunits", doc);
        XMLAssert.assertXpathEvaluatesTo("10.0", "//kml:ScreenOverlay/kml:screenXY/@x", doc);
        XMLAssert.assertXpathEvaluatesTo("20.0", "//kml:ScreenOverlay/kml:screenXY/@y", doc);
        XMLAssert.assertXpathEvaluatesTo("pixels", "//kml:ScreenOverlay/kml:screenXY/@xunits", doc);
        XMLAssert.assertXpathEvaluatesTo("pixels", "//kml:ScreenOverlay/kml:screenXY/@yunits", doc);
    }

    @Test
    public void testLookatOptions() throws Exception {
        String layerId = getLayerId(MockData.BASIC_POLYGONS);
        final String requestUrl = "wms/kml?layers=" + layerId + "&styles=polygon&mode=download"
                + "&format_options=lookatbbox:-20,-20,20,20;altitude:10;heading:0;tilt:30;range:100;altitudemode:absolute";
        Document doc = getAsDOM(requestUrl);
        // print(doc);

        // overlay location
        XMLAssert.assertXpathEvaluatesTo("0.0", "//kml:Document/kml:LookAt/kml:longitude", doc);
        XMLAssert.assertXpathEvaluatesTo("0.0", "//kml:Document/kml:LookAt/kml:latitude", doc);
        XMLAssert.assertXpathEvaluatesTo("10.0", "//kml:Document/kml:LookAt/kml:altitude", doc);
        XMLAssert.assertXpathEvaluatesTo("0.0", "//kml:Document/kml:LookAt/kml:heading", doc);
        XMLAssert.assertXpathEvaluatesTo("30.0", "//kml:Document/kml:LookAt/kml:tilt", doc);
        XMLAssert.assertXpathEvaluatesTo("100.0", "//kml:Document/kml:LookAt/kml:range", doc);
        XMLAssert.assertXpathEvaluatesTo("absolute", "//kml:Document/kml:LookAt/kml:altitudeMode", doc);
    }

    @Test
    public void testExtendedData() throws Exception {
        String layerId = getLayerId(MockData.AGGREGATEGEOFEATURE);
        final String requestUrl = "wms/kml?layers=" + layerId
                + "&mode=download&extendedData=true&kmattr=false&kmscore=100";
        Document doc = getAsDOM(requestUrl);

        // print(doc);

        // there is one schema
        XMLAssert.assertXpathEvaluatesTo("1", "count(//kml:Document/kml:Schema)", doc);
        // check we only have the non geom properties
        XMLAssert.assertXpathEvaluatesTo("6", "count(//kml:Document/kml:Schema/kml:SimpleField)", doc);
        XMLAssert.assertXpathEvaluatesTo("0",
                "count(//kml:Document/kml:Schema/kml:SimpleField[@name='multiPointProperty'])", doc);
        XMLAssert.assertXpathEvaluatesTo("0",
                "count(//kml:Document/kml:Schema/kml:SimpleField[@name='multiCurveProperty'])", doc);
        XMLAssert.assertXpathEvaluatesTo("0",
                "count(//kml:Document/kml:Schema/kml:SimpleField[@name='multiSurfaceProperty'])", doc);
        // check the type mapping
        XMLAssert.assertXpathEvaluatesTo("string",
                "//kml:Document/kml:Schema/kml:SimpleField[@name='description']/@type", doc);
        XMLAssert.assertXpathEvaluatesTo("double",
                "//kml:Document/kml:Schema/kml:SimpleField[@name='doubleProperty']/@type", doc);
        XMLAssert.assertXpathEvaluatesTo("int",
                "//kml:Document/kml:Schema/kml:SimpleField[@name='intRangeProperty']/@type", doc);
        XMLAssert.assertXpathEvaluatesTo("string",
                "//kml:Document/kml:Schema/kml:SimpleField[@name='strProperty']/@type", doc);
        XMLAssert.assertXpathEvaluatesTo("string",
                "//kml:Document/kml:Schema/kml:SimpleField[@name='featureCode']/@type", doc);

        // check the extended data of one feature
        String sd = "//kml:Placemark[@id='AggregateGeoFeature.f005']/kml:ExtendedData/kml:SchemaData/kml:SimpleData";
        XMLAssert.assertXpathEvaluatesTo("description-f005", sd + "[@name='description']", doc);
        XMLAssert.assertXpathEvaluatesTo("name-f005", sd + "[@name='name']", doc);
        XMLAssert.assertXpathEvaluatesTo("2012.78", sd + "[@name='doubleProperty']", doc);
        XMLAssert.assertXpathEvaluatesTo(
                "Ma quande lingues coalesce, li grammatica del resultant "
                        + "lingue es plu simplic e regulari quam ti del coalescent lingues. Li nov lingua "
                        + "franca va esser plu simplic e regulari quam li existent Europan lingues.",
                sd + "[@name='strProperty']", doc);
        XMLAssert.assertXpathEvaluatesTo("BK030", sd + "[@name='featureCode']", doc);
    }

    @Test
    public void testHeightTemplate() throws Exception {
        File template = null;
        try {
            String layerId = getLayerId(MockData.LAKES);
            FeatureTypeInfo resource = getCatalog().getResourceByName(layerId, FeatureTypeInfo.class);
            File parent = getDataDirectory().findOrCreateResourceDir(resource);
            template = new File(parent, "height.ftl");
            FileUtils.write(template, "${FID.value}");

            final String requestUrl = "wms/kml?layers=" + layerId + "&mode=download";
            Document doc = getAsDOM(requestUrl);
            // print(doc);

            String base = "//kml:Placemark[@id='Lakes.1107531835962']/kml:MultiGeometry";
            XMLAssert.assertXpathEvaluatesTo("1", "count(" + base + ")", doc);
            XMLAssert.assertXpathEvaluatesTo("1", base + "/kml:Point/kml:extrude", doc);
            XMLAssert.assertXpathEvaluatesTo("relativeToGround", base + "/kml:Point/kml:altitudeMode", doc);
            XMLAssert.assertXpathEvaluatesTo("0.0017851936218678816,-0.0010838268792710709,101.0",
                    base + "/kml:Point/kml:coordinates", doc);
            XMLAssert.assertXpathEvaluatesTo("1", base + "/kml:Polygon/kml:extrude", doc);
            XMLAssert.assertXpathEvaluatesTo("relativeToGround", base + "/kml:Polygon/kml:altitudeMode", doc);
            XMLAssert.assertXpathEvaluatesTo(
                    "6.0E-4,-0.0018,101.0 0.0010,-6.0E-4,101.0 0.0024,-1.0E-4,101.0 0.0031,-0.0015,101.0 6.0E-4,-0.0018,101.0",
                    base + "/kml:Polygon/kml:outerBoundaryIs/kml:LinearRing/kml:coordinates", doc);
        } finally {
            if (template != null) {
                template.delete();
            }
        }
    }

    @Test
    public void testHeightTemplateNoExtrude() throws Exception {
        File template = null;
        try {
            String layerId = getLayerId(MockData.LAKES);
            FeatureTypeInfo resource = getCatalog().getResourceByName(layerId, FeatureTypeInfo.class);
            File parent = getDataDirectory().findOrCreateResourceDir(resource);
            template = new File(parent, "height.ftl");
            FileUtils.write(template, "${FID.value}");

            final String requestUrl = "wms/kml?layers=" + layerId + "&mode=download&extrude=false";
            Document doc = getAsDOM(requestUrl);
            // print(doc);

            String base = "//kml:Placemark[@id='Lakes.1107531835962']/kml:MultiGeometry";
            XMLAssert.assertXpathEvaluatesTo("1", "count(" + base + ")", doc);
            XMLAssert.assertXpathEvaluatesTo("0", base + "/kml:Point/kml:extrude", doc);
            XMLAssert.assertXpathEvaluatesTo("relativeToGround", base + "/kml:Point/kml:altitudeMode", doc);
            XMLAssert.assertXpathEvaluatesTo("0.0017851936218678816,-0.0010838268792710709,101.0",
                    base + "/kml:Point/kml:coordinates", doc);
            XMLAssert.assertXpathEvaluatesTo("0", base + "/kml:Polygon/kml:extrude", doc);
            XMLAssert.assertXpathEvaluatesTo("relativeToGround", base + "/kml:Polygon/kml:altitudeMode", doc);
            XMLAssert.assertXpathEvaluatesTo(
                    "6.0E-4,-0.0018,101.0 0.0010,-6.0E-4,101.0 0.0024,-1.0E-4,101.0 0.0031,-0.0015,101.0 6.0E-4,-0.0018,101.0",
                    base + "/kml:Polygon/kml:outerBoundaryIs/kml:LinearRing/kml:coordinates", doc);
        } finally {
            if (template != null) {
                template.delete();
            }
        }
    }

    /**
     * Verify that when GE asks for coordinates larger than 180 we still manage gracefully
     */
    @Test
    public void testCoordinateShift() throws Exception {
        Document document = getAsDOM(
                "wms/kml?layers=" + getLayerId(MockData.BASIC_POLYGONS) + "&mode=download&bbox=150,-90,380,90");
        // print(document);

        assertEquals(3, document.getElementsByTagName("Placemark").getLength());
    }

    @Test
    public void testExternalImageSize() throws Exception {
        GetMapRequest req = createGetMapRequest(MockData.STREAMS);
        req.setWidth(256);
        req.setHeight(256);

        WMSMapContent mapContent = new WMSMapContent(req);
        mapContent.addLayer(createMapLayer(MockData.STREAMS, "big-local-image"));

        mapContent.getViewport().setBounds(new ReferencedEnvelope(-180, 0, -90, 90, DefaultGeographicCRS.WGS84));
        mapContent.setMapHeight(256);
        mapContent.setMapWidth(256);

        KMLMapOutputFormat of = new KMLMapOutputFormat(getWMS());
        KMLMap map = of.produceMap(mapContent);

        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        new KMLEncoder().encode(map.getKml(), bout);

        Document document = dom(new ByteArrayInputStream(bout.toByteArray()));

        assertEquals("kml", document.getDocumentElement().getNodeName());
        assertEquals(1, document.getElementsByTagName("Style").getLength());

        XMLAssert.assertXpathExists("//kml:IconStyle/kml:scale", document);

        XPath xPath = XPathFactory.newInstance().newXPath();
        initXPath(xPath);

        Double scale = (Double) xPath.evaluate("//kml:IconStyle/kml:scale", document.getDocumentElement(),
                XPathConstants.NUMBER);
        assertEquals(42d / 16d, scale, 0.01);
    }

    @Test
    public void testKmzEmbededPointImageSize() throws Exception {

        WMSMapContent mapContent = createMapContext(MockData.POINTS, "big-mark");

        File temp = File.createTempFile("test", "kmz", new File("target"));
        temp.delete();
        temp.mkdir();
        temp.deleteOnExit();

        File zip = new File(temp, "kmz.zip");
        zip.deleteOnExit();

        // create hte map producer
        KMZMapOutputFormat mapProducer = new KMZMapOutputFormat(getWMS());
        KMLMap map = mapProducer.produceMap(mapContent);

        FileOutputStream output = new FileOutputStream(zip);
        new KMLMapResponse(new KMLEncoder(), getWMS()).write(map, output, null);

        output.flush();
        output.close();

        assertTrue(zip.exists());

        // unzip and test it
        ZipFile zipFile = new ZipFile(zip);

        ZipEntry kmlEntry = zipFile.getEntry("wms.kml");
        InputStream kmlStream = zipFile.getInputStream(kmlEntry);

        Document kmlResult = XMLUnit.buildTestDocument(new InputSource(kmlStream));

        Double scale = Double.parseDouble(XMLUnit.newXpathEngine()
                .getMatchingNodes("(//kml:Style)[1]/kml:IconStyle/kml:scale", kmlResult).item(0).getTextContent());
        assertEquals(49d / 16d, scale, 0.01);

        zipFile.close();
    }

    WMSMapContent createMapContext(QName layer, String style) throws Exception {

        // create a map context
        WMSMapContent mapContent = new WMSMapContent();
        mapContent.addLayer(createMapLayer(layer, style));
        mapContent.setMapHeight(256);
        mapContent.setMapWidth(256);

        GetMapRequest getMapRequest = createGetMapRequest(new QName[] { layer });
        getMapRequest.setWidth(256);
        getMapRequest.setHeight(256);

        mapContent.setRequest(getMapRequest);
        mapContent.getViewport().setBounds(new ReferencedEnvelope(-180, 180, -90, 90, DefaultGeographicCRS.WGS84));
        return mapContent;
    }

    void initXPath(XPath xpath) {
        SimpleNamespaceContext ctx = new SimpleNamespaceContext();
        ctx.bindNamespaceUri("kml", "http://www.opengis.net/kml/2.2");
        xpath.setNamespaceContext(ctx);
    }

    /**
     * Creates a key/value pair map from the cgi parameters in the provided url
     * 
     * @param url an url where all the cgi parameter values are url encoded
     * @return a map with the key value pairs from the url with all the parameter names in upper
     *         case
     */
    static Map<String, String> toKvp(String url) {
        if (url.indexOf('?') > 0) {
            url = url.substring(url.indexOf('?') + 1);
        }
        Map<String, String> kvpMap = new HashMap<String, String>();

        String[] tuples = url.split("&");
        for (String tuple : tuples) {
            String[] kvp = tuple.split("=");
            String key = kvp[0].toUpperCase();
            String value = kvp.length > 1 ? kvp[1] : null;
            if (value != null) {
                try {
                    value = URLDecoder.decode(value, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    throw new RuntimeException(e);
                }
            }
            kvpMap.put(key, value);
        }

        return kvpMap;
    }

    static void assertMapsEqual(Map<String, Object> expected, Map<String, Object> actual) throws Exception {
        for (Map.Entry<String, Object> entry : expected.entrySet()) {
            if (entry.getKey().equalsIgnoreCase("format_options")) {
                FormatOptionsKvpParser parser = new FormatOptionsKvpParser();
                Map expectedFormatOptions = (Map) parser.parse((String) entry.getValue());
                Map actualFormatOptions = (Map) parser.parse((String) actual.get(entry.getKey()));

                for (Object o : expectedFormatOptions.entrySet()) {
                    Map.Entry formatOption = (Map.Entry) o;
                    assertEquals(formatOption.getValue(), actualFormatOptions.get(formatOption.getKey()));
                }

                for (Object key : actualFormatOptions.keySet()) {
                    assertTrue("found unexpected key " + key + " in format options",
                            expectedFormatOptions.containsKey(key));
                }

                // special treatment for the format options
            } else {
                assertEquals(entry.getValue(), actual.get(entry.getKey()));
            }
        }

        for (String key : actual.keySet()) {
            assertTrue(expected.containsKey(key));
        }
    }
}