Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> kvp, URLType type) {
// Default URL assembly is handled by URLManglerUtils.
}
}
26 changes: 15 additions & 11 deletions geowebcache/core/src/main/java/org/geowebcache/util/URLMangler.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,24 @@
*/
package org.geowebcache.util;

/**
* subset copied from org.geoserver.ows.URLMangler
*
* <p>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<String, String> kvp, URLType type);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* 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.
*
* <p>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.
*
* <p>You should have received a copy of the GNU Lesser General Public License along with this program. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Fernando Mino, GeoSolutions, Copyright 2026
*/
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.
*
* <p>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<String, String> 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<String, String> 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<String, String> 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<String, String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<String, String> 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));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* 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.
*
* <p>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.
*
* <p>You should have received a copy of the GNU Lesser General Public License along with this program. If not, see
* <http://www.gnu.org/licenses/>.
*
* @author Fernando Mino, GeoSolutions, Copyright 2026
*/
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<String, String> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading