From 2ce617fba93c4cb54d177b121be5c723d2373d35 Mon Sep 17 00:00:00 2001 From: Daniele Romagnoli Date: Fri, 27 Sep 2024 19:18:34 +0200 Subject: [PATCH] skip GetFeatureInfo query link when not supported formats --- .../geoserver/mapml/MapMLDocumentBuilder.java | 80 +++++++------ .../geoserver/mapml/MapMLRequestMangler.java | 107 +++++++++++++---- .../MapMLWMSGetFeatureInfoProxyTest.java | 85 +++++++++++++ .../__files/wmscapsgmlfeatureinfo.xml | 112 ++++++++++++++++++ 4 files changed, 327 insertions(+), 57 deletions(-) create mode 100644 src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSGetFeatureInfoProxyTest.java create mode 100644 src/extension/mapml/src/test/resources/__files/wmscapsgmlfeatureinfo.xml diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java index 2fc7786a5f5..59ef20a433a 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLDocumentBuilder.java @@ -1641,21 +1641,6 @@ private void createMinAndMaxWidthHeight() { * Generate inputs and links that the client will use to generate WMTS GetFeatureInfo requests */ private void generateWMTSQueryClientLinks(MapMLLayerMetadata mapMLLayerMetadata) { - // query i value (x) - Input input = new Input(); - input.setName("i"); - input.setType(InputType.LOCATION); - input.setUnits(UnitType.TILE); - input.setAxis(AxisType.I); - extentList.add(input); - - // query j value (y) - input = new Input(); - input.setName("j"); - input.setType(InputType.LOCATION); - input.setUnits(UnitType.TILE); - input.setAxis(AxisType.J); - extentList.add(input); // query link Link queryLink = new Link(); @@ -1680,8 +1665,29 @@ private void generateWMTSQueryClientLinks(MapMLLayerMetadata mapMLLayerMetadata) new MapMLRequestMangler( mapContent, mapMLLayerMetadata, baseUrlPattern, path, params, proj); String urlTemplate = mangler.getUrlTemplate(); - queryLink.setTref(urlTemplate); - extentList.add(queryLink); + // It may be that the mangler decided to not generate any query URL due + // to unsupported info formats from the remote layer. So we are not + // generating the query link. + if (urlTemplate != null) { + // query i value (x) + Input input = new Input(); + input.setName("i"); + input.setType(InputType.LOCATION); + input.setUnits(UnitType.TILE); + input.setAxis(AxisType.I); + extentList.add(input); + + // query j value (y) + input = new Input(); + input.setName("j"); + input.setType(InputType.LOCATION); + input.setUnits(UnitType.TILE); + input.setAxis(AxisType.J); + extentList.add(input); + + queryLink.setTref(urlTemplate); + extentList.add(queryLink); + } } /** Generate inputs and links the client will use to create WMS GetFeatureInfo requests */ @@ -1690,21 +1696,6 @@ private void generateWMSQueryClientLinks(MapMLLayerMetadata mapMLLayerMetadata) if (mapMLLayerMetadata.isUseTiles()) { units = UnitType.TILE; } - // query i value (x) - Input input = new Input(); - input.setName("i"); - input.setType(InputType.LOCATION); - input.setUnits(units); - input.setAxis(AxisType.I); - extentList.add(input); - - // query j value (y) - input = new Input(); - input.setName("j"); - input.setType(InputType.LOCATION); - input.setUnits(units); - input.setAxis(AxisType.J); - extentList.add(input); // query link Link queryLink = new Link(); @@ -1744,8 +1735,29 @@ private void generateWMSQueryClientLinks(MapMLLayerMetadata mapMLLayerMetadata) new MapMLRequestMangler( mapContent, mapMLLayerMetadata, baseUrlPattern, path, params, proj); String urlTemplate = mangler.getUrlTemplate(); - queryLink.setTref(urlTemplate); - extentList.add(queryLink); + // It may be that the mangler decided to not generate any query URL due + // to unsupported info formats from the remote layer. So we are not + // generating the query link. + if (urlTemplate != null) { + // query i value (x) + Input input = new Input(); + input.setName("i"); + input.setType(InputType.LOCATION); + input.setUnits(units); + input.setAxis(AxisType.I); + extentList.add(input); + + // query j value (y) + input = new Input(); + input.setName("j"); + input.setType(InputType.LOCATION); + input.setUnits(units); + input.setAxis(AxisType.J); + extentList.add(input); + + queryLink.setTref(urlTemplate); + extentList.add(queryLink); + } } private void setCqlFilterParam( diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLRequestMangler.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLRequestMangler.java index 9c9873d827e..34649e38637 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLRequestMangler.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLRequestMangler.java @@ -9,6 +9,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -38,6 +40,7 @@ import org.geoserver.wms.WMSMapContent; import org.geotools.api.filter.Filter; import org.geotools.api.referencing.FactoryException; +import org.geotools.data.ows.OperationType; import org.geotools.ows.wms.Layer; import org.geotools.ows.wms.WMSCapabilities; import org.geotools.ows.wmts.model.TileMatrix; @@ -75,6 +78,9 @@ public class MapMLRequestMangler { private static final double ORIGIN_DELTA = 0.1; private static final double SCALE_DELTA = 1E-5; + private static final List GET_FEATURE_INFO_FORMATS = + Arrays.asList("text/mapml", "text/html", "text/plain"); + static class CRSMapper { Set inputCRSs; @@ -162,7 +168,7 @@ public String getUrlTemplate() { baseUrlPattern, path, params, URLMangler.URLType.SERVICE), "UTF-8"); } else { - urlTemplate = tryCascading(path, params, layerInfo); + urlTemplate = generateURL(path, params, layerInfo); } } catch (UnsupportedEncodingException uee) { } @@ -202,16 +208,20 @@ private boolean canCascade(LayerInfo layerInfo) { return false; } /** - * Try cascading to the remote Server unless any condition is preventing that (i.e. CRS not - * supported on the remote server) + * Try cascading to the remote Server and generate the cascaded URL. If cascading cannot be + * performed (i.e. CRS not supported on the remote server) a local URL will be generated. If the + * URL should not be generated at all (i.e. requesting a GetFeatureInfo to a remote that is not + * supporting text/plain, text/html, mapml) null will be returned and the document builder won't + * add the URL (i.e. the query link). */ - private String tryCascading(String path, HashMap params, LayerInfo layerInfo) + private String generateURL(String path, HashMap params, LayerInfo layerInfo) throws UnsupportedEncodingException { String baseUrl = baseUrlPattern; String version = "1.3.0"; String reason = null; boolean doCascade = false; URLMangler.URLType urlType = URLMangler.URLType.SERVICE; + List infoFormats = new ArrayList<>(); if (layerInfo != null) { ResourceInfo resourceInfo = layerInfo.getResource(); String layerName = resourceInfo.getNativeName(); @@ -239,20 +249,31 @@ private String tryCascading(String path, HashMap params, LayerIn WMSCapabilities capabilities = wmsStoreInfo.getWebMapServer(null).getCapabilities(); version = capabilities.getVersion(); - - if (!WMS_1_3_0.equals(version)) { - version = "1.1.1"; - } - List layerList = capabilities.getLayerList(); - boolean isSupportedCrs = false; - for (Layer layer : layerList) { - if (layerName.equals(layer.getName())) { - isSupportedCrs = isSRSInLayerOrParents(layer, requestedCRS); - break; + if (checkLayers( + capabilities.getRequest().getGetFeatureInfo(), + params, + infoFormats)) { + if (!WMS_1_3_0.equals(version)) { + version = "1.1.1"; + } + List layerList = capabilities.getLayerList(); + boolean isSupportedCrs = false; + for (Layer layer : layerList) { + if (layerName.equals(layer.getName())) { + isSupportedCrs = isSRSInLayerOrParents(layer, requestedCRS); + break; + } + } + isSupportedCrs &= (outputCRS != null); + doCascade = isSupportedCrs; + } else { + if ("GetFeatureInfo".equalsIgnoreCase(params.get("request")) + && infoFormats.isEmpty()) { + LOGGER.fine( + "URL won't be generated due to Requesting a not supported GetFeatureInfo format"); + return null; } } - isSupportedCrs &= (outputCRS != null); - doCascade = isSupportedCrs; } catch (IOException e) { reason = "Unable to extract the WMS remote capabilities. Cascading won't be performed"; @@ -293,7 +314,8 @@ private String tryCascading(String path, HashMap params, LayerIn baseUrl = baseUrlAndPath[0]; path = baseUrlAndPath[1]; urlType = URLMangler.URLType.EXTERNAL; - refineRequestParams(params, layerName, version, requestedCRS, tileMatrixSet); + refineRequestParams( + params, layerName, version, requestedCRS, tileMatrixSet, infoFormats); } else { LOGGER.fine("Cascading won't be performed, due to: " + reason); } @@ -305,6 +327,20 @@ private String tryCascading(String path, HashMap params, LayerIn return urlTemplate; } + private boolean checkLayers( + OperationType getFeatureInfo, + HashMap params, + List featureInfoFormats) { + if ("GetFeatureInfo".equalsIgnoreCase(params.get("request"))) { + featureInfoFormats.addAll(getFeatureInfo.getFormats()); + featureInfoFormats.retainAll(GET_FEATURE_INFO_FORMATS); + if (featureInfoFormats.isEmpty()) { + return false; + } + } + return true; + } + private void cleanupCRS(HashMap params, String version, String requestedCRS) { boolean cleanupCrs = params.containsKey(CRS_PARAM) || params.containsKey(SRS_PARAM); if (cleanupCrs) { @@ -320,7 +356,8 @@ private void refineRequestParams( String layerName, String version, String requestedCRS, - String tileMatrixSetName) { + String tileMatrixSetName, + List infoFormats) { String requestType = params.get(REQUEST); String service = params.get(SERVICE); if (params.containsKey(LAYER)) { @@ -333,11 +370,7 @@ private void refineRequestParams( if (params.containsKey("query_layers")) { params.put("query_layers", layerName); } - if (params.containsKey("info_format")) { - params.put("info_format", "text/html"); - } else if (params.containsKey("infoformat")) { - params.put("infoformat", "text/html"); - } + refineInfoFormat(params, infoFormats); cleanupCRS(params, version, requestedCRS); } // Extra settings for WMTS @@ -354,6 +387,34 @@ private void refineRequestParams( } } + private void refineInfoFormat(HashMap params, List infoFormats) { + // When entering this method, infoFormats cannot be empty. + + String paramName = "info_format"; + String infoFormat = params.get(paramName); + if (infoFormat == null) { + paramName = "infoformat"; + infoFormat = params.get(paramName); + } + if (infoFormat != null) { + // replace the infoFormat with a supported one + infoFormat = updateInfoFormat(infoFormat, infoFormats); + params.put(paramName, infoFormat); + } + } + + private String updateInfoFormat(String infoFormat, List infoFormats) { + if (!infoFormats.contains(infoFormat)) { + // Fall back on text/html + if (infoFormats.contains("text/html") || infoFormats.isEmpty()) { + infoFormat = "text/html"; + } else { + infoFormat = infoFormats.get(0); + } + } + return infoFormat; + } + @SuppressWarnings("PMD.UseCollectionIsEmpty") private boolean hasVendorParams() { GetMapRequest req = mapContent.getRequest(); diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSGetFeatureInfoProxyTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSGetFeatureInfoProxyTest.java new file mode 100644 index 00000000000..c44c5435f8f --- /dev/null +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSGetFeatureInfoProxyTest.java @@ -0,0 +1,85 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.mapml; + +import static org.junit.Assert.assertTrue; + +import org.geoserver.catalog.Catalog; +import org.geoserver.catalog.LayerInfo; +import org.geoserver.catalog.ResourceInfo; +import org.geoserver.catalog.WMSLayerInfo; +import org.geoserver.catalog.WMSStoreInfo; +import org.geoserver.data.test.SystemTestData; +import org.junit.BeforeClass; +import org.junit.Test; +import org.w3c.dom.Document; + +public class MapMLWMSGetFeatureInfoProxyTest extends MapMLBaseProxyTest { + + @BeforeClass + public static void beforeClass() { + initMockService( + "/mockgeoserver", + "/wms", + "REQUEST=GetCapabilities&VERSION=1.3.0&SERVICE=WMS", + "wmscapsgmlfeatureinfo.xml"); + } + + @Override + protected void onSetUp(SystemTestData testData) throws Exception { + super.onSetUp(testData); + Catalog catalog = getCatalog(); + + WMSStoreInfo wmsStore = catalog.getFactory().createWebMapServer(); + wmsStore.setName("wmsStore"); + wmsStore.setWorkspace(catalog.getDefaultWorkspace()); + wmsStore.setCapabilitiesURL( + "http://localhost:" + mockService.port() + getCapabilitiesURL()); + wmsStore.setEnabled(true); + catalog.add(wmsStore); + + // Create WMSLayerInfo using the Catalog factory + WMSLayerInfo wmsLayer = catalog.getFactory().createWMSLayer(); + wmsLayer.setName("cascadedLayer"); + wmsLayer.setNativeName("topp:states"); + wmsLayer.setStore(wmsStore); + wmsLayer.setAdvertised(true); + wmsLayer.setEnabled(true); + + // Add the layer to the catalog + LayerInfo layer = catalog.getFactory().createLayer(); + layer.setResource(wmsLayer); + layer.setDefaultStyle(catalog.getStyleByName("default")); + catalog.add(wmsLayer); + catalog.add(layer); + } + + @Test + public void testMapMLLocalQueryLink() throws Exception { + Catalog cat = getCatalog(); + // Verify the layer was added + LayerInfo layerInfo = cat.getLayerByName("cascadedLayer"); + ResourceInfo layerMeta = layerInfo.getResource(); + layerMeta.getMetadata().put("mapml.useRemote", false); + cat.save(layerMeta); + String path = BASE_REQUEST; + Document doc = getMapML(path); + + String url = xpath.evaluate("//html:map-link[@rel='query']/@tref", doc); + assertTrue(url.startsWith("http://localhost:8080/geoserver" + CONTEXT)); + assertTrue(url.contains("layers=cascadedLayer")); + assertTrue(url.contains("request=GetFeatureInfo")); + + layerMeta.getMetadata().put("mapml.useRemote", true); + cat.save(layerMeta); + + // The sample getCapabilities is only returning GML as supported + // Formats for the GetFeatureInfo so the query link will not be generated + // when cascading + doc = getMapML(path); + url = xpath.evaluate("//html:map-link[@rel='query']/@tref", doc); + assertTrue(url.isEmpty()); + } +} diff --git a/src/extension/mapml/src/test/resources/__files/wmscapsgmlfeatureinfo.xml b/src/extension/mapml/src/test/resources/__files/wmscapsgmlfeatureinfo.xml new file mode 100644 index 00000000000..8825978e759 --- /dev/null +++ b/src/extension/mapml/src/test/resources/__files/wmscapsgmlfeatureinfo.xml @@ -0,0 +1,112 @@ + + + + WMS + WMS + Minimal test caps document for a WMS 1.3 server + + GEOSERVER + + + + + + + text/xml + + + + + + + + + + image/png + + + + + + + + + + application/vnd.ogc.gml + application/vnd.ogc.gml/3.1.1 + + + + + + + + + + + XML + INIMAGE + BLANK + + + Root + + EPSG:4326 + CRS:84 + + -180 + 180 + -90 + 90 + + + world4326 + world4326 + EPSG:4326 + CRS:84 + + -180 + 180 + -90 + 90 + + + + + anotherLayer + A second layer + EPSG:4326 + CRS:84 + + -180 + 180 + -90 + 90 + + + + + topp:states + An old friend + EPSG:4326 + CRS:84 + + -180 + 180 + -90 + 90 + + + + + +