From c36d1ad20cf6be258dbf10af004ec1f5000b3785 Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Thu, 27 Nov 2025 09:49:22 +0100 Subject: [PATCH 01/15] Spring 7 and Jackson 3 update --- geowebcache/core/pom.xml | 6 ++++-- .../org/geowebcache/config/XMLFileResourceProvider.java | 2 +- .../java/org/geowebcache/mbtiles/layer/MBTilesInfo.java | 8 ++++---- geowebcache/pom.xml | 6 +++--- .../rest/statistics/MemoryCacheControllerTest.java | 4 +--- .../java/org/geowebcache/service/wmts/WMTSTileJSON.java | 8 +++++--- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/geowebcache/core/pom.xml b/geowebcache/core/pom.xml index f846c2357..834e59324 100644 --- a/geowebcache/core/pom.xml +++ b/geowebcache/core/pom.xml @@ -145,16 +145,18 @@ - com.fasterxml.jackson.core + tools.jackson.core jackson-databind + 3.0.2 com.fasterxml.jackson.core jackson-annotations - com.fasterxml.jackson.core + tools.jackson.core jackson-core + 3.0.2 org.apache.httpcomponents.client5 diff --git a/geowebcache/core/src/main/java/org/geowebcache/config/XMLFileResourceProvider.java b/geowebcache/core/src/main/java/org/geowebcache/config/XMLFileResourceProvider.java index 2028ea951..03a0fbac2 100644 --- a/geowebcache/core/src/main/java/org/geowebcache/config/XMLFileResourceProvider.java +++ b/geowebcache/core/src/main/java/org/geowebcache/config/XMLFileResourceProvider.java @@ -32,8 +32,8 @@ import org.geowebcache.storage.DefaultStorageFinder; import org.geowebcache.util.ApplicationContextProvider; import org.geowebcache.util.GWCVars; +import org.jspecify.annotations.NonNull; import org.springframework.context.ApplicationContext; -import org.springframework.lang.NonNull; import org.springframework.web.context.WebApplicationContext; /** Default implementation of ConfigurationResourceProvider that uses the file system. */ diff --git a/geowebcache/mbtiles/src/main/java/org/geowebcache/mbtiles/layer/MBTilesInfo.java b/geowebcache/mbtiles/src/main/java/org/geowebcache/mbtiles/layer/MBTilesInfo.java index daedec129..c1a54326b 100644 --- a/geowebcache/mbtiles/src/main/java/org/geowebcache/mbtiles/layer/MBTilesInfo.java +++ b/geowebcache/mbtiles/src/main/java/org/geowebcache/mbtiles/layer/MBTilesInfo.java @@ -16,9 +16,6 @@ import static org.geotools.mbtiles.MBTilesFile.SPHERICAL_MERCATOR; import static org.geotools.mbtiles.MBTilesFile.WORLD_ENVELOPE; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.util.List; import java.util.logging.Level; @@ -35,6 +32,9 @@ import org.geowebcache.grid.BoundingBox; import org.geowebcache.layer.meta.TileJSON; import org.geowebcache.layer.meta.VectorLayerMetadata; +import tools.jackson.core.JacksonException; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; /** Info Object storing basic MBTiles Cached info */ public class MBTilesInfo { @@ -151,7 +151,7 @@ public void decorateTileJSON(TileJSON tileJSON) { List layers = null; try { layers = mapper.readValue(json, new TypeReference<>() {}); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { throw new IllegalArgumentException("Exception occurred while parsing the layers metadata. " + e); } tileJSON.setLayers(layers); diff --git a/geowebcache/pom.xml b/geowebcache/pom.xml index a4239a8bf..acd7f8236 100644 --- a/geowebcache/pom.xml +++ b/geowebcache/pom.xml @@ -53,7 +53,7 @@ 35-SNAPSHOT - 6.2.12 + 7.0.1 6.5.5 1.4.21 1.18.0 @@ -336,7 +336,7 @@ maven-failsafe-plugin - 3.5.3 + 3.5.4 @@ -387,7 +387,7 @@ maven-surefire-plugin - 3.5.3 + 3.5.4 diff --git a/geowebcache/rest/src/test/java/org/geowebcache/rest/statistics/MemoryCacheControllerTest.java b/geowebcache/rest/src/test/java/org/geowebcache/rest/statistics/MemoryCacheControllerTest.java index 6d7f70820..455bfd7e8 100644 --- a/geowebcache/rest/src/test/java/org/geowebcache/rest/statistics/MemoryCacheControllerTest.java +++ b/geowebcache/rest/src/test/java/org/geowebcache/rest/statistics/MemoryCacheControllerTest.java @@ -54,9 +54,7 @@ public void setup() throws GeoWebCacheException { xmlConfig.afterPropertiesSet(); mcc = new MemoryCacheController(null); - this.mockMvc = MockMvcBuilders.standaloneSetup(mcc) - .setUseSuffixPatternMatch(true) - .build(); + this.mockMvc = MockMvcBuilders.standaloneSetup(mcc).build(); } @Test diff --git a/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSTileJSON.java b/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSTileJSON.java index b60b2c4f0..38121f9a3 100644 --- a/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSTileJSON.java +++ b/geowebcache/wmts/src/main/java/org/geowebcache/service/wmts/WMTSTileJSON.java @@ -14,7 +14,6 @@ package org.geowebcache.service.wmts; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; @@ -34,6 +33,8 @@ import org.geowebcache.mime.ApplicationMime; import org.geowebcache.mime.MimeType; import org.geowebcache.util.URLMangler; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; public class WMTSTileJSON { @@ -76,8 +77,9 @@ public void writeResponse(TileLayer layer) { @SuppressWarnings("PMD.CloseResource") // managed by servlet container OutputStream os = convTile.servletResp.getOutputStream(); - ObjectMapper mapper = new ObjectMapper(); - mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + ObjectMapper mapper = JsonMapper.builder() + .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.NON_NULL)) + .build(); mapper.writeValue(os, json); os.flush(); } catch (IOException ioe) { From 276941fac250aaa2eca6f6b9f02906ad4392e7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Thu, 22 Jan 2026 15:18:20 +0100 Subject: [PATCH 02/15] Spring 7: remove suffix pattern for content negociation --- geowebcache/rest/pom.xml | 4 ---- .../src/main/webapp/WEB-INF/geowebcache-rest-context.xml | 9 +++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/geowebcache/rest/pom.xml b/geowebcache/rest/pom.xml index ebe42681c..59da37122 100644 --- a/geowebcache/rest/pom.xml +++ b/geowebcache/rest/pom.xml @@ -29,10 +29,6 @@ com.google.guava guava - - jakarta.servlet - jakarta.servlet-api - diff --git a/geowebcache/web/src/main/webapp/WEB-INF/geowebcache-rest-context.xml b/geowebcache/web/src/main/webapp/WEB-INF/geowebcache-rest-context.xml index 697e6298d..650ac7fad 100644 --- a/geowebcache/web/src/main/webapp/WEB-INF/geowebcache-rest-context.xml +++ b/geowebcache/web/src/main/webapp/WEB-INF/geowebcache-rest-context.xml @@ -10,12 +10,13 @@ - - - + + + + - + From 2a2062b9175a489e26cc86df50b4b77c6523bf87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Thu, 22 Jan 2026 15:36:23 +0100 Subject: [PATCH 03/15] Temporary override jackson-annotations 2.19.0 from GeoTools with 2.20 --- geowebcache/pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/geowebcache/pom.xml b/geowebcache/pom.xml index acd7f8236..29b1c443c 100644 --- a/geowebcache/pom.xml +++ b/geowebcache/pom.xml @@ -255,6 +255,13 @@ ${joda-time.version} + + + com.fasterxml.jackson.core + jackson-annotations + 2.20 + + From 214f5aefbb7d3eabd352132528f9e0356647010d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Thu, 22 Jan 2026 16:15:27 +0100 Subject: [PATCH 04/15] Jackson 3 is stricter regarding trailing commas --- .../mbtiles/layer/MBTilesInfo.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/geowebcache/mbtiles/src/main/java/org/geowebcache/mbtiles/layer/MBTilesInfo.java b/geowebcache/mbtiles/src/main/java/org/geowebcache/mbtiles/layer/MBTilesInfo.java index c1a54326b..02aaf6298 100644 --- a/geowebcache/mbtiles/src/main/java/org/geowebcache/mbtiles/layer/MBTilesInfo.java +++ b/geowebcache/mbtiles/src/main/java/org/geowebcache/mbtiles/layer/MBTilesInfo.java @@ -146,15 +146,20 @@ public void decorateTileJSON(TileJSON tileJSON) { int index = -1; if (json != null && ((index = json.indexOf("[")) > 0)) { // skip the "vector_layers initial part and go straight to the array - json = json.substring(index, json.length() - 1).trim(); - ObjectMapper mapper = new ObjectMapper(); - List layers = null; - try { - layers = mapper.readValue(json, new TypeReference<>() {}); - } catch (JacksonException e) { - throw new IllegalArgumentException("Exception occurred while parsing the layers metadata. " + e); + // Find the closing bracket for the array + int endIndex = json.indexOf("]", index); + if (endIndex > 0) { + json = json.substring(index, endIndex + 1).trim(); + ObjectMapper mapper = new ObjectMapper(); + List layers = null; + try { + layers = mapper.readValue(json, new TypeReference<>() {}); + } catch (JacksonException e) { + throw new IllegalArgumentException( + "Exception occurred while parsing the layers metadata. " + e); + } + tileJSON.setLayers(layers); } - tileJSON.setLayers(layers); } } } From 2932fa2da72c76753d607fe382d4273f23f27286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Fri, 23 Jan 2026 13:14:35 +0100 Subject: [PATCH 05/15] Path suffix content negotiation and mapping --- .../rest/filter/SuffixStripFilter.java | 87 +++++++++++++++++++ .../SuffixContentNegotiationStrategy.java | 49 +++++++++++ .../WEB-INF/geowebcache-rest-context.xml | 20 ++++- .../web/src/main/webapp/WEB-INF/web.xml | 10 +++ 4 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 geowebcache/rest/src/main/java/org/geowebcache/rest/filter/SuffixStripFilter.java create mode 100644 geowebcache/rest/src/main/java/org/geowebcache/rest/negotiation/SuffixContentNegotiationStrategy.java diff --git a/geowebcache/rest/src/main/java/org/geowebcache/rest/filter/SuffixStripFilter.java b/geowebcache/rest/src/main/java/org/geowebcache/rest/filter/SuffixStripFilter.java new file mode 100644 index 000000000..b7699c67c --- /dev/null +++ b/geowebcache/rest/src/main/java/org/geowebcache/rest/filter/SuffixStripFilter.java @@ -0,0 +1,87 @@ +/** + * 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 Cécile Vuilleumier, Camptocamp, Copyright 2026 + */ +package org.geowebcache.rest.filter; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Servlet filter for GeoWebCache + * + *

Extracts the path suffix (extension) and stores it for content negotiation. Removes the extension from the path + * for path mapping. + */ +public class SuffixStripFilter implements Filter { + + private static final Pattern EXTENSION_PATTERN = Pattern.compile("^(.*?)\\.(json|xml)$"); + + public static final String FORMAT_ATTRIBUTE = "gwc.formatExtension"; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + if (request instanceof HttpServletRequest httpRequest) { + String requestURI = httpRequest.getRequestURI(); + Matcher matcher = EXTENSION_PATTERN.matcher(requestURI); + + if (matcher.matches()) { + String pathWithoutExtension = matcher.group(1); + String extension = matcher.group(2); + + // Wrap the request to return modified paths + HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(httpRequest) { + @Override + public String getRequestURI() { + return pathWithoutExtension; + } + + @Override + public StringBuffer getRequestURL() { + StringBuffer url = + new StringBuffer(super.getRequestURL().toString()); + int extIndex = url.lastIndexOf("." + extension); + if (extIndex > 0) { + url.delete(extIndex, url.length()); + } + return url; + } + + @Override + public String getServletPath() { + String servletPath = super.getServletPath(); + return servletPath.replaceFirst("\\." + extension + "$", ""); + } + }; + + // Store extension for content negotiation + wrapper.setAttribute(FORMAT_ATTRIBUTE, extension); + + chain.doFilter(wrapper, response); + return; + } + } + + // No extension found, pass through unchanged + chain.doFilter(request, response); + } +} diff --git a/geowebcache/rest/src/main/java/org/geowebcache/rest/negotiation/SuffixContentNegotiationStrategy.java b/geowebcache/rest/src/main/java/org/geowebcache/rest/negotiation/SuffixContentNegotiationStrategy.java new file mode 100644 index 000000000..cff9ae4fc --- /dev/null +++ b/geowebcache/rest/src/main/java/org/geowebcache/rest/negotiation/SuffixContentNegotiationStrategy.java @@ -0,0 +1,49 @@ +/** + * 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 Cécile Vuilleumier, Camptocamp, Copyright 2026 + */ +package org.geowebcache.rest.negotiation; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.List; +import org.springframework.http.MediaType; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * Spring ContentNegotiationStrategy for GeoWebCache + * + *

Reads the media type stored by {@link org.geowebcache.rest.filter.SuffixStripFilter} + */ +public class SuffixContentNegotiationStrategy implements ContentNegotiationStrategy { + + public static final String FORMAT_ATTRIBUTE = "gwc.formatExtension"; + + @Override + public List resolveMediaTypes(NativeWebRequest request) { + HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); + if (servletRequest != null) { + // Check if filter stored the extension + String extension = (String) servletRequest.getAttribute(FORMAT_ATTRIBUTE); + + if (extension != null) { + if ("json".equals(extension)) { + return Collections.singletonList(MediaType.APPLICATION_JSON); + } else if ("xml".equals(extension)) { + return Collections.singletonList(MediaType.APPLICATION_XML); + } + } + } + return Collections.emptyList(); + } +} diff --git a/geowebcache/web/src/main/webapp/WEB-INF/geowebcache-rest-context.xml b/geowebcache/web/src/main/webapp/WEB-INF/geowebcache-rest-context.xml index 650ac7fad..a78517172 100644 --- a/geowebcache/web/src/main/webapp/WEB-INF/geowebcache-rest-context.xml +++ b/geowebcache/web/src/main/webapp/WEB-INF/geowebcache-rest-context.xml @@ -10,10 +10,22 @@ - - - - + + + + + + + + + + + + + + + + diff --git a/geowebcache/web/src/main/webapp/WEB-INF/web.xml b/geowebcache/web/src/main/webapp/WEB-INF/web.xml index c880dbd84..14292f79f 100644 --- a/geowebcache/web/src/main/webapp/WEB-INF/web.xml +++ b/geowebcache/web/src/main/webapp/WEB-INF/web.xml @@ -26,6 +26,16 @@ /* + + + pathSuffixStripFilter + org.geowebcache.rest.filter.SuffixStripFilter + + + pathSuffixStripFilter + /rest/* + + org.geowebcache.util.LoggingContextListener From 02527ca93c57b6155a2ad72de6dec62214137e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Mon, 26 Jan 2026 15:02:08 +0100 Subject: [PATCH 06/15] Add path mapping for /seed/{layer} without extension --- .../rest/controller/SeedController.java | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/geowebcache/rest/src/main/java/org/geowebcache/rest/controller/SeedController.java b/geowebcache/rest/src/main/java/org/geowebcache/rest/controller/SeedController.java index 9bd49c2dd..2fa0eea2d 100644 --- a/geowebcache/rest/src/main/java/org/geowebcache/rest/controller/SeedController.java +++ b/geowebcache/rest/src/main/java/org/geowebcache/rest/controller/SeedController.java @@ -106,11 +106,14 @@ public ResponseEntity doPost(HttpServletRequest request) { } /** - * POST method for Seeding and Truncating + * POST method for Seeding and Truncating via form submission * * @param params Query parameters, including urlencoded form values */ - @RequestMapping(value = "/seed/{layer:.+}", method = RequestMethod.POST) + @RequestMapping( + value = "/seed/{layer:.+}", + method = RequestMethod.POST, + consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.ALL_VALUE}) public ResponseEntity doPost( HttpServletRequest request, InputStream inputStream, @@ -133,7 +136,11 @@ public ResponseEntity doPost( } } - @RequestMapping(value = "/seed/{layer}.json", method = RequestMethod.POST) + /** POST method for JSON seeding/truncating with path extension. */ + @RequestMapping( + value = "/seed/{layer}.json", + method = RequestMethod.POST, + consumes = {MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity seedOrTruncateWithJsonPayload( HttpServletRequest request, InputStream inputStream, @PathVariable(name = "layer") String layerName) { @@ -142,7 +149,11 @@ public ResponseEntity seedOrTruncateWithJsonPayload( return seedService.doSeeding(request, layerName, extension, body); } - @RequestMapping(value = "/seed/{layer}.xml", method = RequestMethod.POST) + /** POST method for XML seeding/truncating without path extension. */ + @RequestMapping( + value = "/seed/{layer}.xml", + method = RequestMethod.POST, + consumes = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE}) public ResponseEntity seedOrTruncateWithXmlPayload( HttpServletRequest request, InputStream inputStream, @PathVariable(name = "layer") String layerName) { @@ -151,6 +162,32 @@ public ResponseEntity seedOrTruncateWithXmlPayload( return seedService.doSeeding(request, layerName, extension, body); } + /** POST method for JSON seeding/truncating without path extension. */ + @RequestMapping( + value = "/seed/{layer:[^.]+}", + method = RequestMethod.POST, + consumes = {MediaType.APPLICATION_JSON_VALUE}) + public ResponseEntity seedOrTruncateJson( + HttpServletRequest request, InputStream inputStream, @PathVariable(name = "layer") String layerName) { + + String body = readBody(inputStream); + String extension = "json"; + return seedService.doSeeding(request, layerName, extension, body); + } + + /** POST method for XML seeding/truncating without path extension. */ + @RequestMapping( + value = "/seed/{layer:[^.]+}", + method = RequestMethod.POST, + consumes = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE}) + public ResponseEntity seedOrTruncateXml( + HttpServletRequest request, InputStream inputStream, @PathVariable(name = "layer") String layerName) { + + String body = readBody(inputStream); + String extension = "xml"; + return seedService.doSeeding(request, layerName, extension, body); + } + private String readBody(InputStream inputStream) { return new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.joining("\n")); } From ab85207e58b1849f1c4e7ac541a4d70ff98248ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Tue, 27 Jan 2026 13:38:38 +0100 Subject: [PATCH 07/15] Fix content negotiation based on header --- .../SuffixContentNegotiationStrategy.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/geowebcache/rest/src/main/java/org/geowebcache/rest/negotiation/SuffixContentNegotiationStrategy.java b/geowebcache/rest/src/main/java/org/geowebcache/rest/negotiation/SuffixContentNegotiationStrategy.java index cff9ae4fc..1e41b5fa1 100644 --- a/geowebcache/rest/src/main/java/org/geowebcache/rest/negotiation/SuffixContentNegotiationStrategy.java +++ b/geowebcache/rest/src/main/java/org/geowebcache/rest/negotiation/SuffixContentNegotiationStrategy.java @@ -27,23 +27,26 @@ */ public class SuffixContentNegotiationStrategy implements ContentNegotiationStrategy { + List MEDIA_TYPE_ALL_LIST = Collections.singletonList(MediaType.ALL); + public static final String FORMAT_ATTRIBUTE = "gwc.formatExtension"; @Override public List resolveMediaTypes(NativeWebRequest request) { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); - if (servletRequest != null) { - // Check if filter stored the extension - String extension = (String) servletRequest.getAttribute(FORMAT_ATTRIBUTE); + if (servletRequest == null) { + return MEDIA_TYPE_ALL_LIST; + } + // Check if filter stored the extension + String extension = (String) servletRequest.getAttribute(FORMAT_ATTRIBUTE); - if (extension != null) { - if ("json".equals(extension)) { - return Collections.singletonList(MediaType.APPLICATION_JSON); - } else if ("xml".equals(extension)) { - return Collections.singletonList(MediaType.APPLICATION_XML); - } + if (extension != null) { + if ("json".equals(extension)) { + return Collections.singletonList(MediaType.APPLICATION_JSON); + } else if ("xml".equals(extension)) { + return Collections.singletonList(MediaType.APPLICATION_XML); } } - return Collections.emptyList(); + return MEDIA_TYPE_ALL_LIST; } } From 74ee0e1e1471da93931021ff50706d00aa48842f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Tue, 27 Jan 2026 14:35:55 +0100 Subject: [PATCH 08/15] XML parser is less lenient Use accurate content type header --- .../geowebcache/jetty/RestIntegrationTest.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/geowebcache/web/src/test/java/org/geowebcache/jetty/RestIntegrationTest.java b/geowebcache/web/src/test/java/org/geowebcache/jetty/RestIntegrationTest.java index 568891b3c..db0f8cd2f 100644 --- a/geowebcache/web/src/test/java/org/geowebcache/jetty/RestIntegrationTest.java +++ b/geowebcache/web/src/test/java/org/geowebcache/jetty/RestIntegrationTest.java @@ -1086,8 +1086,11 @@ public void testKillAll() throws Exception { @Test public void testLayerKillAll() throws Exception { String killCommand = "kill_all=all"; - try (ClassicHttpResponse response = - handlePost(URI.create("/geowebcache/rest/seed/states"), admin.getClient(), killCommand)) { + try (ClassicHttpResponse response = handlePost( + URI.create("/geowebcache/rest/seed/states"), + admin.getClient(), + killCommand, + ContentType.APPLICATION_FORM_URLENCODED)) { assertEquals(200, response.getCode()); } } @@ -1266,13 +1269,18 @@ private ClassicHttpResponse handlePut(URI uri, CloseableHttpClient client, Strin } @SuppressWarnings("PMD.CloseResource") - private ClassicHttpResponse handlePost(URI uri, CloseableHttpClient client, String data) throws Exception { + private ClassicHttpResponse handlePost(URI uri, CloseableHttpClient client, String data, ContentType contentType) + throws Exception { HttpPost request = new HttpPost(jetty.getUri().resolve(uri)); - StringEntity entity = new StringEntity(data, ContentType.TEXT_XML); + StringEntity entity = new StringEntity(data, contentType); request.setEntity(entity); return client.executeOpen(determineHost(request), request, null); } + private ClassicHttpResponse handlePost(URI uri, CloseableHttpClient client, String data) throws Exception { + return handlePost(uri, client, data, ContentType.TEXT_XML); + } + private static class StatusCheckHandler implements HttpClientResponseHandler { private final int expectedStatus; From 8614f6ba5116666a7a051af54e61771ba556b95b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Tue, 27 Jan 2026 15:41:19 +0100 Subject: [PATCH 09/15] Add path mapping for /seed without extension --- .../geowebcache/rest/controller/SeedController.java | 13 +++++++++++-- .../org/geowebcache/jetty/RestIntegrationTest.java | 12 ++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/geowebcache/rest/src/main/java/org/geowebcache/rest/controller/SeedController.java b/geowebcache/rest/src/main/java/org/geowebcache/rest/controller/SeedController.java index 2fa0eea2d..d1458453d 100644 --- a/geowebcache/rest/src/main/java/org/geowebcache/rest/controller/SeedController.java +++ b/geowebcache/rest/src/main/java/org/geowebcache/rest/controller/SeedController.java @@ -62,13 +62,22 @@ public class SeedController { /** GET method for querying running GWC tasks */ @RequestMapping( - value = "/seed.json", + value = "/seed", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity doGet(HttpServletRequest req) { return seedService.getRunningTasks(req); } + /** GET method for querying running GWC tasks with path extension */ + @RequestMapping( + value = "/seed.json", + method = RequestMethod.GET, + produces = {MediaType.APPLICATION_JSON_VALUE}) + public ResponseEntity doGetJson(HttpServletRequest req) { + return seedService.getRunningTasks(req); + } + /** GET method for querying running tasks for the provided layer */ @RequestMapping( value = "/seed/{layer:.+}.json", @@ -149,7 +158,7 @@ public ResponseEntity seedOrTruncateWithJsonPayload( return seedService.doSeeding(request, layerName, extension, body); } - /** POST method for XML seeding/truncating without path extension. */ + /** POST method for XML seeding/truncating with path extension. */ @RequestMapping( value = "/seed/{layer}.xml", method = RequestMethod.POST, diff --git a/geowebcache/web/src/test/java/org/geowebcache/jetty/RestIntegrationTest.java b/geowebcache/web/src/test/java/org/geowebcache/jetty/RestIntegrationTest.java index db0f8cd2f..18ecb257c 100644 --- a/geowebcache/web/src/test/java/org/geowebcache/jetty/RestIntegrationTest.java +++ b/geowebcache/web/src/test/java/org/geowebcache/jetty/RestIntegrationTest.java @@ -1031,22 +1031,22 @@ public void testSeedPost() throws Exception { } @Test - public void testSeedGet() throws Exception { + public void testSeedGetLayer() throws Exception { try (ClassicHttpResponse response = handleGet(URI.create("/geowebcache/rest/seed/states"), admin.getClient())) { assertEquals(200, response.getCode()); } } @Test - public void testSeedGetNoLayer() throws Exception { - try (ClassicHttpResponse response = handleGet(URI.create("/geowebcache/rest/seed"), admin.getClient())) { - assertEquals(405, response.getCode()); + public void testSeedGetSeedForm() throws Exception { + try (ClassicHttpResponse response = handleGet(URI.create("/geowebcache/rest/seed/states"), admin.getClient())) { + assertEquals(200, response.getCode()); } } @Test - public void testSeedGetSeedForm() throws Exception { - try (ClassicHttpResponse response = handleGet(URI.create("/geowebcache/rest/seed/states"), admin.getClient())) { + public void testSeedGet() throws Exception { + try (ClassicHttpResponse response = handleGet(URI.create("/geowebcache/rest/seed"), admin.getClient())) { assertEquals(200, response.getCode()); } } From d5b2338e5e5a32f5833d3dd237dcf1684bf31c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Wed, 28 Jan 2026 08:13:52 +0100 Subject: [PATCH 10/15] Stick to Junit4 for now --- .../java/org/geowebcache/rest/bounds/BoundsControllerTest.java | 1 + .../org/geowebcache/rest/controller/BlobStoreControllerTest.java | 1 + .../org/geowebcache/rest/controller/BoundsControllerMVCTest.java | 1 + .../geowebcache/rest/controller/ByteStreamControllerTest.java | 1 + .../rest/controller/FilterUpdateControllerMVCTest.java | 1 + .../org/geowebcache/rest/controller/GridSetControllerTest.java | 1 + .../java/org/geowebcache/rest/controller/SeedControllerTest.java | 1 + .../org/geowebcache/rest/controller/TileLayerControllerTest.java | 1 + .../src/test/java/org/geowebcache/rest/reload/ReloadTest.java | 1 + .../org/geowebcache/rest/seed/MassTruncateControllerTest.java | 1 + .../geowebcache/rest/statistics/MemoryCacheControllerTest.java | 1 + .../src/test/java/org/geowebcache/sqlite/OperationsRestTest.java | 1 + 12 files changed, 12 insertions(+) diff --git a/geowebcache/rest/src/test/java/org/geowebcache/rest/bounds/BoundsControllerTest.java b/geowebcache/rest/src/test/java/org/geowebcache/rest/bounds/BoundsControllerTest.java index 783bcf919..a7bdf1829 100644 --- a/geowebcache/rest/src/test/java/org/geowebcache/rest/bounds/BoundsControllerTest.java +++ b/geowebcache/rest/src/test/java/org/geowebcache/rest/bounds/BoundsControllerTest.java @@ -38,6 +38,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +@SuppressWarnings("deprecation") @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"file*:/webapp/WEB-INF/web.xml", "file*:/webapp/WEB-INF/geowebcache-servlet.xml"}) public class BoundsControllerTest { diff --git a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/BlobStoreControllerTest.java b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/BlobStoreControllerTest.java index 0f7533f3f..bee4cc625 100644 --- a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/BlobStoreControllerTest.java +++ b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/BlobStoreControllerTest.java @@ -29,6 +29,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +@SuppressWarnings("deprecation") @RunWith(SpringRunner.class) @WebAppConfiguration() @ContextConfiguration({ diff --git a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/BoundsControllerMVCTest.java b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/BoundsControllerMVCTest.java index 2b8ca257a..81e7623f6 100644 --- a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/BoundsControllerMVCTest.java +++ b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/BoundsControllerMVCTest.java @@ -29,6 +29,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +@SuppressWarnings("deprecation") @RunWith(SpringRunner.class) @WebAppConfiguration() @ContextConfiguration({ diff --git a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/ByteStreamControllerTest.java b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/ByteStreamControllerTest.java index 58ee13b9e..d571e80dd 100644 --- a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/ByteStreamControllerTest.java +++ b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/ByteStreamControllerTest.java @@ -28,6 +28,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +@SuppressWarnings("deprecation") @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"file*:/webapp/WEB-INF/web.xml", "file*:/webapp/WEB-INF/geowebcache-servlet.xml"}) public class ByteStreamControllerTest { diff --git a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/FilterUpdateControllerMVCTest.java b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/FilterUpdateControllerMVCTest.java index 5ba3588d2..0c0828803 100644 --- a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/FilterUpdateControllerMVCTest.java +++ b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/FilterUpdateControllerMVCTest.java @@ -29,6 +29,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +@SuppressWarnings("deprecation") @RunWith(SpringRunner.class) @WebAppConfiguration() @ContextConfiguration({ diff --git a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/GridSetControllerTest.java b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/GridSetControllerTest.java index 62ea0deb8..558042361 100644 --- a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/GridSetControllerTest.java +++ b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/GridSetControllerTest.java @@ -29,6 +29,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +@SuppressWarnings("deprecation") @RunWith(SpringRunner.class) @WebAppConfiguration() @ContextConfiguration({ diff --git a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/SeedControllerTest.java b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/SeedControllerTest.java index ebab1b405..f6b882ad0 100644 --- a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/SeedControllerTest.java +++ b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/SeedControllerTest.java @@ -46,6 +46,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +@SuppressWarnings("deprecation") @RunWith(SpringRunner.class) @WebAppConfiguration() @ContextConfiguration({ diff --git a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/TileLayerControllerTest.java b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/TileLayerControllerTest.java index 2e3da1d40..5b0a75b82 100644 --- a/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/TileLayerControllerTest.java +++ b/geowebcache/rest/src/test/java/org/geowebcache/rest/controller/TileLayerControllerTest.java @@ -29,6 +29,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +@SuppressWarnings("deprecation") @RunWith(SpringRunner.class) @WebAppConfiguration() @ContextConfiguration({ diff --git a/geowebcache/rest/src/test/java/org/geowebcache/rest/reload/ReloadTest.java b/geowebcache/rest/src/test/java/org/geowebcache/rest/reload/ReloadTest.java index f0060aa48..8cc47e3da 100644 --- a/geowebcache/rest/src/test/java/org/geowebcache/rest/reload/ReloadTest.java +++ b/geowebcache/rest/src/test/java/org/geowebcache/rest/reload/ReloadTest.java @@ -34,6 +34,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +@SuppressWarnings("deprecation") @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"file*:/webapp/WEB-INF/web.xml", "file*:/webapp/WEB-INF/geowebcache-servlet.xml"}) public class ReloadTest { diff --git a/geowebcache/rest/src/test/java/org/geowebcache/rest/seed/MassTruncateControllerTest.java b/geowebcache/rest/src/test/java/org/geowebcache/rest/seed/MassTruncateControllerTest.java index 1083687e7..e11707b8c 100644 --- a/geowebcache/rest/src/test/java/org/geowebcache/rest/seed/MassTruncateControllerTest.java +++ b/geowebcache/rest/src/test/java/org/geowebcache/rest/seed/MassTruncateControllerTest.java @@ -50,6 +50,7 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +@SuppressWarnings("deprecation") @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"file*:/webapp/WEB-INF/web.xml", "file*:/webapp/WEB-INF/geowebcache-servlet.xml"}) public class MassTruncateControllerTest { diff --git a/geowebcache/rest/src/test/java/org/geowebcache/rest/statistics/MemoryCacheControllerTest.java b/geowebcache/rest/src/test/java/org/geowebcache/rest/statistics/MemoryCacheControllerTest.java index 455bfd7e8..bf13367d4 100644 --- a/geowebcache/rest/src/test/java/org/geowebcache/rest/statistics/MemoryCacheControllerTest.java +++ b/geowebcache/rest/src/test/java/org/geowebcache/rest/statistics/MemoryCacheControllerTest.java @@ -37,6 +37,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +@SuppressWarnings("deprecation") @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"file*:/webapp/WEB-INF/web.xml", "file*:/webapp/WEB-INF/geowebcache-servlet.xml"}) public class MemoryCacheControllerTest { diff --git a/geowebcache/sqlite/src/test/java/org/geowebcache/sqlite/OperationsRestTest.java b/geowebcache/sqlite/src/test/java/org/geowebcache/sqlite/OperationsRestTest.java index be4ad7ae0..140e954e4 100644 --- a/geowebcache/sqlite/src/test/java/org/geowebcache/sqlite/OperationsRestTest.java +++ b/geowebcache/sqlite/src/test/java/org/geowebcache/sqlite/OperationsRestTest.java @@ -55,6 +55,7 @@ @WebAppConfiguration @ContextConfiguration(classes = OperationsRestWebConfig.class) +@SuppressWarnings("deprecation") @RunWith(SpringJUnit4ClassRunner.class) @ActiveProfiles("test") public class OperationsRestTest extends TestSupport { From d5343b9390cf659bc1be79dcdb336930055acb71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Wed, 28 Jan 2026 11:44:01 +0100 Subject: [PATCH 11/15] TransactionCallbackWithoutResult is deprecated in Spring 7 --- .../diskquota/jdbc/JDBCQuotaStore.java | 380 ++++++++---------- 1 file changed, 172 insertions(+), 208 deletions(-) diff --git a/geowebcache/diskquota/jdbc/src/main/java/org/geowebcache/diskquota/jdbc/JDBCQuotaStore.java b/geowebcache/diskquota/jdbc/src/main/java/org/geowebcache/diskquota/jdbc/JDBCQuotaStore.java index 7d5054cb0..bcd9cee3c 100644 --- a/geowebcache/diskquota/jdbc/src/main/java/org/geowebcache/diskquota/jdbc/JDBCQuotaStore.java +++ b/geowebcache/diskquota/jdbc/src/main/java/org/geowebcache/diskquota/jdbc/JDBCQuotaStore.java @@ -51,7 +51,6 @@ import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; /** @@ -134,37 +133,33 @@ public void initialize() { throw new IllegalStateException( "Please provide both the sql dialect and the data " + "source before calling inizialize"); } - tt.execute(new TransactionCallbackWithoutResult() { + tt.executeWithoutResult(status -> { - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - // setup the tables if necessary - dialect.initializeTables(schema, jt); + // setup the tables if necessary + dialect.initializeTables(schema, jt); - // get the existing table names - List existingLayers = - jt.query(dialect.getAllLayersQuery(schema), (rs, rowNum) -> rs.getString(1)); + // get the existing table names + List existingLayers = jt.query(dialect.getAllLayersQuery(schema), (rs, rowNum) -> rs.getString(1)); - // compare with the ones available in the config - final Set layerNames = calculator.getLayerNames(); - final Set layersToDelete = new HashSet<>(existingLayers); - layersToDelete.removeAll(layerNames); + // compare with the ones available in the config + final Set layerNames = calculator.getLayerNames(); + final Set layersToDelete = new HashSet<>(existingLayers); + layersToDelete.removeAll(layerNames); - // remove all the layers we don't need - for (String layerName : layersToDelete) { - deleteLayer(layerName); - } + // remove all the layers we don't need + for (String layerName : layersToDelete) { + deleteLayer(layerName); + } - // add any missing tileset - for (String layerName : layerNames) { - createLayerInternal(layerName); - } + // add any missing tileset + for (String layerName : layerNames) { + createLayerInternal(layerName); + } - // create the global quota if necessary - Quota global = getUsedQuotaByTileSetIdInternal(GLOBAL_QUOTA_NAME); - if (global == null) { - createLayerInternal(GLOBAL_QUOTA_NAME); - } + // create the global quota if necessary + Quota global = getUsedQuotaByTileSetIdInternal(GLOBAL_QUOTA_NAME); + if (global == null) { + createLayerInternal(GLOBAL_QUOTA_NAME); } }); } @@ -175,21 +170,17 @@ public void createLayer(String layerName) throws InterruptedException { } private void createLayerInternal(final String layerName) { - tt.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - Set layerTileSets; - if (!GLOBAL_QUOTA_NAME.equals(layerName)) { - layerTileSets = calculator.getTileSetsFor(layerName); - } else { - layerTileSets = Collections.singleton(new TileSet(GLOBAL_QUOTA_NAME)); - } - for (TileSet tset : layerTileSets) { - // other nodes in the cluster might be trying to create the same layer, - // so use getOrCreate - getOrCreateTileSet(tset); - } + tt.executeWithoutResult(status -> { + Set layerTileSets; + if (!GLOBAL_QUOTA_NAME.equals(layerName)) { + layerTileSets = calculator.getTileSetsFor(layerName); + } else { + layerTileSets = Collections.singleton(new TileSet(GLOBAL_QUOTA_NAME)); + } + for (TileSet tset : layerTileSets) { + // other nodes in the cluster might be trying to create the same layer, + // so use getOrCreate + getOrCreateTileSet(tset); } }); } @@ -259,77 +250,61 @@ private Quota nonNullQuota(Quota optionalQuota) { @Override public void deleteLayer(final String layerName) { - tt.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - deleteLayerInternal(layerName); - } + tt.executeWithoutResult(status -> { + deleteLayerInternal(layerName); }); } @Override public void deleteGridSubset(final String layerName, final String gridSetId) { - tt.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - // get the disk quota used by the layer gridset - Quota quota = getUsedQuotaByLayerGridset(layerName, gridSetId); - // we will subtracting the current disk quota value - quota.setBytes(quota.getBytes().negate()); - // update the global disk quota by subtracting the value above - String updateQuota = dialect.getUpdateQuotaStatement(schema, "tileSetId", "bytes"); - Map params = new HashMap<>(); - params.put("tileSetId", GLOBAL_QUOTA_NAME); - params.put("bytes", new BigDecimal(quota.getBytes())); - jt.update(updateQuota, params); - // delete layer gridset - String statement = dialect.getLayerGridDeletionStatement(schema, "layerName", "gridSetId"); - params = new HashMap<>(); - params.put("layerName", layerName); - params.put("gridSetId", gridSetId); - jt.update(statement, params); - } + tt.executeWithoutResult(status -> { + // get the disk quota used by the layer gridset + Quota quota = getUsedQuotaByLayerGridset(layerName, gridSetId); + // we will subtracting the current disk quota value + quota.setBytes(quota.getBytes().negate()); + // update the global disk quota by subtracting the value above + String updateQuota = dialect.getUpdateQuotaStatement(schema, "tileSetId", "bytes"); + Map params = new HashMap<>(); + params.put("tileSetId", GLOBAL_QUOTA_NAME); + params.put("bytes", new BigDecimal(quota.getBytes())); + jt.update(updateQuota, params); + // delete layer gridset + String statement = dialect.getLayerGridDeletionStatement(schema, "layerName", "gridSetId"); + params = new HashMap<>(); + params.put("layerName", layerName); + params.put("gridSetId", gridSetId); + jt.update(statement, params); }); } public void deleteLayerInternal(final String layerName) { getUsedQuotaByLayerName(layerName); - tt.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus arg0) { - // update the global quota - Quota quota = getUsedQuotaByLayerName(layerName); - quota.setBytes(quota.getBytes().negate()); - String updateQuota = dialect.getUpdateQuotaStatement(schema, "tileSetId", "bytes"); - Map params = new HashMap<>(); - params.put("tileSetId", GLOBAL_QUOTA_NAME); - params.put("bytes", new BigDecimal(quota.getBytes())); - jt.update(updateQuota, params); - - // delete the layer - log.info("Deleting disk quota information for layer '" + layerName + "'"); - String statement = dialect.getLayerDeletionStatement(schema, "layerName"); - jt.update(statement, Collections.singletonMap("layerName", layerName)); - } + tt.executeWithoutResult(status -> { + // update the global quota + Quota quota = getUsedQuotaByLayerName(layerName); + quota.setBytes(quota.getBytes().negate()); + String updateQuota = dialect.getUpdateQuotaStatement(schema, "tileSetId", "bytes"); + Map params = new HashMap<>(); + params.put("tileSetId", GLOBAL_QUOTA_NAME); + params.put("bytes", new BigDecimal(quota.getBytes())); + jt.update(updateQuota, params); + + // delete the layer + log.info("Deleting disk quota information for layer '" + layerName + "'"); + String statement = dialect.getLayerDeletionStatement(schema, "layerName"); + jt.update(statement, Collections.singletonMap("layerName", layerName)); }); } @Override public void renameLayer(final String oldLayerName, final String newLayerName) throws InterruptedException { - tt.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - String sql = dialect.getRenameLayerStatement(schema, "oldName", "newName"); - Map params = new HashMap<>(); - params.put("oldName", oldLayerName); - params.put("newName", newLayerName); - int updated = jt.update(sql, params); - log.info("Updated " + updated + " tile sets after layer rename"); - } + tt.executeWithoutResult(status -> { + String sql = dialect.getRenameLayerStatement(schema, "oldName", "newName"); + Map params = new HashMap<>(); + params.put("oldName", oldLayerName); + params.put("newName", newLayerName); + int updated = jt.update(sql, params); + log.info("Updated " + updated + " tile sets after layer rename"); }); } @@ -428,97 +403,14 @@ public TilePageCalculator getTilePageCalculator() { public void addToQuotaAndTileCounts( final TileSet tileSet, final Quota quotaDiff, final Collection tileCountDiffs) throws InterruptedException { - tt.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - getOrCreateTileSet(tileSet); - updateQuotas(tileSet, quotaDiff); - - if (tileCountDiffs != null) { - // sort the payloads by page id as a deadlock avoidance measure, out - // of order updates may result in deadlock with the - // addHitsAndSetAccessTime method - List sorted = sortPayloads(tileCountDiffs); - for (PageStatsPayload payload : sorted) { - upsertTilePageFillFactor(payload); - } - } - } - - private void updateQuotas(final TileSet tileSet, final Quota quotaDiff) { - if (log.isLoggable(Level.FINE)) { - log.info("Applying quota diff " + quotaDiff.getBytes() + " on tileset " + tileSet); - } - - String updateQuota = dialect.getUpdateQuotaStatement(schema, "tileSetId", "bytes"); - Map params = new HashMap<>(); - params.put("tileSetId", tileSet.getId()); - params.put("bytes", new BigDecimal(quotaDiff.getBytes())); - jt.update(updateQuota, params); - params.put("tileSetId", GLOBAL_QUOTA_NAME); - jt.update(updateQuota, params); - } - - private void upsertTilePageFillFactor(PageStatsPayload payload) { - if (log.isLoggable(Level.FINE)) { - log.info("Applying page stats payload " + payload); - } - - // see http://en.wikipedia.org/wiki/Merge_(SQL) - // Even the Merge command that some databases support is prone to race - // conditions - // under concurrent load, but we don't want to lose data and it's difficult - // to - // tell apart the race conditions from other failures, so we use tolerant - // commands - // and loop over them. - // Loop conditions: we find the page stats, but they are deleted before we - // can - // update - // them, we don't find the page stats, but they are inserted before we can - // do so, in - // both cases we re-start from zero - TilePage page = payload.getPage(); - final byte level = page.getZoomLevel(); - final BigInteger tilesPerPage = calculator.getTilesPerPage(tileSet, level); - - int modified = 0; - int count = 0; - while (modified == 0 && count < maxLoops) { - try { - count++; - PageStats stats = getPageStats(page.getKey()); - if (stats != null) { - float oldFillFactor = stats.getFillFactor(); - stats.addTiles(payload.getNumTiles(), tilesPerPage); - // if no change, bail out early - if (oldFillFactor == stats.getFillFactor()) { - return; - } - - // update the record in the db - modified = updatePageFillFactor(page, stats, oldFillFactor); - } else { - // create the stats and update the fill factor - stats = new PageStats(0); - stats.addTiles(payload.getNumTiles(), tilesPerPage); - - modified = createNewPageStats(stats, page); - } - } catch (PessimisticLockingFailureException e) { - if (log.isLoggable(Level.FINE)) { - log.log(Level.FINE, "Deadlock while updating page stats, will retry", e); - } - } - } + tt.executeWithoutResult(status -> { + getOrCreateTileSet(tileSet); + updateQuotas(tileSet, quotaDiff); - if (modified == 0) { - throw new ConcurrencyFailureException("Failed to create or update page stats for page " - + payload.getPage() - + " after " - + count - + " attempts"); + if (tileCountDiffs != null) { + List sorted = sortPayloads(tileCountDiffs); + for (PageStatsPayload payload : sorted) { + upsertTilePageFillFactor(tileSet, payload); } } }); @@ -535,6 +427,82 @@ protected List sortPayloads(Collection tileC return result; } + private void updateQuotas(final TileSet tileSet, final Quota quotaDiff) { + if (log.isLoggable(Level.FINE)) { + log.info("Applying quota diff " + quotaDiff.getBytes() + " on tileset " + tileSet); + } + + String updateQuota = dialect.getUpdateQuotaStatement(schema, "tileSetId", "bytes"); + Map params = new HashMap<>(); + params.put("tileSetId", tileSet.getId()); + params.put("bytes", new BigDecimal(quotaDiff.getBytes())); + jt.update(updateQuota, params); + params.put("tileSetId", GLOBAL_QUOTA_NAME); + jt.update(updateQuota, params); + } + + private void upsertTilePageFillFactor(final TileSet tileSet, PageStatsPayload payload) { + if (log.isLoggable(Level.FINE)) { + log.info("Applying page stats payload " + payload); + } + + // see http://en.wikipedia.org/wiki/Merge_(SQL) + // Even the Merge command that some databases support is prone to race + // conditions + // under concurrent load, but we don't want to lose data and it's difficult + // to + // tell apart the race conditions from other failures, so we use tolerant + // commands + // and loop over them. + // Loop conditions: we find the page stats, but they are deleted before we + // can + // update + // them, we don't find the page stats, but they are inserted before we can + // do so, in + // both cases we re-start from zero + TilePage page = payload.getPage(); + final byte level = page.getZoomLevel(); + final BigInteger tilesPerPage = calculator.getTilesPerPage(tileSet, level); + + int modified = 0; + int count = 0; + while (modified == 0 && count < maxLoops) { + try { + count++; + PageStats stats = getPageStats(page.getKey()); + if (stats != null) { + float oldFillFactor = stats.getFillFactor(); + stats.addTiles(payload.getNumTiles(), tilesPerPage); + // if no change, bail out early + if (oldFillFactor == stats.getFillFactor()) { + return; + } + + // update the record in the db + modified = updatePageFillFactor(page, stats, oldFillFactor); + } else { + // create the stats and update the fill factor + stats = new PageStats(0); + stats.addTiles(payload.getNumTiles(), tilesPerPage); + + modified = createNewPageStats(stats, page); + } + } catch (PessimisticLockingFailureException e) { + if (log.isLoggable(Level.FINE)) { + log.log(Level.FINE, "Deadlock while updating page stats, will retry", e); + } + } + } + + if (modified == 0) { + throw new ConcurrencyFailureException("Failed to create or update page stats for page " + + payload.getPage() + + " after " + + count + + " attempts"); + } + } + private int updatePageFillFactor(TilePage page, PageStats stats, float oldFillFactor) { if (log.isLoggable(Level.FINE)) { log.info("Updating page " + page + " fill factor from " + oldFillFactor + " to " + stats.getFillFactor()); @@ -755,27 +723,23 @@ public TilePage mapRow(ResultSet rs, int rowNum) throws SQLException { @Override public void deleteParameters(final String layerName, final String parametersId) { - tt.execute(new TransactionCallbackWithoutResult() { - - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - // first gather the disk quota used by the gridset, and update the global - // quota - Quota quota = getUsedQuotaByParametersId(parametersId); - quota.setBytes(quota.getBytes().negate()); - String updateQuota = dialect.getUpdateQuotaStatement(schema, "tileSetId", "bytes"); - Map params = new HashMap<>(); - params.put("tileSetId", GLOBAL_QUOTA_NAME); - params.put("bytes", new BigDecimal(quota.getBytes())); - jt.update(updateQuota, params); - - // then delete all the gridsets with the specified id - String statement = dialect.getLayerParametersDeletionStatement(schema, "layerName", "parametersId"); - params = new HashMap<>(); - params.put("layerName", layerName); - params.put("parametersId", parametersId); - jt.update(statement, params); - } + tt.executeWithoutResult(status -> { + // first gather the disk quota used by the gridset, and update the global + // quota + Quota quota = getUsedQuotaByParametersId(parametersId); + quota.setBytes(quota.getBytes().negate()); + String updateQuota = dialect.getUpdateQuotaStatement(schema, "tileSetId", "bytes"); + Map params = new HashMap<>(); + params.put("tileSetId", GLOBAL_QUOTA_NAME); + params.put("bytes", new BigDecimal(quota.getBytes())); + jt.update(updateQuota, params); + + // then delete all the gridsets with the specified id + String statement = dialect.getLayerParametersDeletionStatement(schema, "layerName", "parametersId"); + params = new HashMap<>(); + params.put("layerName", layerName); + params.put("parametersId", parametersId); + jt.update(statement, params); }); } From 0e7d1a4f45888db531927185b6155da364a2ee3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Fri, 30 Jan 2026 09:03:49 +0100 Subject: [PATCH 12/15] Spring and Spring Security 7.0.2 upgrade --- geowebcache/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geowebcache/pom.xml b/geowebcache/pom.xml index 29b1c443c..e8814d0f2 100644 --- a/geowebcache/pom.xml +++ b/geowebcache/pom.xml @@ -53,8 +53,8 @@ 35-SNAPSHOT - 7.0.1 - 6.5.5 + 7.0.2 + 7.0.2 1.4.21 1.18.0 2.0.0-M4 From 94faeec8a5c3446de4fc49b6a9eea29e185ac3ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Fri, 30 Jan 2026 09:06:57 +0100 Subject: [PATCH 13/15] GeoTools is now on httpclient 5 --- geowebcache/pom.xml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/geowebcache/pom.xml b/geowebcache/pom.xml index e8814d0f2..816bdfb4f 100644 --- a/geowebcache/pom.xml +++ b/geowebcache/pom.xml @@ -271,13 +271,6 @@ test - - - org.apache.httpcomponents.client5 - httpclient5 - 5.4.4 - - From 12731d13eac13d197e0e96753434dc090085a4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Fri, 30 Jan 2026 16:16:40 +0100 Subject: [PATCH 14/15] Fix GeoServer integration --- .../rest/controller/SeedController.java | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/geowebcache/rest/src/main/java/org/geowebcache/rest/controller/SeedController.java b/geowebcache/rest/src/main/java/org/geowebcache/rest/controller/SeedController.java index d1458453d..6ad36c0d9 100644 --- a/geowebcache/rest/src/main/java/org/geowebcache/rest/controller/SeedController.java +++ b/geowebcache/rest/src/main/java/org/geowebcache/rest/controller/SeedController.java @@ -119,10 +119,7 @@ public ResponseEntity doPost(HttpServletRequest request) { * * @param params Query parameters, including urlencoded form values */ - @RequestMapping( - value = "/seed/{layer:.+}", - method = RequestMethod.POST, - consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.ALL_VALUE}) + @RequestMapping(value = "/seed/{layer:.+}", method = RequestMethod.POST) public ResponseEntity doPost( HttpServletRequest request, InputStream inputStream, @@ -146,10 +143,7 @@ public ResponseEntity doPost( } /** POST method for JSON seeding/truncating with path extension. */ - @RequestMapping( - value = "/seed/{layer}.json", - method = RequestMethod.POST, - consumes = {MediaType.APPLICATION_JSON_VALUE}) + @RequestMapping(value = "/seed/{layer}.json", method = RequestMethod.POST) public ResponseEntity seedOrTruncateWithJsonPayload( HttpServletRequest request, InputStream inputStream, @PathVariable(name = "layer") String layerName) { @@ -159,10 +153,7 @@ public ResponseEntity seedOrTruncateWithJsonPayload( } /** POST method for XML seeding/truncating with path extension. */ - @RequestMapping( - value = "/seed/{layer}.xml", - method = RequestMethod.POST, - consumes = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE}) + @RequestMapping(value = "/seed/{layer}.xml", method = RequestMethod.POST) public ResponseEntity seedOrTruncateWithXmlPayload( HttpServletRequest request, InputStream inputStream, @PathVariable(name = "layer") String layerName) { From 2f26d3ab3d5c59c7b6f3a220ff976881f3ba7e93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9cile=20Vuilleumier?= Date: Mon, 2 Feb 2026 09:41:53 +0100 Subject: [PATCH 15/15] Use Jackson version from geotools --- geowebcache/pom.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/geowebcache/pom.xml b/geowebcache/pom.xml index 816bdfb4f..9480778e0 100644 --- a/geowebcache/pom.xml +++ b/geowebcache/pom.xml @@ -254,14 +254,6 @@ joda-time ${joda-time.version} - - - - com.fasterxml.jackson.core - jackson-annotations - 2.20 - -