Skip to content

Commit 057e796

Browse files
fernandor777aaime
authored andcommitted
[GEOS-12149] GeoServer WMTS capabilities put query parameters in the wrong place in generated URLs
Review fixes
1 parent 8969442 commit 057e796

16 files changed

Lines changed: 681 additions & 199 deletions

File tree

geowebcache/core/src/main/java/org/geowebcache/util/NullURLMangler.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,12 @@
1313
*/
1414
package org.geowebcache.util;
1515

16-
import org.apache.commons.lang3.StringUtils;
16+
import java.util.Map;
1717

1818
public class NullURLMangler implements URLMangler {
1919

2020
@Override
21-
public String buildURL(String baseURL, String contextPath, String path) {
22-
final String context = StringUtils.strip(contextPath, "/");
23-
24-
// if context is root ("/") then don't append it to prevent double slashes ("//") in return
25-
// URLs
26-
if (context == null || context.isEmpty()) {
27-
return StringUtils.strip(baseURL, "/") + "/" + StringUtils.strip(path, "/");
28-
} else {
29-
return StringUtils.strip(baseURL, "/") + "/" + context + "/" + StringUtils.strip(path, "/");
30-
}
21+
public void mangleURL(StringBuilder baseURL, StringBuilder path, Map<String, String> kvp, URLType type) {
22+
// Default URL assembly is handled by URLManglerUtils.
3123
}
3224
}

geowebcache/core/src/main/java/org/geowebcache/util/URLMangler.java

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,24 @@
1313
*/
1414
package org.geowebcache.util;
1515

16-
/**
17-
* subset copied from org.geoserver.ows.URLMangler
18-
*
19-
* <p>This hook allows others to plug in custom url generation.
20-
*/
16+
import java.util.Map;
17+
18+
/** Hook allowing custom URL generation and mangling. */
2119
public interface URLMangler {
2220

21+
enum URLType {
22+
EXTERNAL,
23+
RESOURCE,
24+
SERVICE
25+
}
26+
2327
/**
24-
* Allows for a custom url generation strategy
28+
* Callback that can change the base URL, path, or query parameter map before URL serialization.
2529
*
26-
* @param baseURL the base url - contains the url up to the domain and port
27-
* @param contextPath the servlet context path, like /geoserver/gwc
28-
* @param path the remaining path after the context path
29-
* @return the full generated url from the pieces
30+
* @param baseURL mutable base URL buffer containing host, port, and application base
31+
* @param path mutable path buffer after the application name
32+
* @param kvp mutable GET request parameters, which may be enriched or modified
33+
* @param type URL type for consideration during mangling
3034
*/
31-
public String buildURL(String baseURL, String contextPath, String path);
35+
void mangleURL(StringBuilder baseURL, StringBuilder path, Map<String, String> kvp, URLType type);
3236
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
3+
* Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
4+
* later version.
5+
*
6+
* <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
7+
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
8+
*
9+
* <p>You should have received a copy of the GNU Lesser General Public License along with this program. If not, see
10+
* <http://www.gnu.org/licenses/>.
11+
*
12+
* @author Fernando Mino, GeoSolutions, Copyright 2026
13+
*/
14+
package org.geowebcache.util;
15+
16+
import java.util.LinkedHashMap;
17+
import java.util.Map;
18+
import org.apache.commons.lang3.StringUtils;
19+
20+
/** Shared URL assembly for GeoWebCache URL manglers. */
21+
public final class URLManglerUtils {
22+
23+
private URLManglerUtils() {}
24+
25+
/**
26+
* Builds a URL after allowing one mangler to mutate its base, path, and query parameters.
27+
*
28+
* <p>Query parameters embedded in {@code path} remain in place; parameters from {@code kvp} are appended after them
29+
* and before any fragment. The input map is copied so callers retain ownership of their map.
30+
*/
31+
public static String buildURL(
32+
String baseURL,
33+
String contextPath,
34+
String path,
35+
Map<String, String> kvp,
36+
URLMangler urlMangler,
37+
URLMangler.URLType type) {
38+
StringBuilder base = new StringBuilder(StringUtils.strip(baseURL, "/"));
39+
String context = StringUtils.strip(contextPath, "/");
40+
String remainingPath = StringUtils.stripStart(path, "/");
41+
boolean trailingSlash = path != null && (path.isEmpty() || path.endsWith("/"));
42+
StringBuilder pathBuffer = new StringBuilder();
43+
if (StringUtils.isNotBlank(context)) pathBuffer.append(context);
44+
if (StringUtils.isNotBlank(remainingPath)) {
45+
if (!pathBuffer.isEmpty()) pathBuffer.append('/');
46+
pathBuffer.append(remainingPath);
47+
}
48+
Map<String, String> parameters = kvp == null ? new LinkedHashMap<>() : new LinkedHashMap<>(kvp);
49+
50+
urlMangler.mangleURL(base, pathBuffer, parameters, type);
51+
52+
String result = base.toString();
53+
if (!pathBuffer.isEmpty()) {
54+
result = appendPath(result, pathBuffer.toString());
55+
}
56+
if (trailingSlash && !pathBuffer.isEmpty() && !result.endsWith("/")) result += "/";
57+
return appendQueryParameters(result, parameters);
58+
}
59+
60+
private static String appendPath(String base, String path) {
61+
if (base.endsWith("/") || path.isEmpty()) return base + path;
62+
return base + "/" + path;
63+
}
64+
65+
private static String appendQueryParameters(String url, Map<String, String> parameters) {
66+
if (parameters.isEmpty()) return url;
67+
String fragment = "";
68+
int fragmentIndex = url.indexOf('#');
69+
if (fragmentIndex >= 0) {
70+
fragment = url.substring(fragmentIndex);
71+
url = url.substring(0, fragmentIndex);
72+
}
73+
StringBuilder query = new StringBuilder();
74+
for (Map.Entry<String, String> entry : parameters.entrySet()) {
75+
if (!query.isEmpty()) query.append('&');
76+
query.append(ServletUtils.URLEncode(entry.getKey())).append('=');
77+
if (entry.getValue() != null) query.append(ServletUtils.URLEncode(entry.getValue()));
78+
}
79+
if (url.endsWith("?") || url.endsWith("&")) return url + query + fragment;
80+
return url + (url.indexOf('?') >= 0 ? '&' : '?') + query + fragment;
81+
}
82+
}
Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.geowebcache.util;
22

3+
import java.util.LinkedHashMap;
4+
import java.util.Map;
35
import org.junit.Assert;
46
import org.junit.Before;
57
import org.junit.Test;
@@ -9,43 +11,54 @@ public class NullURLManglerTest {
911
private URLMangler urlMangler;
1012

1113
@Before
12-
public void setUp() throws Exception {
14+
public void setUp() {
1315
urlMangler = new NullURLMangler();
1416
}
1517

1618
@Test
1719
public void testBuildURL() {
18-
String url = urlMangler.buildURL("http://foo.example.com", "/foo", "/bar");
19-
Assert.assertEquals("http://foo.example.com/foo/bar", url);
20+
Assert.assertEquals(
21+
"http://foo.example.com/foo/bar",
22+
URLManglerUtils.buildURL(
23+
"http://foo.example.com", "/foo", "/bar", null, urlMangler, URLMangler.URLType.SERVICE));
2024
}
2125

2226
@Test
23-
public void testBuildTrailingSlashes() throws Exception {
24-
String url = urlMangler.buildURL("http://foo.example.com/", "/foo/", "/bar");
25-
Assert.assertEquals("http://foo.example.com/foo/bar", url);
27+
public void testBuildURLNormalizesSlashes() {
28+
Assert.assertEquals(
29+
"http://foo.example.com/foo/bar",
30+
URLManglerUtils.buildURL(
31+
"http://foo.example.com/", "/foo/", "/bar", null, urlMangler, URLMangler.URLType.SERVICE));
32+
Assert.assertEquals(
33+
"http://foo.example.com/foo/bar",
34+
URLManglerUtils.buildURL(
35+
"http://foo.example.com/", "foo/", "bar", null, urlMangler, URLMangler.URLType.SERVICE));
2636
}
2737

2838
@Test
29-
public void testBuildNoLeadingSlashes() throws Exception {
30-
String url = urlMangler.buildURL("http://foo.example.com/", "foo/", "bar");
31-
Assert.assertEquals("http://foo.example.com/foo/bar", url);
39+
public void testBuildURLWithEmptyContext() {
40+
Assert.assertEquals(
41+
"http://foo.example.com/bar",
42+
URLManglerUtils.buildURL(
43+
"http://foo.example.com/", "/", "/bar", null, urlMangler, URLMangler.URLType.SERVICE));
44+
Assert.assertEquals(
45+
"http://foo.example.com/bar",
46+
URLManglerUtils.buildURL(
47+
"http://foo.example.com/", null, "/bar", null, urlMangler, URLMangler.URLType.SERVICE));
3248
}
3349

3450
@Test
35-
public void testBuildRootContext() throws Exception {
36-
String url = urlMangler.buildURL("http://foo.example.com/", "/", "/bar");
37-
Assert.assertEquals("http://foo.example.com/bar", url);
38-
}
39-
40-
@Test
41-
public void testBuildNullContext() throws Exception {
42-
String url = urlMangler.buildURL("http://foo.example.com/", null, "/bar");
43-
Assert.assertEquals("http://foo.example.com/bar", url);
44-
}
45-
46-
@Test
47-
public void testBuildEmptyContext() throws Exception {
48-
String url = urlMangler.buildURL("http://foo.example.com/", "", "/bar");
49-
Assert.assertEquals("http://foo.example.com/bar", url);
51+
public void testBuildURLWithQueryParameters() {
52+
Map<String, String> queryParameters = new LinkedHashMap<>();
53+
queryParameters.put("projecttoken", "abc123");
54+
Assert.assertEquals(
55+
"http://foo.example.com/foo/bar?projecttoken=abc123",
56+
URLManglerUtils.buildURL(
57+
"http://foo.example.com/",
58+
"/foo",
59+
"/bar",
60+
queryParameters,
61+
urlMangler,
62+
URLMangler.URLType.SERVICE));
5063
}
5164
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
3+
* Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
4+
* later version.
5+
*
6+
* <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
7+
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
8+
*
9+
* <p>You should have received a copy of the GNU Lesser General Public License along with this program. If not, see
10+
* <http://www.gnu.org/licenses/>.
11+
*
12+
* @author Fernando Mino, GeoSolutions, Copyright 2026
13+
*/
14+
package org.geowebcache.util;
15+
16+
import static org.junit.Assert.assertEquals;
17+
18+
import java.util.LinkedHashMap;
19+
import java.util.Map;
20+
import org.junit.Test;
21+
22+
public class URLManglerUtilsTest {
23+
24+
@Test
25+
public void testBuildURLAppendsMutatedParametersAfterPathQuery() {
26+
URLMangler mangler = (base, path, kvp, type) -> kvp.put("projecttoken", "abc 123");
27+
assertEquals(
28+
"https://host/app/wmts/{TileRow}?format=image/png&projecttoken=abc+123",
29+
URLManglerUtils.buildURL(
30+
"https://host/",
31+
"/app",
32+
"/wmts/{TileRow}?format=image/png",
33+
new LinkedHashMap<>(),
34+
mangler,
35+
URLMangler.URLType.SERVICE));
36+
}
37+
38+
@Test
39+
public void testBuildURLPreservesOrderAndFragment() {
40+
Map<String, String> parameters = new LinkedHashMap<>();
41+
parameters.put("first", "one");
42+
parameters.put("second", "two");
43+
assertEquals(
44+
"https://host/path?existing=yes&first=one&second=two#fragment",
45+
URLManglerUtils.buildURL(
46+
"https://host",
47+
null,
48+
"/path?existing=yes#fragment",
49+
parameters,
50+
new NullURLMangler(),
51+
URLMangler.URLType.RESOURCE));
52+
}
53+
54+
@Test
55+
public void testBuildURLUsesCompleteURLCreatedByMangler() {
56+
URLMangler mangler = (base, path, kvp, type) -> {
57+
base.setLength(0);
58+
base.append("https://proxy.example.com/complete");
59+
path.setLength(0);
60+
kvp.clear();
61+
};
62+
assertEquals(
63+
"https://proxy.example.com/complete",
64+
URLManglerUtils.buildURL(
65+
"https://host", "/context", "/path", null, mangler, URLMangler.URLType.SERVICE));
66+
}
67+
68+
@Test
69+
public void testBuildURLPassesTypeAndAllowsBaseAndPathChanges() {
70+
URLMangler mangler = (base, path, kvp, type) -> {
71+
assertEquals(URLMangler.URLType.RESOURCE, type);
72+
base.append("/proxy");
73+
path.append("/resource");
74+
kvp.put("token", "value");
75+
};
76+
assertEquals(
77+
"https://host/proxy/context/path/resource?token=value",
78+
URLManglerUtils.buildURL(
79+
"https://host", "/context", "/path", null, mangler, URLMangler.URLType.RESOURCE));
80+
}
81+
}

geowebcache/tms/src/main/java/org/geowebcache/service/tms/TMSDocumentFactory.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.geowebcache.layer.meta.LayerMetaInformation;
2828
import org.geowebcache.mime.MimeType;
2929
import org.geowebcache.util.URLMangler;
30+
import org.geowebcache.util.URLManglerUtils;
3031

3132
/**
3233
* 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) {
9596
xml.header("1.0", encoding);
9697
xml.indentElement("TileMapService")
9798
.attribute("version", "1.0.0")
98-
.attribute("services", urlMangler.buildURL(baseUrl, contextPath, ""));
99+
.attribute("services", buildURL(baseUrl, contextPath, ""));
99100
// TODO can have these set through Spring
100101
xml.simpleElement("Title", "Tile Map Service", true);
101102
xml.simpleElement("Abstract", "A Tile Map Service served by GeoWebCache", true);
@@ -178,7 +179,7 @@ protected String getTileMapDoc(
178179
xml.header("1.0", encoding);
179180
xml.indentElement("TileMap")
180181
.attribute("version", "1.0.0")
181-
.attribute("tilemapservice", urlMangler.buildURL(baseUrl, contextPath, SERVICE_PATH));
182+
.attribute("tilemapservice", buildURL(baseUrl, contextPath, SERVICE_PATH));
182183
xml.simpleElement("Title", tileMapTitle(layer), true);
183184
xml.simpleElement("Abstract", tileMapDescription(layer), true);
184185

@@ -243,12 +244,16 @@ protected String profileForGridSet(GridSet gridSet) {
243244

244245
protected String tileMapUrl(
245246
TileLayer tl, GridSubset gridSub, MimeType mimeType, String baseUrl, String contextPath) {
246-
return urlMangler.buildURL(baseUrl, contextPath, SERVICE_PATH + "/" + tileMapName(tl, gridSub, mimeType));
247+
return buildURL(baseUrl, contextPath, SERVICE_PATH + "/" + tileMapName(tl, gridSub, mimeType));
248+
}
249+
250+
private String buildURL(String baseUrl, String contextPath, String path) {
251+
return URLManglerUtils.buildURL(baseUrl, contextPath, path, null, urlMangler, URLMangler.URLType.SERVICE);
247252
}
248253

249254
protected String tileMapUrl(
250255
TileLayer tl, GridSubset gridSub, MimeType mimeType, int z, String baseUrl, String contextPath) {
251-
return tileMapUrl(tl, gridSub, mimeType, baseUrl, contextPath) + "/" + z;
256+
return buildURL(baseUrl, contextPath, SERVICE_PATH + "/" + tileMapName(tl, gridSub, mimeType) + "/" + z);
252257
}
253258

254259
protected String tileMapName(TileLayer tl, GridSubset gridSub, MimeType mimeType) {

0 commit comments

Comments
 (0)