diff --git a/geowebcache/core/src/main/java/org/geowebcache/util/NullURLMangler.java b/geowebcache/core/src/main/java/org/geowebcache/util/NullURLMangler.java index 6d6b98eba7..ae87cf5241 100644 --- a/geowebcache/core/src/main/java/org/geowebcache/util/NullURLMangler.java +++ b/geowebcache/core/src/main/java/org/geowebcache/util/NullURLMangler.java @@ -13,20 +13,12 @@ */ package org.geowebcache.util; -import org.apache.commons.lang3.StringUtils; +import java.util.Map; public class NullURLMangler implements URLMangler { @Override - public String buildURL(String baseURL, String contextPath, String path) { - final String context = StringUtils.strip(contextPath, "/"); - - // if context is root ("/") then don't append it to prevent double slashes ("//") in return - // URLs - if (context == null || context.isEmpty()) { - return StringUtils.strip(baseURL, "/") + "/" + StringUtils.strip(path, "/"); - } else { - return StringUtils.strip(baseURL, "/") + "/" + context + "/" + StringUtils.strip(path, "/"); - } + public void mangleURL(StringBuilder baseURL, StringBuilder path, Map kvp, URLType type) { + // Default URL assembly is handled by URLManglerUtils. } } diff --git a/geowebcache/core/src/main/java/org/geowebcache/util/URLMangler.java b/geowebcache/core/src/main/java/org/geowebcache/util/URLMangler.java index 8739b83921..3de2c302bc 100644 --- a/geowebcache/core/src/main/java/org/geowebcache/util/URLMangler.java +++ b/geowebcache/core/src/main/java/org/geowebcache/util/URLMangler.java @@ -1,32 +1,28 @@ -/** +/* * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General - * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any - * later version. - * - *

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied - * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - *

You should have received a copy of the GNU Lesser General Public License along with this program. If not, see - * . - * - * @author Robert Marianski, OpenGeo, 20012 + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) + * any later version. */ package org.geowebcache.util; -/** - * subset copied from org.geoserver.ows.URLMangler - * - *

This hook allows others to plug in custom url generation. - */ +import java.util.Map; + +/** Hook allowing custom URL generation and mangling. */ public interface URLMangler { + enum URLType { + EXTERNAL, + RESOURCE, + SERVICE + } + /** - * Allows for a custom url generation strategy + * Callback that can change the base URL, path, or query parameter map before URL serialization. * - * @param baseURL the base url - contains the url up to the domain and port - * @param contextPath the servlet context path, like /geoserver/gwc - * @param path the remaining path after the context path - * @return the full generated url from the pieces + * @param baseURL mutable base URL buffer containing host, port, and application base + * @param path mutable path buffer after the application name + * @param kvp mutable GET request parameters, which may be enriched or modified + * @param type URL type for consideration during mangling */ - public String buildURL(String baseURL, String contextPath, String path); + void mangleURL(StringBuilder baseURL, StringBuilder path, Map kvp, URLType type); } diff --git a/geowebcache/core/src/main/java/org/geowebcache/util/URLManglerUtils.java b/geowebcache/core/src/main/java/org/geowebcache/util/URLManglerUtils.java new file mode 100644 index 0000000000..ceed27c729 --- /dev/null +++ b/geowebcache/core/src/main/java/org/geowebcache/util/URLManglerUtils.java @@ -0,0 +1,74 @@ +/* + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) + * any later version. + */ +package org.geowebcache.util; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; + +/** Shared URL assembly for GeoWebCache URL manglers. */ +public final class URLManglerUtils { + + private URLManglerUtils() {} + + /** + * Builds a URL after allowing one mangler to mutate its base, path, and query parameters. + * + *

Query parameters embedded in {@code path} remain in place; parameters from {@code kvp} are appended after them + * and before any fragment. The input map is copied so callers retain ownership of their map. + */ + public static String buildURL( + String baseURL, + String contextPath, + String path, + Map kvp, + URLMangler urlMangler, + URLMangler.URLType type) { + StringBuilder base = new StringBuilder(StringUtils.strip(baseURL, "/")); + String context = StringUtils.strip(contextPath, "/"); + String remainingPath = StringUtils.stripStart(path, "/"); + boolean trailingSlash = path != null && (path.isEmpty() || path.endsWith("/")); + StringBuilder pathBuffer = new StringBuilder(); + if (StringUtils.isNotBlank(context)) pathBuffer.append(context); + if (StringUtils.isNotBlank(remainingPath)) { + if (!pathBuffer.isEmpty()) pathBuffer.append('/'); + pathBuffer.append(remainingPath); + } + Map parameters = kvp == null ? new LinkedHashMap<>() : new LinkedHashMap<>(kvp); + + urlMangler.mangleURL(base, pathBuffer, parameters, type); + + String result = base.toString(); + if (!pathBuffer.isEmpty()) { + result = appendPath(result, pathBuffer.toString()); + } + if (trailingSlash && !pathBuffer.isEmpty() && !result.endsWith("/")) result += "/"; + return appendQueryParameters(result, parameters); + } + + private static String appendPath(String base, String path) { + if (base.endsWith("/") || path.isEmpty()) return base + path; + return base + "/" + path; + } + + private static String appendQueryParameters(String url, Map parameters) { + if (parameters.isEmpty()) return url; + String fragment = ""; + int fragmentIndex = url.indexOf('#'); + if (fragmentIndex >= 0) { + fragment = url.substring(fragmentIndex); + url = url.substring(0, fragmentIndex); + } + StringBuilder query = new StringBuilder(); + for (Map.Entry entry : parameters.entrySet()) { + if (!query.isEmpty()) query.append('&'); + query.append(ServletUtils.URLEncode(entry.getKey())).append('='); + if (entry.getValue() != null) query.append(ServletUtils.URLEncode(entry.getValue())); + } + if (url.endsWith("?") || url.endsWith("&")) return url + query + fragment; + return url + (url.indexOf('?') >= 0 ? '&' : '?') + query + fragment; + } +} diff --git a/geowebcache/core/src/test/java/org/geowebcache/util/NullURLManglerTest.java b/geowebcache/core/src/test/java/org/geowebcache/util/NullURLManglerTest.java index 6cf3f957ce..479fe04ab3 100644 --- a/geowebcache/core/src/test/java/org/geowebcache/util/NullURLManglerTest.java +++ b/geowebcache/core/src/test/java/org/geowebcache/util/NullURLManglerTest.java @@ -1,5 +1,7 @@ package org.geowebcache.util; +import java.util.LinkedHashMap; +import java.util.Map; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -9,43 +11,54 @@ public class NullURLManglerTest { private URLMangler urlMangler; @Before - public void setUp() throws Exception { + public void setUp() { urlMangler = new NullURLMangler(); } @Test public void testBuildURL() { - String url = urlMangler.buildURL("http://foo.example.com", "/foo", "/bar"); - Assert.assertEquals("http://foo.example.com/foo/bar", url); + Assert.assertEquals( + "http://foo.example.com/foo/bar", + URLManglerUtils.buildURL( + "http://foo.example.com", "/foo", "/bar", null, urlMangler, URLMangler.URLType.SERVICE)); } @Test - public void testBuildTrailingSlashes() throws Exception { - String url = urlMangler.buildURL("http://foo.example.com/", "/foo/", "/bar"); - Assert.assertEquals("http://foo.example.com/foo/bar", url); + public void testBuildURLNormalizesSlashes() { + Assert.assertEquals( + "http://foo.example.com/foo/bar", + URLManglerUtils.buildURL( + "http://foo.example.com/", "/foo/", "/bar", null, urlMangler, URLMangler.URLType.SERVICE)); + Assert.assertEquals( + "http://foo.example.com/foo/bar", + URLManglerUtils.buildURL( + "http://foo.example.com/", "foo/", "bar", null, urlMangler, URLMangler.URLType.SERVICE)); } @Test - public void testBuildNoLeadingSlashes() throws Exception { - String url = urlMangler.buildURL("http://foo.example.com/", "foo/", "bar"); - Assert.assertEquals("http://foo.example.com/foo/bar", url); + public void testBuildURLWithEmptyContext() { + Assert.assertEquals( + "http://foo.example.com/bar", + URLManglerUtils.buildURL( + "http://foo.example.com/", "/", "/bar", null, urlMangler, URLMangler.URLType.SERVICE)); + Assert.assertEquals( + "http://foo.example.com/bar", + URLManglerUtils.buildURL( + "http://foo.example.com/", null, "/bar", null, urlMangler, URLMangler.URLType.SERVICE)); } @Test - public void testBuildRootContext() throws Exception { - String url = urlMangler.buildURL("http://foo.example.com/", "/", "/bar"); - Assert.assertEquals("http://foo.example.com/bar", url); - } - - @Test - public void testBuildNullContext() throws Exception { - String url = urlMangler.buildURL("http://foo.example.com/", null, "/bar"); - Assert.assertEquals("http://foo.example.com/bar", url); - } - - @Test - public void testBuildEmptyContext() throws Exception { - String url = urlMangler.buildURL("http://foo.example.com/", "", "/bar"); - Assert.assertEquals("http://foo.example.com/bar", url); + public void testBuildURLWithQueryParameters() { + Map queryParameters = new LinkedHashMap<>(); + queryParameters.put("projecttoken", "abc123"); + Assert.assertEquals( + "http://foo.example.com/foo/bar?projecttoken=abc123", + URLManglerUtils.buildURL( + "http://foo.example.com/", + "/foo", + "/bar", + queryParameters, + urlMangler, + URLMangler.URLType.SERVICE)); } } diff --git a/geowebcache/core/src/test/java/org/geowebcache/util/URLManglerUtilsTest.java b/geowebcache/core/src/test/java/org/geowebcache/util/URLManglerUtilsTest.java new file mode 100644 index 0000000000..5592f8321f --- /dev/null +++ b/geowebcache/core/src/test/java/org/geowebcache/util/URLManglerUtilsTest.java @@ -0,0 +1,73 @@ +/* + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) + * any later version. + */ +package org.geowebcache.util; + +import static org.junit.Assert.assertEquals; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.Test; + +public class URLManglerUtilsTest { + + @Test + public void testBuildURLAppendsMutatedParametersAfterPathQuery() { + URLMangler mangler = (base, path, kvp, type) -> kvp.put("projecttoken", "abc 123"); + assertEquals( + "https://host/app/wmts/{TileRow}?format=image/png&projecttoken=abc+123", + URLManglerUtils.buildURL( + "https://host/", + "/app", + "/wmts/{TileRow}?format=image/png", + new LinkedHashMap<>(), + mangler, + URLMangler.URLType.SERVICE)); + } + + @Test + public void testBuildURLPreservesOrderAndFragment() { + Map parameters = new LinkedHashMap<>(); + parameters.put("first", "one"); + parameters.put("second", "two"); + assertEquals( + "https://host/path?existing=yes&first=one&second=two#fragment", + URLManglerUtils.buildURL( + "https://host", + null, + "/path?existing=yes#fragment", + parameters, + new NullURLMangler(), + URLMangler.URLType.RESOURCE)); + } + + @Test + public void testBuildURLUsesCompleteURLCreatedByMangler() { + URLMangler mangler = (base, path, kvp, type) -> { + base.setLength(0); + base.append("https://proxy.example.com/complete"); + path.setLength(0); + kvp.clear(); + }; + assertEquals( + "https://proxy.example.com/complete", + URLManglerUtils.buildURL( + "https://host", "/context", "/path", null, mangler, URLMangler.URLType.SERVICE)); + } + + @Test + public void testBuildURLPassesTypeAndAllowsBaseAndPathChanges() { + URLMangler mangler = (base, path, kvp, type) -> { + assertEquals(URLMangler.URLType.RESOURCE, type); + base.append("/proxy"); + path.append("/resource"); + kvp.put("token", "value"); + }; + assertEquals( + "https://host/proxy/context/path/resource?token=value", + URLManglerUtils.buildURL( + "https://host", "/context", "/path", null, mangler, URLMangler.URLType.RESOURCE)); + } +} diff --git a/geowebcache/tms/src/main/java/org/geowebcache/service/tms/TMSDocumentFactory.java b/geowebcache/tms/src/main/java/org/geowebcache/service/tms/TMSDocumentFactory.java index 508b62cb98..7c05fcd2f1 100644 --- a/geowebcache/tms/src/main/java/org/geowebcache/service/tms/TMSDocumentFactory.java +++ b/geowebcache/tms/src/main/java/org/geowebcache/service/tms/TMSDocumentFactory.java @@ -27,6 +27,7 @@ import org.geowebcache.layer.meta.LayerMetaInformation; import org.geowebcache.mime.MimeType; import org.geowebcache.util.URLMangler; +import org.geowebcache.util.URLManglerUtils; /** * Basic implementation of the TMS documents. Not all of GWCs more advanced features can easily be accomodated by this @@ -95,7 +96,7 @@ protected String getTileMapServiceDoc(String baseUrl, String contextPath) { xml.header("1.0", encoding); xml.indentElement("TileMapService") .attribute("version", "1.0.0") - .attribute("services", urlMangler.buildURL(baseUrl, contextPath, "")); + .attribute("services", buildURL(baseUrl, contextPath, "")); // TODO can have these set through Spring xml.simpleElement("Title", "Tile Map Service", true); xml.simpleElement("Abstract", "A Tile Map Service served by GeoWebCache", true); @@ -178,7 +179,7 @@ protected String getTileMapDoc( xml.header("1.0", encoding); xml.indentElement("TileMap") .attribute("version", "1.0.0") - .attribute("tilemapservice", urlMangler.buildURL(baseUrl, contextPath, SERVICE_PATH)); + .attribute("tilemapservice", buildURL(baseUrl, contextPath, SERVICE_PATH)); xml.simpleElement("Title", tileMapTitle(layer), true); xml.simpleElement("Abstract", tileMapDescription(layer), true); @@ -243,12 +244,16 @@ protected String profileForGridSet(GridSet gridSet) { protected String tileMapUrl( TileLayer tl, GridSubset gridSub, MimeType mimeType, String baseUrl, String contextPath) { - return urlMangler.buildURL(baseUrl, contextPath, SERVICE_PATH + "/" + tileMapName(tl, gridSub, mimeType)); + return buildURL(baseUrl, contextPath, SERVICE_PATH + "/" + tileMapName(tl, gridSub, mimeType)); + } + + private String buildURL(String baseUrl, String contextPath, String path) { + return URLManglerUtils.buildURL(baseUrl, contextPath, path, null, urlMangler, URLMangler.URLType.SERVICE); } protected String tileMapUrl( TileLayer tl, GridSubset gridSub, MimeType mimeType, int z, String baseUrl, String contextPath) { - return tileMapUrl(tl, gridSub, mimeType, baseUrl, contextPath) + "/" + z; + return buildURL(baseUrl, contextPath, SERVICE_PATH + "/" + tileMapName(tl, gridSub, mimeType) + "/" + z); } protected String tileMapName(TileLayer tl, GridSubset gridSub, MimeType mimeType) { diff --git a/geowebcache/tms/src/test/java/org/geowebcache/service/tms/TMSServiceTest.java b/geowebcache/tms/src/test/java/org/geowebcache/service/tms/TMSServiceTest.java index ab649393d1..0639bc4e4e 100644 --- a/geowebcache/tms/src/test/java/org/geowebcache/service/tms/TMSServiceTest.java +++ b/geowebcache/tms/src/test/java/org/geowebcache/service/tms/TMSServiceTest.java @@ -16,7 +16,6 @@ import java.util.List; import java.util.Map; import java.util.regex.Pattern; -import org.apache.commons.lang3.StringUtils; import org.custommonkey.xmlunit.XMLUnit; import org.custommonkey.xmlunit.XpathEngine; import org.geowebcache.GeoWebCacheDispatcher; @@ -35,6 +34,7 @@ import org.geowebcache.stats.RuntimeStats; import org.geowebcache.storage.StorageBroker; import org.geowebcache.util.URLMangler; +import org.geowebcache.util.URLManglerUtils; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -67,15 +67,12 @@ public void setUp() throws Exception { final Pattern PATTERN = Pattern.compile("http"); @Override - public String buildURL(String baseURL, String contextPath, String path) { - String url = StringUtils.strip(baseURL, "/") - + "/" - + StringUtils.strip(contextPath, "/") - + "/" - + StringUtils.stripStart(path, "/"); - url = url.startsWith("https") ? url : PATTERN.matcher(url).replaceFirst("https"); - - return url; + public void mangleURL( + StringBuilder baseURL, StringBuilder path, Map kvp, URLMangler.URLType type) { + if (!baseURL.toString().startsWith("https")) { + baseURL.replace( + 0, baseURL.length(), PATTERN.matcher(baseURL).replaceFirst("https")); + } } }; customFactory = new TMSCustomFactoryTest( @@ -143,7 +140,7 @@ protected String getTileMapServiceDoc(String baseUrl, String contextPath) { StringBuilder str = new StringBuilder(); str.append("\n"); str.append("\n"); str.append(" Custom Tile Map Service\n"); str.append(" A Custom Tile Map Service served by GeoWebCache\n"); @@ -173,7 +170,13 @@ protected void tileMapsForLayer( str.append(" href=\""); String tileMapName = layer.name + "@EPSG:4326" + "@" + format; - String url = urlMangler.buildURL(baseUrl, contextPath, TMSDocumentFactory.SERVICE_PATH + "/" + tileMapName); + String url = URLManglerUtils.buildURL( + baseUrl, + contextPath, + TMSDocumentFactory.SERVICE_PATH + "/" + tileMapName, + null, + urlMangler, + URLMangler.URLType.SERVICE); str.append(url).append("\" />\n"); } } diff --git a/geowebcache/wms/src/main/java/org/geowebcache/service/wms/WMSGetCapabilities.java b/geowebcache/wms/src/main/java/org/geowebcache/service/wms/WMSGetCapabilities.java index 5a21977cb2..7d1d3b6ee1 100644 --- a/geowebcache/wms/src/main/java/org/geowebcache/service/wms/WMSGetCapabilities.java +++ b/geowebcache/wms/src/main/java/org/geowebcache/service/wms/WMSGetCapabilities.java @@ -53,6 +53,7 @@ import org.geowebcache.mime.MimeType; import org.geowebcache.util.ServletUtils; import org.geowebcache.util.URLMangler; +import org.geowebcache.util.URLManglerUtils; public class WMSGetCapabilities { @@ -72,7 +73,13 @@ protected WMSGetCapabilities( URLMangler urlMangler) { this.tld = tld; - urlStr = urlMangler.buildURL(baseUrl, contextPath, WMSService.SERVICE_PATH) + "?SERVICE=WMS&"; + urlStr = URLManglerUtils.buildURL( + baseUrl, + contextPath, + WMSService.SERVICE_PATH + "?SERVICE=WMS&", + null, + urlMangler, + URLMangler.URLType.SERVICE); String[] tiledKey = {"TILED"}; Map tiledValue = ServletUtils.selectedStringsFromMap( diff --git a/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSGetCapabilities.java b/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSGetCapabilities.java index 536510f591..cdd9ab520e 100644 --- a/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSGetCapabilities.java +++ b/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSGetCapabilities.java @@ -66,9 +66,7 @@ public class WMTSGetCapabilities { private GridSetBroker gsb; - private String baseUrl; - - private String restBaseUrl; + private final WMTSUrlBuilder urls; private final Collection extensions; @@ -90,21 +88,26 @@ protected WMTSGetCapabilities( String contextPath, URLMangler urlMangler, Collection extensions) { + this(tld, gsb, servReq, buildUrls(servReq, baseUrl, contextPath, urlMangler), extensions); + } + + protected WMTSGetCapabilities( + TileLayerDispatcher tld, + GridSetBroker gsb, + HttpServletRequest servReq, + WMTSUrlBuilder urls, + Collection extensions) { this.tld = tld; this.gsb = gsb; + this.urls = urls; + this.extensions = extensions; + } + private static WMTSUrlBuilder buildUrls( + HttpServletRequest request, String baseUrl, String contextPath, URLMangler urlMangler) { String forcedBaseUrl = - ServletUtils.stringFromMap(servReq.getParameterMap(), servReq.getCharacterEncoding(), "base_url"); - - if (forcedBaseUrl != null) { - this.baseUrl = forcedBaseUrl; - } else { - this.baseUrl = urlMangler.buildURL(baseUrl, contextPath, WMTSService.SERVICE_PATH); - } - - this.restBaseUrl = urlMangler.buildURL(baseUrl, contextPath, WMTSService.REST_PATH); - - this.extensions = extensions; + ServletUtils.stringFromMap(request.getParameterMap(), request.getCharacterEncoding(), "base_url"); + return new WMTSUrlBuilder(forcedBaseUrl == null ? baseUrl : forcedBaseUrl, baseUrl, contextPath, urlMangler); } protected void writeResponse(HttpServletResponse response, RuntimeStats stats) { @@ -169,11 +172,14 @@ private String generateGetCapabilities(Charset encoding) { contents(xml); xml.indentElement("ServiceMetadataURL") - .attribute("xlink:href", WMTSUtils.getKvpServiceMetadataURL(baseUrl)) + .attribute( + "xlink:href", + urls.serviceUrl( + WMTSService.SERVICE_PATH + "?SERVICE=wmts&REQUEST=getcapabilities&VERSION=1.0.0")) .endElement(); xml.indentElement("ServiceMetadataURL") - .attribute("xlink:href", restBaseUrl + "/WMTSCapabilities.xml") + .attribute("xlink:href", urls.restUrl(WMTSService.REST_PATH + "/WMTSCapabilities.xml")) .endElement(); xml.endElement("Capabilities"); @@ -365,9 +371,9 @@ private void serviceProvider(XMLBuilder xml, ServiceInformation servInfo) throws xml.endElement(); } } else { - appendTag(xml, "ows:ProviderName", baseUrl, null); + appendTag(xml, "ows:ProviderName", urls.serviceBaseUrl(), null); xml.indentElement("ows:ProviderSite") - .attribute("xlink:href", baseUrl) + .attribute("xlink:href", urls.serviceBaseUrl()) .endElement(); xml.indentElement("ows:ServiceContact"); appendTag(xml, "ows:IndividualName", "GeoWebCache User", null); @@ -379,18 +385,15 @@ private void serviceProvider(XMLBuilder xml, ServiceInformation servInfo) throws private void operationsMetadata(XMLBuilder xml) throws IOException { xml.indentElement("ows:OperationsMetadata"); - operation(xml, "GetCapabilities", baseUrl); - operation(xml, "GetTile", baseUrl); - operation(xml, "GetFeatureInfo", baseUrl); + operation(xml, "GetCapabilities", null); + operation(xml, "GetTile", null); + operation(xml, "GetFeatureInfo", null); // allow extension to inject their own metadata for (WMTSExtension extension : extensions) { List operationsMetaData = extension.getExtraOperationsMetadata(); if (operationsMetaData != null) { for (WMTSExtension.OperationMetadata operationMetadata : operationsMetaData) { - operation( - xml, - operationMetadata.getName(), - operationMetadata.getBaseUrl() == null ? baseUrl : operationMetadata.getBaseUrl()); + operation(xml, operationMetadata.getName(), operationMetadata.getBaseUrl()); } } extension.encodedOperationsMetadata(xml); @@ -398,14 +401,15 @@ private void operationsMetadata(XMLBuilder xml) throws IOException { xml.endElement("ows:OperationsMetadata"); } - private void operation(XMLBuilder xml, String operationName, String baseUrl) throws IOException { + private void operation(XMLBuilder xml, String operationName, String customBaseUrl) throws IOException { xml.indentElement("ows:Operation").attribute("name", operationName); xml.indentElement("ows:DCP"); xml.indentElement("ows:HTTP"); - if (baseUrl.contains("?")) { - xml.indentElement("ows:Get").attribute("xlink:href", baseUrl + "&"); + String href = customBaseUrl == null ? urls.serviceUrl(WMTSService.SERVICE_PATH) : urls.customUrl(customBaseUrl); + if (href.contains("?")) { + xml.indentElement("ows:Get").attribute("xlink:href", href + "&"); } else { - xml.indentElement("ows:Get").attribute("xlink:href", baseUrl + "?"); + xml.indentElement("ows:Get").attribute("xlink:href", href + "?"); } xml.indentElement("ows:Constraint").attribute("name", "GetEncoding"); xml.indentElement("ows:AllowedValues"); @@ -426,7 +430,7 @@ private void contents(XMLBuilder xml) throws IOException { if (!layer.isEnabled() || !layer.isAdvertised()) { continue; } - layer(xml, layer, baseUrl, usedGridsets); + layer(xml, layer, usedGridsets); } // only dump the gridsets actually used, as the OGC TMS spec introduced many default ones @@ -441,7 +445,7 @@ private void contents(XMLBuilder xml) throws IOException { xml.endElement("Contents"); } - private void layer(XMLBuilder xml, TileLayer layer, String baseurl, Set usedGridsets) throws IOException { + private void layer(XMLBuilder xml, TileLayer layer, Set usedGridsets) throws IOException { xml.indentElement("Layer"); LayerMetaInformation layerMeta = layer.getMetaInformation(); @@ -488,7 +492,7 @@ private void layer(XMLBuilder xml, TileLayer layer, String baseurl, Set layerGridSubSets(xml, layer, usedGridsets); - layerResourceUrls(xml, layer, filters, restBaseUrl); + layerResourceUrls(xml, layer, filters); // allow extensions to contribute extra metadata to this layer for (WMTSExtension extension : extensions) { @@ -712,9 +716,8 @@ private void layerGridSubSets(XMLBuilder xml, TileLayer layer, Set used * For each layer discovers the available image formats, feature info formats and dimensions and produce the * necessary elements. */ - private void layerResourceUrls(XMLBuilder xml, TileLayer layer, List filters, String baseurl) - throws IOException { - String baseTemplate = baseurl + "/" + layer.getName(); + private void layerResourceUrls(XMLBuilder xml, TileLayer layer, List filters) throws IOException { + String baseTemplate = WMTSService.REST_PATH + "/" + layer.getName(); String commonTemplate = baseTemplate + "/{style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}"; String commonDimensions = ""; // Extracts layer dimension @@ -728,13 +731,13 @@ private void layerResourceUrls(XMLBuilder xml, TileLayer layer, List mimeFormats = WMTSUtils.getLayerFormats(layer); for (String format : mimeFormats) { - String template = commonTemplate + "?format=" + format + commonDimensions; + String template = urls.restUrl(commonTemplate + "?format=" + format + commonDimensions); layerResourceUrlsGen(xml, format, "tile", template); } // Extracts feature info formats List infoFormats = WMTSUtils.getInfoFormats(layer); for (String format : infoFormats) { - String template = commonTemplate + "/{J}/{I}?format=" + format + commonDimensions; + String template = urls.restUrl(commonTemplate + "/{J}/{I}?format=" + format + commonDimensions); layerResourceUrlsGen(xml, format, "FeatureInfo", template); } if (layer instanceof TileJSONProvider provider) { @@ -742,7 +745,8 @@ private void layerResourceUrls(XMLBuilder xml, TileLayer layer, List urls = new ArrayList<>(); + List tileUrls = new ArrayList<>(); MimeType mimeType = convTile.getMimeType(); Set gridSubSets = layer.getGridSubsets(); for (String gridSubSet : gridSubSets) { - addTileUrl(layer, gridSubSet, mimeType, urls); + addTileUrl(layer, gridSubSet, mimeType, tileUrls); } List vectorLayers = json.getLayers(); if (vectorLayers != null && !vectorLayers.isEmpty() && !mimeType.isVector()) { @@ -68,8 +66,8 @@ public void writeResponse(TileLayer layer) { json.setLayers(null); } - String[] tileUrls = urls.toArray(new String[urls.size()]); - json.setTiles(tileUrls); + String[] serializedTileUrls = tileUrls.toArray(new String[tileUrls.size()]); + json.setTiles(serializedTileUrls); convTile.servletResp.setStatus(HttpServletResponse.SC_OK); convTile.servletResp.setContentType(ApplicationMime.json.getMimeType()); @@ -87,7 +85,7 @@ public void writeResponse(TileLayer layer) { } } - private void addTileUrl(TileLayer layer, String gridSubSet, MimeType mimeType, List urls) { + private void addTileUrl(TileLayer layer, String gridSubSet, MimeType mimeType, List tileUrls) { GridSubset grid = layer.getGridSubset(gridSubSet); int zoomLevelStart = -1; int start = -1; @@ -108,8 +106,7 @@ private void addTileUrl(TileLayer layer, String gridSubSet, MimeType mimeType, L zoomLevelStart = start; } - String tileUrl = restBaseUrl - + "/" + String tileUrl = this.urls.restUrl(WMTSService.REST_PATH + "/" + layer.getName() + "/" + (style != null ? (style + "/") : "") @@ -119,7 +116,7 @@ private void addTileUrl(TileLayer layer, String gridSubSet, MimeType mimeType, L + "{z}" + "/{y}/{x}" + "?format=" - + mimeType; - urls.add(tileUrl); + + mimeType); + tileUrls.add(tileUrl); } } diff --git a/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSUrlBuilder.java b/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSUrlBuilder.java new file mode 100644 index 0000000000..be2b47aa97 --- /dev/null +++ b/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSUrlBuilder.java @@ -0,0 +1,62 @@ +/* + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General + * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) + * any later version. + */ +package org.geowebcache.service.wmts; + +import org.geowebcache.util.URLMangler; +import org.geowebcache.util.URLManglerUtils; + +/** + * Holds the base components used to generate WMTS service and REST URLs. + * + *

The service and REST bases are deliberately kept separate. A request-level {@code base_url} override may apply to + * service and KVP links while REST resource templates must continue to use the regular servlet base URL. This builder + * does not store serialized query parameters; each complete URL is assembled through {@link URLManglerUtils} only after + * its full path template has been supplied. + * + * @param serviceBaseUrl base URL used for WMTS service and KVP links, including any request-level override + * @param restBaseUrl base URL used for WMTS REST resource links + * @param contextPath servlet context path shared by the service and REST endpoints + * @param urlMangler callback used to apply proxy, security, or other URL transformations + */ +public record WMTSUrlBuilder(String serviceBaseUrl, String restBaseUrl, String contextPath, URLMangler urlMangler) { + + /** + * Builds a URL for a WMTS service or KVP endpoint. + * + *

The supplied path may contain an existing query string or unresolved WMTS template placeholders. Parameters + * added by the mangler are serialized after that path query string. + * + * @param path endpoint path, including any endpoint-specific query string + * @return the fully assembled and mangled service URL + */ + public String serviceUrl(String path) { + return URLManglerUtils.buildURL( + serviceBaseUrl, contextPath, path, null, urlMangler, URLMangler.URLType.SERVICE); + } + + /** + * Builds a URL for a WMTS REST resource or REST capabilities endpoint. + * + * @param path REST resource path, including any existing query string or unresolved WMTS placeholders + * @return the fully assembled and mangled REST URL + */ + public String restUrl(String path) { + return URLManglerUtils.buildURL(restBaseUrl, contextPath, path, null, urlMangler, URLMangler.URLType.SERVICE); + } + + /** + * Applies URL mangling to an extension-provided absolute operation URL. + * + *

The custom URL is treated as the complete base URL, so the standard WMTS context path is not added. This + * preserves extension-specific paths while still allowing registered manglers to add or modify query parameters. + * + * @param url extension-provided operation URL + * @return the fully mangled custom operation URL + */ + public String customUrl(String url) { + return URLManglerUtils.buildURL(url, null, null, null, urlMangler, URLMangler.URLType.SERVICE); + } +} diff --git a/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSUtils.java b/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSUtils.java index 8b130791d8..3a102aacef 100644 --- a/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSUtils.java +++ b/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSUtils.java @@ -46,31 +46,4 @@ public static List getLayerDimensions(List fil } return dimensions; } - - public static String getKvpServiceMetadataURL(String baseUrl) { - String base = baseUrl; - String anchor = ""; - - // Split anchor - if (base.indexOf('#') != -1) { - anchor = base.substring(base.indexOf('#')); - base = base.substring(0, base.indexOf('#')); - } - - // Remove stray ? and &'s at the end of the URL - int l = base.length(); - while (l > 0 && (base.charAt(l - 1) == '?' || base.charAt(l - 1) == '&')) { - base = base.substring(0, l - 1); - l--; - } - - // Append the correct delimiter - if (base.indexOf('?') == -1) { - base += "?"; - } else { - base += "&"; - } - - return base + "SERVICE=wmts&REQUEST=getcapabilities&VERSION=1.0.0" + anchor; - } } diff --git a/geowebcache/wmts/src/test/java/org/geowebcache/service/wmts/WMTSRestTest.java b/geowebcache/wmts/src/test/java/org/geowebcache/service/wmts/WMTSRestTest.java index d7f54c83fe..9eb3582af3 100644 --- a/geowebcache/wmts/src/test/java/org/geowebcache/service/wmts/WMTSRestTest.java +++ b/geowebcache/wmts/src/test/java/org/geowebcache/service/wmts/WMTSRestTest.java @@ -70,6 +70,7 @@ import org.geowebcache.stats.RuntimeStats; import org.geowebcache.storage.StorageBroker; import org.geowebcache.util.ResponseUtils; +import org.geowebcache.util.URLMangler; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -207,6 +208,47 @@ public void testGetTileJSONWithoutStyle() throws Exception { assertTrue(content.contains("vector_layers")); } + /** + * Verifies that WMTS REST capabilities and TileJSON links append the propagated project token after the existing + * endpoint query parameters, while leaving the template path portion untouched. + */ + @Test + public void testProjectTokenPlacement() throws Exception { + WMTSService tokenService = wmtsServiceWithProjectToken(); + + MockHttpServletRequest capReq = new MockHttpServletRequest(); + capReq.setPathInfo("geowebcache/service/wmts/rest/WMTSCapabilities.xml"); + MockHttpServletResponse capResp = dispatch(capReq, tokenService); + + assertEquals(200, capResp.getStatus()); + Document capDoc = XMLUnit.buildTestDocument(capResp.getContentAsString()); + assertXpathExists( + "//wmts:Contents/wmts:Layer/wmts:ResourceURL[@resourceType='tile']" + + "[@format='image/jpeg']" + + "[@template='http://localhost/service/wmts/rest/mockLayer/{style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}?format=image/jpeg&time={time}&elevation={elevation}&projecttoken=abc123']", + capDoc); + assertXpathExists( + "//wmts:Contents/wmts:Layer/wmts:ResourceURL[@resourceType='tile']" + + "[@format='image/png']" + + "[@template='http://localhost/service/wmts/rest/mockLayer/{style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}?format=image/png&time={time}&elevation={elevation}&projecttoken=abc123']", + capDoc); + assertXpathExists( + "//wmts:ServiceMetadataURL[@xlink:href='http://localhost/service/wmts/rest/WMTSCapabilities.xml?projecttoken=abc123']", + capDoc); + + addTileLayerJsonMock("image/png"); + MockHttpServletRequest tileJsonReq = new MockHttpServletRequest(); + tileJsonReq.setPathInfo("geowebcache/service/wmts/rest/mockLayerTileJSON/style-a/tilejson/png"); + tileJsonReq.addParameter("format", "application/json"); + MockHttpServletResponse tileJsonResp = dispatch(tileJsonReq, tokenService); + + assertEquals(200, tileJsonResp.getStatus()); + String tileJsonContent = tileJsonResp.getContentAsString(); + assertTrue( + tileJsonContent.contains( + "\"tiles\":[\"http://localhost/service/wmts/rest/mockLayerTileJSON/style-a/EPSG:900913/EPSG:900913:{z}/{y}/{x}?format=image/png&projecttoken=abc123\"]")); + } + private void addTileLayerJsonMock(String mimeType) throws GeoWebCacheException { final MimeType mimeType1 = MimeType.createFromFormat(mimeType); @@ -236,9 +278,13 @@ private void addTileLayerJsonMock(String mimeType) throws GeoWebCacheException { } public MockHttpServletResponse dispatch(MockHttpServletRequest req) throws Exception { + return dispatch(req, wmtsService); + } + + private MockHttpServletResponse dispatch(MockHttpServletRequest req, WMTSService service) throws Exception { MockHttpServletResponse resp = new MockHttpServletResponse(); try { - final Conveyor conveyor = wmtsService.getConveyor(req, resp); + final Conveyor conveyor = service.getConveyor(req, resp); if (conveyor.reqHandler == Conveyor.RequestHandler.SERVICE) { final String layerName = conveyor.getLayerId(); @@ -254,7 +300,7 @@ public MockHttpServletResponse dispatch(MockHttpServletRequest req) throws Excep tile.setTileLayer(layer); } } - wmtsService.handleRequest(conveyor); + service.handleRequest(conveyor); } else { ResponseUtils.writeTile( securityDispatcher(), @@ -277,6 +323,23 @@ public MockHttpServletResponse dispatch(MockHttpServletRequest req) throws Excep return resp; } + private WMTSService wmtsServiceWithProjectToken() { + URLMangler tokenMangler = new URLMangler() { + @Override + public void mangleURL( + StringBuilder baseURL, + StringBuilder path, + Map queryParameters, + URLMangler.URLType type) { + queryParameters.put("projecttoken", "abc123"); + } + }; + WMTSService service = + new WMTSService(storageBroker, tileLayerDispatcher, broker, runtimeStats, tokenMangler, null); + service.setSecurityDispatcher(securityDispatcher); + return service; + } + @Test public void testGetTileWithoutStyle() throws Exception { MockHttpServletRequest req = new MockHttpServletRequest(); diff --git a/geowebcache/wmts/src/test/java/org/geowebcache/service/wmts/WMTSServiceTest.java b/geowebcache/wmts/src/test/java/org/geowebcache/service/wmts/WMTSServiceTest.java index affc2d431d..ad6355085f 100644 --- a/geowebcache/wmts/src/test/java/org/geowebcache/service/wmts/WMTSServiceTest.java +++ b/geowebcache/wmts/src/test/java/org/geowebcache/service/wmts/WMTSServiceTest.java @@ -14,6 +14,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -85,6 +86,7 @@ import org.geowebcache.stats.RuntimeStats; import org.geowebcache.storage.StorageBroker; import org.geowebcache.util.NullURLMangler; +import org.geowebcache.util.URLMangler; import org.geowebcache.util.URLs; import org.junit.Before; import org.junit.Rule; @@ -1346,6 +1348,235 @@ public void testGetFeatureInfoSecure() throws Exception { } } + /** + * Verifies that GetCapabilities builds distinct query parameter maps for the service and REST URLs so each one can + * be mutated independently before final serialization. + */ + @Test + public void testGetCapabilitiesQueryMaps() throws Exception { + ProjectTokenCapabilitiesResult result = runProjectTokenCapabilitiesScenario(); + + // Expect two separate invocations: one for the service backlink and one for + // the REST capabilities backlink. + assertTrue(result.capturedQueryMaps.size() >= 2); + assertTrue( + result.capturedPaths.stream().anyMatch(path -> path.contains(WMTSService.SERVICE_PATH.substring(1)))); + assertTrue(result.capturedPaths.stream().anyMatch(path -> path.contains(WMTSService.REST_PATH.substring(1)))); + + // The two query maps must not be shared, and both must carry the propagated + // project token before serialization. + assertNotSame(result.capturedQueryMaps.get(0), result.capturedQueryMaps.get(1)); + assertTrue(result.capturedQueryMaps.stream().allMatch(map -> "abc123".equals(map.get("projecttoken")))); + } + + /** + * Verifies that GetCapabilities serializes WMTS backlinks with propagated security parameters appended after the + * endpoint-specific query parameters. + */ + @Test + public void testGetCapabilitiesQueryParamsPlacement() throws Exception { + ProjectTokenCapabilitiesResult result = runProjectTokenCapabilitiesScenario(); + Document doc = result.document; + XpathEngine xpath = buildWMTSXPath(); + + assertEquals( + "1", + xpath.evaluate( + "count(//wmts:Contents/wmts:Layer/wmts:ResourceURL[@resourceType='tile']" + + "[@format='image/png']" + + "[contains(@template,'/mockLayer/{style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}?format=image/png&time={time}&elevation={elevation}&projecttoken=abc123')])", + doc)); + assertEquals( + "1", + xpath.evaluate( + "count(//wmts:Contents/wmts:Layer/wmts:ResourceURL[@resourceType='FeatureInfo']" + + "[@format='text/plain']" + + "[contains(@template,'/mockLayer/{style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}/{J}/{I}?format=text/plain&time={time}&elevation={elevation}&projecttoken=abc123')])", + doc)); + assertEquals( + "1", + xpath.evaluate( + "count(//wmts:Contents/wmts:Layer/wmts:ResourceURL[@resourceType='TileJSON']" + + "[@format='application/json']" + + "[contains(@template,'/mockLayer/{style}/tilejson/png?format=application/json&projecttoken=abc123')])", + doc)); + assertEquals( + "1", + xpath.evaluate( + "count(//wmts:ServiceMetadataURL[contains(@xlink:href,'SERVICE=wmts&REQUEST=getcapabilities&VERSION=1.0.0&projecttoken=abc123')])", + doc)); + assertEquals( + "1", + xpath.evaluate( + "count(//wmts:ServiceMetadataURL[contains(@xlink:href,'/WMTSCapabilities.xml?projecttoken=abc123')])", + doc)); + assertEquals( + "1", + xpath.evaluate( + "count(//ows:OperationsMetadata/ows:Operation[@name='GetCapabilities']" + + "/ows:DCP/ows:HTTP/ows:Get[contains(@xlink:href,'?projecttoken=abc123&')])", + doc)); + assertEquals( + "1", + xpath.evaluate( + "count(//ows:OperationsMetadata/ows:Operation[@name='GetTile']" + + "/ows:DCP/ows:HTTP/ows:Get[contains(@xlink:href,'?projecttoken=abc123&')])", + doc)); + assertEquals( + "1", + xpath.evaluate( + "count(//ows:OperationsMetadata/ows:Operation[@name='GetFeatureInfo']" + + "/ows:DCP/ows:HTTP/ows:Get[contains(@xlink:href,'?projecttoken=abc123&')])", + doc)); + } + + /** + * Verifies that the GetCapabilities {@code base_url} parameter overrides the service backlinks while REST resource + * URLs continue to use the regular servlet base URL. + */ + @Test + public void testGetCapabilitiesHonorsForcedBaseUrl() throws Exception { + ProjectTokenCapabilitiesResult result = runProjectTokenCapabilitiesScenario("https://proxy.example.com"); + XpathEngine xpath = buildWMTSXPath(); + + assertEquals( + "1", + xpath.evaluate( + "count(//wmts:ServiceMetadataURL[contains(@xlink:href,'https://proxy.example.com')])", + result.document)); + assertTrue(result.capturedBaseUrls.contains("https://proxy.example.com")); + assertTrue(result.capturedBaseUrls.contains("http://localhost")); + } + + /** + * Verifies that {@code operationsMetadata} propagates the service query parameters (e.g. the security token added + * by a registered {@link URLMangler}) to a {@code WMTSExtension}-supplied {@code OperationMetadata} even when that + * extension provides its own custom base URL instead of {@code urls.serviceBaseUrl()}. + */ + @Test + public void testGetCapabilitiesQueryParamPropagatesToExtensionCustomBaseUrl() throws Exception { + WMTSExtension extension = new WMTSExtensionImpl() { + @Override + public List getExtraOperationsMetadata() throws IOException { + return Collections.singletonList( + new OperationMetadata("ExtraOperation", "http://extension.example.com/custom")); + } + }; + + ProjectTokenCapabilitiesResult result = + runProjectTokenCapabilitiesScenario(Collections.singletonList(extension)); + XpathEngine xpath = buildWMTSXPath(); + + assertEquals( + "1", + xpath.evaluate( + "count(//ows:OperationsMetadata/ows:Operation[@name='ExtraOperation']" + + "/ows:DCP/ows:HTTP/ows:Get[@xlink:href='http://extension.example.com/custom?projecttoken=abc123&'])", + result.document)); + } + + private ProjectTokenCapabilitiesResult runProjectTokenCapabilitiesScenario() throws Exception { + return runProjectTokenCapabilitiesScenario(Collections.emptyList(), null); + } + + private ProjectTokenCapabilitiesResult runProjectTokenCapabilitiesScenario(List extraExtensions) + throws Exception { + return runProjectTokenCapabilitiesScenario(extraExtensions, null); + } + + private ProjectTokenCapabilitiesResult runProjectTokenCapabilitiesScenario(String forcedBaseUrl) throws Exception { + return runProjectTokenCapabilitiesScenario(Collections.emptyList(), forcedBaseUrl); + } + + private ProjectTokenCapabilitiesResult runProjectTokenCapabilitiesScenario( + List extraExtensions, String forcedBaseUrl) throws Exception { + // Capture the paths and query maps passed to the URL mangler so we can + // verify that service and REST backlinks are built from separate inputs. + List> capturedQueryMaps = new ArrayList<>(); + List capturedPaths = new ArrayList<>(); + List capturedBaseUrls = new ArrayList<>(); + // Record the WMTS URL-building calls while still returning a plausible URL + // string so the GetCapabilities flow can complete normally. + URLMangler recordingUrlMangler = new URLMangler() { + @Override + public void mangleURL( + StringBuilder baseURL, + StringBuilder path, + Map queryParameters, + URLMangler.URLType type) { + capturedBaseUrls.add(baseURL.toString()); + capturedPaths.add(path.toString()); + queryParameters.put("projecttoken", "abc123"); + capturedQueryMaps.add(new HashMap<>(queryParameters)); + } + }; + // Wire the service with the recording mangler and a minimal dispatcher so we + // can run the GetCapabilities request end to end. + GeoWebCacheDispatcher gwcd = mock(GeoWebCacheDispatcher.class); + when(gwcd.getServletPrefix()).thenReturn(null); + service = new WMTSService(sb, tld, gridsetBroker, mock(RuntimeStats.class), recordingUrlMangler, gwcd); + extraExtensions.forEach(service::addExtension); + + // Provide one layer and one gridset so the capabilities document includes the + // backlinks that exercise this code path. + List gridSetNames = Arrays.asList("EPSG:900913"); + TileLayer tileLayer = mockTileLayerWithJSONSupport("mockLayer", gridSetNames); + when(tileLayer.getInfoMimeTypes()) + .thenReturn(Collections.singletonList(MimeType.createFromFormat("text/plain"))); + ParameterFilter time = mock(ParameterFilter.class); + when(time.isUserVisible()).thenReturn(true); + when(time.getKey()).thenReturn("time"); + when(time.getDefaultValue()).thenReturn("2024-01-01"); + when(time.getLegalValues()).thenReturn(Collections.singletonList("2024-01-01")); + ParameterFilter elevation = mock(ParameterFilter.class); + when(elevation.isUserVisible()).thenReturn(true); + when(elevation.getKey()).thenReturn("elevation"); + when(elevation.getDefaultValue()).thenReturn("0"); + when(elevation.getLegalValues()).thenReturn(Collections.singletonList("0")); + when(tileLayer.getParameterFilters()).thenReturn(Arrays.asList(time, elevation)); + when(tld.getLayerList()).thenReturn(Collections.singletonList(tileLayer)); + when(tld.getLayerListFiltered()).thenReturn(Collections.singletonList(tileLayer)); + + // Build a GetCapabilities request and let the WMTS service handle it. + MockHttpServletRequest req = new MockHttpServletRequest(); + req.setPathInfo("geowebcache/service/wmts"); + req.addParameter("service", "WMTS"); + req.addParameter("request", "GetCapabilities"); + if (forcedBaseUrl != null) { + req.addParameter("base_url", forcedBaseUrl); + } + MockHttpServletResponse resp = new MockHttpServletResponse(); + + Conveyor conv = service.getConveyor(req, resp); + assertNotNull(conv); + + service.handleRequest(conv); + + return new ProjectTokenCapabilitiesResult( + capturedBaseUrls, + capturedQueryMaps, + capturedPaths, + XMLUnit.buildTestDocument(resp.getContentAsString())); + } + + private static final class ProjectTokenCapabilitiesResult { + private final List capturedBaseUrls; + private final List> capturedQueryMaps; + private final List capturedPaths; + private final Document document; + + private ProjectTokenCapabilitiesResult( + List capturedBaseUrls, + List> capturedQueryMaps, + List capturedPaths, + Document document) { + this.capturedBaseUrls = capturedBaseUrls; + this.capturedQueryMaps = capturedQueryMaps; + this.capturedPaths = capturedPaths; + this.document = document; + } + } + @Test public void testGetCapWithTileJSONDifferentUrls() throws Exception { @@ -1431,7 +1662,8 @@ public void testGetCapWithTileJSONDifferentUrls() throws Exception { private String writeTileJsonResponse(ConveyorTile conv, TileLayer tileLayer, MockHttpServletResponse resp) throws UnsupportedEncodingException { - WMTSTileJSON tileJSON = new WMTSTileJSON(conv, "http://localhost", "", null, new NullURLMangler()); + WMTSTileJSON tileJSON = new WMTSTileJSON( + conv, new WMTSUrlBuilder("http://localhost", "http://localhost", "", new NullURLMangler()), null); tileJSON.writeResponse(tileLayer); return resp.getContentAsString(); } diff --git a/geowebcache/wmts/src/test/java/org/geowebcache/service/wmts/WMTSUtilsTest.java b/geowebcache/wmts/src/test/java/org/geowebcache/service/wmts/WMTSUtilsTest.java deleted file mode 100644 index caec299b45..0000000000 --- a/geowebcache/wmts/src/test/java/org/geowebcache/service/wmts/WMTSUtilsTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.geowebcache.service.wmts; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -public class WMTSUtilsTest { - - @Test - public void testKvpServiceMetadataURLNoParameters() throws Exception { - String result = WMTSUtils.getKvpServiceMetadataURL("https://www.foo.com/"); - assertEquals("https://www.foo.com/?SERVICE=wmts&REQUEST=getcapabilities&VERSION=1.0.0", result); - } - - @Test - public void testKvpServiceMetadataURLNoParametersWithAnchor() throws Exception { - String result = WMTSUtils.getKvpServiceMetadataURL("https://www.foo.com/#anchor"); - assertEquals("https://www.foo.com/?SERVICE=wmts&REQUEST=getcapabilities&VERSION=1.0.0#anchor", result); - } - - @Test - public void testKvpServiceMetadataURLNoParameters_questionMark() throws Exception { - String result = WMTSUtils.getKvpServiceMetadataURL("https://www.foo.com/?"); - assertEquals("https://www.foo.com/?SERVICE=wmts&REQUEST=getcapabilities&VERSION=1.0.0", result); - } - - @Test - public void testKvpServiceMetadataURLSingleParameter() throws Exception { - String result = WMTSUtils.getKvpServiceMetadataURL("https://www.foo.com/?bar=doo"); - assertEquals("https://www.foo.com/?bar=doo&SERVICE=wmts&REQUEST=getcapabilities&VERSION=1.0.0", result); - } - - @Test - public void testKvpServiceMetadataURLSingleParameter_ampersand() throws Exception { - String result = WMTSUtils.getKvpServiceMetadataURL("https://www.foo.com/?bar=doo&"); - assertEquals("https://www.foo.com/?bar=doo&SERVICE=wmts&REQUEST=getcapabilities&VERSION=1.0.0", result); - } - - @Test - public void testKvpServiceMetadataURLMultipleParameters() throws Exception { - String result = WMTSUtils.getKvpServiceMetadataURL("https://www.foo.com/?bar=doo&dii=daa"); - assertEquals("https://www.foo.com/?bar=doo&dii=daa&SERVICE=wmts&REQUEST=getcapabilities&VERSION=1.0.0", result); - } - - @Test - public void testKvpServiceMetadataURLMultipleParameters_manyAmpersands() throws Exception { - String result = WMTSUtils.getKvpServiceMetadataURL("https://www.foo.com/?bar=doo&dii=daa&&&"); - assertEquals("https://www.foo.com/?bar=doo&dii=daa&SERVICE=wmts&REQUEST=getcapabilities&VERSION=1.0.0", result); - } - - @Test - public void testKvpServiceMetadataURLMultipleParameters_manyAmpersands_withAnchor() throws Exception { - String result = WMTSUtils.getKvpServiceMetadataURL("https://www.foo.com/?bar=doo&dii=daa&&&#hello"); - assertEquals( - "https://www.foo.com/?bar=doo&dii=daa&SERVICE=wmts&REQUEST=getcapabilities&VERSION=1.0.0#hello", - result); - } -}