/* (c) 2014 - 2017 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * 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.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.Arrays; import java.util.HashMap; import java.util.List; 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.XMLConstants; import javax.xml.bind.JAXBContext; import javax.xml.bind.Unmarshaller; import javax.xml.namespace.QName; import javax.xml.validation.SchemaFactory; 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.custommonkey.xmlunit.exceptions.XpathException; 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.Assert; import org.junit.Test; import org.springframework.util.xml.SimpleNamespaceContext; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.springframework.mock.web.MockHttpServletResponse; import de.micromata.opengis.kml.v_2_2_0.Kml; /** * Some functional tests for kml reflector * * @author David Winslow (OpenGeo) * @author Gabriel Roldan (OpenGeo) * @author Markus Innerebner (EURAC Research) * @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 <a href="https://osgeo-org.atlassian.net/browse/GEOS-2185">GEOS-2185</a> */ @Test public void testNoBBOXInHREF() throws Exception { final String layerName = 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=MODE%3Arefresh%3Bautofit%3Atrue%3BKMPLACEMARK%3Afalse%3BKMATTR%3Atrue%3BKMSCORE%3A40%3BSUPEROVERLAY%3Afalse&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 <a href="https://osgeo-org.atlassian.net/browse/GEOS-2185">GEOS-2185</a> */ @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(); final String titleName = MockData.BASIC_POLYGONS.getLocalPart(); final String abstractValue = "abstract about " + titleName; 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(titleName, "kml:kml/kml:Document/kml:NetworkLink[1]/kml:name", dom); assertXpathEvaluatesTo(abstractValue, "kml:kml/kml:Document/kml:NetworkLink[1]/kml:description", dom); assertXpathEvaluatesTo(titleName, "kml:kml/kml:Document/kml:NetworkLink[2]/kml:name", dom); assertXpathEvaluatesTo(abstractValue, "kml:kml/kml:Document/kml:NetworkLink[2]/kml:description", 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)} */ @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); List<String> formatOptions = Arrays.asList(((String) kvp.get("format_options")).split(";")); assertEquals(9, formatOptions.size()); assertTrue(formatOptions.contains("LEGEND:true")); assertTrue(formatOptions.contains("SUPEROVERLAY:true")); assertTrue(formatOptions.contains("AUTOFIT:true")); assertTrue(formatOptions.contains("KMPLACEMARK:false")); assertTrue(formatOptions.contains("OVERLAYMODE:auto")); assertTrue(formatOptions.contains("KMSCORE:10")); assertTrue(formatOptions.contains("MODE:superoverlay")); assertTrue(formatOptions.contains("KMATTR:true")); assertTrue(formatOptions.contains("KMLTITLE:myCustomLayerTitle")); } @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); } @Test public void testKmlRefreshFormatOption() throws Exception { final String layerName = MockData.BASIC_POLYGONS.getPrefix() + ":" + MockData.BASIC_POLYGONS.getLocalPart(); String requestUrl = "wms/kml?layers=" + layerName + "&format_options=kmlrefresh:expires"; Document dom = getAsDOM(requestUrl); //print(dom); assertEquals("kml", dom.getDocumentElement().getLocalName()); assertXpathEvaluatesTo("onExpire", "/kml:kml/kml:Document/kml:NetworkLink/kml:Url/kml:refreshMode", dom); requestUrl = "wms/kml?layers=" + layerName + "&format_options=kmlrefresh:60"; dom = getAsDOM(requestUrl); assertXpathEvaluatesTo("onInterval", "/kml:kml/kml:Document/kml:NetworkLink/kml:Url/kml:refreshMode", dom); assertXpathEvaluatesTo("60.0", "/kml:kml/kml:Document/kml:NetworkLink/kml:Url/kml:refreshInterval", dom); } @Test public void testKmlVisibleFormatOption() throws Exception { final String layerName = MockData.BASIC_POLYGONS.getPrefix() + ":" + MockData.BASIC_POLYGONS.getLocalPart(); String requestUrl = "wms/kml?layers=" + layerName + "&format_options=kmlvisible:true"; Document dom = getAsDOM(requestUrl); //print(dom); assertEquals("kml", dom.getDocumentElement().getLocalName()); assertXpathEvaluatesTo("1", "/kml:kml/kml:Document/kml:NetworkLink/kml:visibility", dom); requestUrl = "wms/kml?layers=" + layerName + "&format_options=kmlvisible:false"; dom = getAsDOM(requestUrl); assertEquals("kml", dom.getDocumentElement().getLocalName()); assertXpathEvaluatesTo("0", "/kml:kml/kml:Document/kml:NetworkLink/kml:visibility", dom); } /** * See https://osgeo-org.atlassian.net/browse/GEOS-1947 * */ @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 https://osgeo-org.atlassian.net/browse/GEOS-3994 * */ @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 https://osgeo-org.atlassian.net/browse/GEOS-3965 * */ @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/reflect?layers=" + getLayerId(MockData.BASIC_POLYGONS) + "&styles=&format_options=KMSCORE:0;mode:refresh&format= " + KMLMapOutputFormat.MIME_TYPE; 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/reflect?layers=" + getLayerId(MockData.BASIC_POLYGONS) + "&styles=&format_options=KMSCORE:0;mode:refresh&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"); String urlExternal = URLDecoder.decode(url.toExternalForm(), "UTF-8"); final String requestUrl = "wms/reflect?layers=" + getLayerId(MockData.BASIC_POLYGONS) + "&format_options=KMSCORE:0;mode:refresh&format= " + KMLMapOutputFormat.MIME_TYPE + "&sld=" + urlExternal; 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=" + urlExternal)); } @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/reflect?layers=" + getLayerId(MockData.BASIC_POLYGONS) + "&styles=&format_options=mode:refresh;kmscore:0;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.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 https://osgeo-org.atlassian.net/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); assertXPathCoordinates( "LinearRing","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 testHeightTemplatePoint() throws Exception { File template = null; try { String layerId = getLayerId(MockData.POINTS); FeatureTypeInfo resource = getCatalog().getResourceByName(layerId, FeatureTypeInfo.class); File parent = getDataDirectory().findOrCreateResourceDir(resource); template = new File(parent, "height.ftl"); FileUtils.write(template, "${altitude.value}"); final String requestUrl = "wms/kml?layers=" + layerId + "&mode=download"; Document doc = getAsDOM(requestUrl); String base = "//kml:Placemark[@id='Points.0']/kml:Point"; XMLAssert.assertXpathEvaluatesTo("1", "count(" + base+ ")", doc); XMLAssert.assertXpathEvaluatesTo("1", base + "/kml:extrude", doc); XMLAssert.assertXpathEvaluatesTo("relativeToGround", base + "/kml:altitudeMode", doc); } finally { if(template != null) { template.delete(); } } } private void assertXPathCoordinates( String message, String expectedText, String xpath, Document doc ) throws XpathException { XpathEngine engine = XMLUnit.newXpathEngine(); String text = engine.evaluate(xpath, doc); if( equalsRegardingNull( expectedText, text ) ){ return; } if( expectedText != null && text != null ){ String expectedCoordinates[] = expectedText.split("(\\s|,)"); String actualCoordiantes[] = text.split("(\\s|,)"); if( expectedCoordinates.length == actualCoordiantes.length ){ final int LENGTH = actualCoordiantes.length; boolean checked = true; LIST: for( int i = 0; i< LENGTH; i++){ String expected = expectedCoordinates[i]; String actual = actualCoordiantes[i]; if( expected.length() == actual.length()){ if( !expected.equals(actual)){ checked = false; break LIST; // normal equals check will report issue } } else { try { double expectedOrdinate = Double.parseDouble(expected); double actualOridnate = Double.parseDouble(actual); if (Double.compare(expectedOrdinate, actualOridnate) != 0) { // Could do a Math.abs(expectedOrdinate - actualOridnate) <= delta check break LIST; // normal equals check will report issue } } catch (NumberFormatException formatException ){ checked = false; break LIST; // normal equals check will report issue } } } if( checked) { return; // double based comparison checked all elements } } } // call normal assertEquals for consistent failure message assertEquals( message, expectedText, text ); } private boolean equalsRegardingNull(String expected, String actual ){ if( expected == null ){ return actual == null; } return expected.equals(actual); // fast string equals check } @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); // Coordinate Formatting in JDK 1.7.0 does not include trailing 0 - see GEOS-5973 // JDK 1.6: 0.0010 // JDK 1.7: 0.001 assertXPathCoordinates("kml:LinearRing","6.0E-4,-0.0018,101.0 0.001,-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 testValidKML() 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, null); // Explicitly check for known bugs in the JavaAPIforKml library. // https://osgeo-org.atlassian.net/browse/GEOS-7963 // https://github.com/micromata/javaapiforkml/issues/9 Document document = dom(new ByteArrayInputStream(bout.toByteArray())); // print(document); XMLAssert.assertXpathNotExists("//kml:IconStyle/kml:Icon/kml:refreshInterval", document); XMLAssert.assertXpathNotExists("//kml:IconStyle/kml:Icon/kml:viewRefreshTime", document); XMLAssert.assertXpathNotExists("//kml:IconStyle/kml:Icon/kml:viewBoundScale", document); // Validate against the KML 2.2 schema. Unmarshaller unmarshaller = JAXBContext.newInstance(Kml.class).createUnmarshaller(); unmarshaller.setSchema(SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) .newSchema(getClass().getResource("/schema/ogckml/ogckml22.xsd"))); unmarshaller.unmarshal(document); } @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, null); 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)); } } /** * * <p>Method testLookatOptionsWithRefreshMode tests if the two altitude values are obtained from the corresponding bounding box. * The first value (//kml:Document/kml:LookAt/kml:altitude) is calculated from the initial bounding box. * The second value (//kml:Document/kml:NetworkLink/kml:LookAt/kml:altitude) is calculated from the bounding box passed to the WMS request. * Test fails if those values are identical. * @see <a href="https://osgeo-org.atlassian.net/browse/GEOS-6410">GEOS-6410</a> * </p> */ @Test public void testLookatOptionsWithRefreshMode() throws Exception { String layerId = getLayerId(MockData.BASIC_POLYGONS); final String requestUrl = "wms/kml?layers=" + layerId + "&styles=polygon&mode=refresh&bbox=10.56,46.99,11.50,47.26" ; Document doc = getAsDOM(requestUrl); // we expect that those values should not be the same, because first value is obtained from initial bbox of the layer, while the second value from the bbox of the request XMLAssert.assertXpathValuesNotEqual("//kml:Document/kml:LookAt/kml:altitude","//kml:Document/kml:NetworkLink/kml:LookAt/kml:altitude", doc); } /** * <p>Method testWMSTimeRequest tests if the time parameter of the request is also passed to the KML WMS request.</p> * @see <a href="https://osgeo-org.atlassian.net/browse/GEOS-6411">GEOS-6411</a> */ @Test public void testWMSTimeRequest() throws Exception { String layerId = getLayerId(MockData.BASIC_POLYGONS); String expectedTS = "time=2014-03-01"; final String requestUrl = "wms/kml?layers=" + layerId + "&styles=polygon&mode=refresh&bbox=10.56,46.99,11.50,47.26&" + expectedTS ; Document doc = getAsDOM(requestUrl); // we expect that those values should not be the same, because first value is obtained from initial bbox of the layer, while the second value from the bbox of the request NodeList nodes = doc.getElementsByTagName("href"); for (int i = 0; i < nodes.getLength(); ++i) { Element e = (Element) nodes.item(i); String actualTS = e.getTextContent(); Assert.assertTrue("Time parameter missing", actualTS.contains(expectedTS)); } } /** * <p>Method testWMSElevationRequest tests if the elevation parameter of the request is also passed to the KML WMS request.</p> * @see <a href="https://osgeo-org.atlassian.net/browse/GEOS-6411">GEOS-6411</a> */ @Test public void testWMSElevationRequest() throws Exception { String layerId = getLayerId(MockData.BASIC_POLYGONS); String expectedTS = "elevation=500"; final String requestUrl = "wms/kml?layers=" + layerId + "&styles=polygon&mode=refresh&bbox=10.56,46.99,11.50,47.26&" + expectedTS ; Document doc = getAsDOM(requestUrl); // we expect that those values should not be the same, because first value is obtained from initial bbox of the layer, while the second value from the bbox of the request NodeList nodes = doc.getElementsByTagName("href"); for (int i = 0; i < nodes.getLength(); ++i) { Element e = (Element) nodes.item(i); String actualTS = e.getTextContent(); Assert.assertTrue("Elevation parameter missing", actualTS.contains(expectedTS)); } } }