From bce0b37dbd25152bb848bf795104b177a8c5fc12 Mon Sep 17 00:00:00 2001 From: Marc Hammerton Date: Wed, 9 Oct 2024 08:59:52 +1000 Subject: [PATCH 01/43] Add missing JARs to assembly.xml file for cog-s3 plugin The dependency `AWS SDK` of the `cog-s3` plugin requires additional libraries after it was upgraded from version 2.24.13 to 2.27.23. This fixes the issue discussed in https://discourse.osgeo.org/t/missing-jars-for-cog-s3-plugin-in-assembly-xml/110332 --- src/community/cog/cog-s3/src/assembly/assembly.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/community/cog/cog-s3/src/assembly/assembly.xml b/src/community/cog/cog-s3/src/assembly/assembly.xml index 305c1a95c5c..1e8ed93c2c6 100644 --- a/src/community/cog/cog-s3/src/assembly/assembly.xml +++ b/src/community/cog/cog-s3/src/assembly/assembly.xml @@ -27,6 +27,7 @@ imageio-ext-*rangereader*.jar jackson-datatype*.jar jackson-module*.jar + json-utils-2.*.jar kotlin-stdlib*.jar *netty*.jar okhttp*.jar @@ -35,6 +36,8 @@ protocol-core-2.*.jar reactive-streams*.jar regions-2.*.jar + retries-2.*.jar + retries-spi-2.*.jar rxjava*.jar s3-2.*.jar sdk-core-2.*.jar @@ -45,6 +48,7 @@ http-auth-*2*.jar identity-spi*.jar metrics-spi-2.*.jar + third-party-jackson-core-2.*.jar From 6c2a1f52621514522ffcd665ad88d16c61b64ef4 Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Fri, 18 Oct 2024 20:02:39 +0200 Subject: [PATCH 02/43] [GEOS-11578] WMTS Multidim extension, allow usage of a sidecar in a separate store --- .../wmts-multidimensional/images/sidecar.png | Bin 15836 -> 12392 bytes .../wmts-multidimensional/performance.rst | 3 + .../geoserver/gwc/web/MultiDimLayerPanel.html | 4 + .../geoserver/gwc/web/MultiDimLayerPanel.java | 75 +++++- .../gwc/wmts/MultiDimensionalExtension.java | 6 + .../gwc/wmts/dimensions/DimensionsUtils.java | 38 ++- .../resources/GeoServerApplication.properties | 4 + .../gwc/web/MultiDimLayerPanelTest.java | 131 +++++++++++ .../wmts/MultiDimensionalExtensionTest.java | 218 ------------------ .../gwc/wmts/SidecarStoreTypeTest.java | 88 +++++++ .../geoserver/gwc/wmts/SidecarTypeTest.java | 176 ++++++++++++++ .../org/geoserver/gwc/wmts/TestsSupport.java | 101 +++++++- ...idecarTimeElevationWithStartEnd.properties | 2 +- 13 files changed, 610 insertions(+), 236 deletions(-) create mode 100644 src/extension/wmts-multi-dimensional/src/test/java/org/geoserver/gwc/wmts/SidecarStoreTypeTest.java create mode 100644 src/extension/wmts-multi-dimensional/src/test/java/org/geoserver/gwc/wmts/SidecarTypeTest.java diff --git a/doc/en/user/source/extensions/wmts-multidimensional/images/sidecar.png b/doc/en/user/source/extensions/wmts-multidimensional/images/sidecar.png index 318728d9c9a45d6fdc0e66de3c0c7bf45a16ad86..4e8037ebc18f0c28ca66aa7c8b55cbc32f0e6071 100644 GIT binary patch literal 12392 zcmb7~bx@qo((fN2NN{&2KyY_UkYK@G7kAg-PDmiQYjAgW39iB2-CY*F`#Y!Jx9+`l zs_uKcYG?P4*{ykcdV9X}>HeW8FNuOkhzI}xinNrtG5|mkK|aSIKto1vtZ;A0+j|Eo zEoT5g?fvHkmB4@snL!Rni+@)AmVTPyqpG6OaqoA}HXY~48LYpJXx)?WNn0qpg}0a) zt8R=!|Hl`q1ibx3Jo67nOux&ajisx9;!2QojbmS|U2UoMs;d%j#2}Wpebw2jsBS=;G#UK@9E&4NJDWx#5; z!wgh2Pt9)p$l>ez7G@>Myk`0jx?A>B<_ z|F5Z}jX4zs<=^xh97dW0q&4_oi}W7aY>e#&@)&eY1K)b~af4>o#^@lHX5@@3ozeO>(*A_}}-&oJh!@j<0d+Vmp+0Pl% z`S9OhhYYKqa5XQV#DOjc;3jMnA4{xLf2l(Ds!p%s(8~qMU5AL!b3ib)Fd|Y#m=lcx zEj&rYQ|Zhw5#%6hbg@nE!~dLH(1Ox+3gvml@cQ5|U1lGUMRvnIQ^zk-uRebt*lXW? zFzbi?8XC`}%fbH8Rc}K=r_)qi<=zH+e7N3GwJA>9)#yPaEpY@oPXlWbMfR0xA9b40 zwF{N5dD_>o`OeL&+?3c*5IAmC;uNQ;%1XWAq;U{FdRndh(8Sp2+}RDwt)Gke>h5J# zgUQ`-(iwaMs`fo@rP`OQ1(gf8gy=Uc_$?ll(^f=ARKaLm$MQ^$;Ev!jtCx9lRUa_G z8BFc$%O8W?8;+n~@M2#_0x8}0=FoU2-e#LlJQi!#HIgSj*dgD5Fn=#ulpQ$ew^-W_ z$}K$97*Wva;9|$<+W0Pnjk`Wm+ddJ_Vk$T6?4k_8n!y3dk3prTNO<<&8c_iMCwei- zVcZJ&R_(;94Z8t{ATwe$fjmr%N*iz z_UJ!i@>{VquEF|8hIYCE#W`joKVbvt-6r6M7y-6+1|gr9&J4-5?9=e1ofT;L!2NZII~442u49KP(;|JjVPF?z7W_jYV`utIBn!$7fjQbfv9PVHQCF=TaUA-*!-M`nfgx zJ(O!}<2`p*=g_P*!R)(rjJ8O2=cjTK#&R-=I&mmeYDS

I0 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 Rv|Sqz7nb=}@m0_7{{g~kwQB$X diff --git a/doc/en/user/source/extensions/wmts-multidimensional/performance.rst b/doc/en/user/source/extensions/wmts-multidimensional/performance.rst index b92526b8b6a..c23d61b9654 100644 --- a/doc/en/user/source/extensions/wmts-multidimensional/performance.rst +++ b/doc/en/user/source/extensions/wmts-multidimensional/performance.rst @@ -28,6 +28,9 @@ in place of the original table, for any domain extraction purpose: *Setting up a sidecar table.* +The summary table is normally looked up in the same store, but if needed, it can be also found +in a different store, as long as it's accessible to the GeoServer instance. Leave the sidecar +store empty in case the summary table is in the same store as the original table. Conditions for the sidecar table to work: diff --git a/src/extension/wmts-multi-dimensional/src/main/java/org/geoserver/gwc/web/MultiDimLayerPanel.html b/src/extension/wmts-multi-dimensional/src/main/java/org/geoserver/gwc/web/MultiDimLayerPanel.html index 0b57e477b75..02df4b6358f 100644 --- a/src/extension/wmts-multi-dimensional/src/main/java/org/geoserver/gwc/web/MultiDimLayerPanel.html +++ b/src/extension/wmts-multi-dimensional/src/main/java/org/geoserver/gwc/web/MultiDimLayerPanel.html @@ -11,6 +11,10 @@

  • + + + diff --git a/src/web/gwc/src/main/java/org/geoserver/gwc/web/layer/CachedLayersPage.java b/src/web/gwc/src/main/java/org/geoserver/gwc/web/layer/CachedLayersPage.java index 43bc6ccab33..59dfd5fb990 100644 --- a/src/web/gwc/src/main/java/org/geoserver/gwc/web/layer/CachedLayersPage.java +++ b/src/web/gwc/src/main/java/org/geoserver/gwc/web/layer/CachedLayersPage.java @@ -26,7 +26,8 @@ import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.markup.html.AjaxLink; -import org.apache.wicket.behavior.AttributeAppender; +import org.apache.wicket.markup.head.IHeaderResponse; +import org.apache.wicket.markup.head.OnDomReadyHeaderItem; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.image.Image; @@ -149,6 +150,17 @@ protected void onSelectionUpdate(AjaxRequestTarget target) { } } + @Override + public void renderHead(IHeaderResponse response) { + super.renderHead(response); + String script = + "$('.tile-layers-page-menu-select').on('change', function(event) {\n" + + " window.open(this.options[this.selectedIndex].value);\n" + + " this.selectedIndex=0;\n" + + "});"; + response.render(OnDomReadyHeaderItem.forScript(script)); + } + private Component quotaLink(String id, IModel quotaModel) { Quota quota = quotaModel.getObject(); String formattedQuota; @@ -279,19 +291,6 @@ private Component previewLinks(String id, IModel tileLayerModel) { RepeatingView previewLinks = new RepeatingView("previewLink"); - int i = 0; - for (String gridSetId : gridSubsets) { - for (MimeType mimeType : mimeTypes) { - String label = gridSetId + " / " + mimeType.getFileExtension(); - // build option with text and value - Label format = new Label(String.valueOf(i++), label); - String value = "gridSet=" + gridSetId + "&format=" + mimeType.getFormat(); - format.add(new AttributeModifier("value", new Model<>(value))); - previewLinks.add(format); - } - } - menu.add(previewLinks); - // build the wms request, redirect to it in a new window, reset the selection final String baseURL = ResponseUtils.baseURL(getGeoServerApplication().servletRequest()); // Since we're working with an absolute URL, build the URL this way to ensure proxy @@ -303,18 +302,25 @@ private Component previewLinks(String id, IModel tileLayerModel) { workspaceName = layer.getName().substring(0, layer.getName().indexOf(":")) + "/"; } final String demoURL = - "'" - + ResponseUtils.buildURL( + ResponseUtils.buildURL( baseURL + workspaceName, "gwc/demo/" + layer.getName(), null, URLType.EXTERNAL) - + "?' + this.options[this.selectedIndex].value"; - menu.add( - new AttributeAppender( - "onchange", - new Model<>("window.open(" + demoURL + ");this.selectedIndex=0"), - ";")); + + "?gridSet="; + + int i = 0; + for (String gridSetId : gridSubsets) { + for (MimeType mimeType : mimeTypes) { + String label = gridSetId + " / " + mimeType.getFileExtension(); + // build option with text and value + Label format = new Label(String.valueOf(i++), label); + String value = demoURL + gridSetId + "&format=" + mimeType.getFormat(); + format.add(new AttributeModifier("value", new Model<>(value))); + previewLinks.add(format); + } + } + menu.add(previewLinks); f.add(menu); return f; diff --git a/src/web/gwc/src/test/java/org/geoserver/gwc/web/layer/CachedLayersPageTest.java b/src/web/gwc/src/test/java/org/geoserver/gwc/web/layer/CachedLayersPageTest.java index 2eb625f96f0..b7b7205f3b6 100644 --- a/src/web/gwc/src/test/java/org/geoserver/gwc/web/layer/CachedLayersPageTest.java +++ b/src/web/gwc/src/test/java/org/geoserver/gwc/web/layer/CachedLayersPageTest.java @@ -5,15 +5,11 @@ */ package org.geoserver.gwc.web.layer; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import java.lang.reflect.Method; import java.util.List; -import java.util.logging.Level; -import org.apache.wicket.AttributeModifier; -import org.apache.wicket.Component; -import org.apache.wicket.model.IModel; +import org.apache.wicket.util.tester.TagTester; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.CatalogBuilder; import org.geoserver.catalog.LayerGroupInfo; @@ -31,22 +27,6 @@ import org.junit.Test; public class CachedLayersPageTest extends GeoServerWicketTestSupport { - private static final Method getReplaceModelMethod; - - /* - * Yes, we do need to use reflection here as the stupid wicket model is private and has no setter which - * makes it hard to test! - * See https://cwiki.apache.org/confluence/display/WICKET/Testing+Pages - */ - static { - try { - getReplaceModelMethod = AttributeModifier.class.getDeclaredMethod("getReplaceModel"); - getReplaceModelMethod.setAccessible(true); - } catch (Exception e) { - LOGGER.log(Level.WARNING, "", e); - throw new RuntimeException(e); - } - } @Rule public GeoServerExtensionsHelper.ExtensionsHelperRule extensions = @@ -135,27 +115,8 @@ public void testMangleSeedLink() { @Test public void testNoManglePreviewLink() { - // Don't add a mangler - - CachedLayersPage page = new CachedLayersPage(); - - tester.startPage(page); - // print(page, true, true); - Component component = - tester.getComponentFromLastRenderedPage( - "table:listContainer:items:1:itemProperties:6:component:menu"); - List attr = component.getBehaviors(AttributeModifier.class); - try { - IModel model = (IModel) getReplaceModelMethod.invoke(attr.get(0)); - assertTrue( - "Unmangled names fail", - model.getObject() - .toString() - .contains("http://localhost/context/cgf/gwc/demo/cgf")); - } catch (Exception e) { - throw new RuntimeException(e); - } + assertPreviewLinks("http://localhost/context/cgf/gwc/demo/cgf"); } @Test @@ -167,23 +128,21 @@ public void testManglePreviewLink() { base.append("http://rewrite/"); }; extensions.singleton("testMangler", testMangler, URLMangler.class); + assertPreviewLinks("http://rewrite/gwc/demo/cgf"); + } + private static void assertPreviewLinks(String url) { CachedLayersPage page = new CachedLayersPage(); - tester.startPage(page); - Component component = - tester.getComponentFromLastRenderedPage( - "table:listContainer:items:1:itemProperties:6:component:menu"); - List attr = component.getBehaviors(AttributeModifier.class); - try { - IModel model = (IModel) getReplaceModelMethod.invoke(attr.get(0)); - - assertTrue( - "Mangled names fail", - model.getObject().toString().contains("http://rewrite/gwc/demo/cgf")); - } catch (Exception e) { - throw new RuntimeException(e); - } + List tags = + TagTester.createTags( + tester.getLastResponseAsString(), + tag -> { + String value = tag.getAttributes().getString("value"); + return value != null && value.startsWith(url); + }, + false); + assertEquals("Incorrect number of preview links starting with " + url, 20, tags.size()); } @Test From f92d58e2cbb175c197cc13099d471929667cbb6f Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Mon, 28 Oct 2024 10:02:28 +0100 Subject: [PATCH 07/43] Follow up with GEOT-7664 module renames --- src/community/stac-datastore/src/assembly/assembly.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/community/stac-datastore/src/assembly/assembly.xml b/src/community/stac-datastore/src/assembly/assembly.xml index dd26eb46461..295482db260 100644 --- a/src/community/stac-datastore/src/assembly/assembly.xml +++ b/src/community/stac-datastore/src/assembly/assembly.xml @@ -10,8 +10,7 @@ gt-stac-store*.jar - gt-cql2-text-*.jar - gt-cql-json-*.jar + gt-cql2-*.jar From e3862063f063347b24ff4bdc897d550775495694 Mon Sep 17 00:00:00 2001 From: "Ikeoka, Steve" Date: Thu, 17 Oct 2024 12:52:19 -0700 Subject: [PATCH 08/43] Fixed some Wicket CSP violations --- .../monitor/web/ActivityChartBasePanel.html | 12 +- .../geoserver/wps/web/WPSAccessRulePage.html | 17 --- .../java/org/geoserver/GeoServerNodeData.java | 29 ++-- .../web/DefaultGeoServerNodeInfo.java | 42 +++++- .../geoserver/web/GeoServerApplication.java | 52 +++---- .../java/org/geoserver/web/css/geoserver.css | 129 +++++++++++++----- .../data/resource/DimensionEditorBase.java | 11 +- .../web/wicket/browser/FileDataView.java | 34 +++-- .../web/CustomGeoServerNodeIdTest.java | 11 +- .../org/geoserver/web/demo/demo-requests.css | 2 +- .../gwc/web/diskquota/StatusBar.java | 50 ++++--- .../geoserver/gwc/web/diskquota/statusbar.css | 2 + .../gwc/web/layer/GridSubsetsEditor.java | 5 +- .../web/AbstractConfirmRemovalPanel.html | 19 +-- ...curityNamedServiceEditPage$ErrorPanel.html | 6 - .../wms/web/data/ExternalGraphicPanel.java | 9 +- .../geoserver/wms/web/data/StyleEditPage.java | 8 +- .../org/geoserver/wms/web/data/StylePage.css | 16 +-- 18 files changed, 239 insertions(+), 215 deletions(-) diff --git a/src/extension/monitor/core/src/main/java/org/geoserver/monitor/web/ActivityChartBasePanel.html b/src/extension/monitor/core/src/main/java/org/geoserver/monitor/web/ActivityChartBasePanel.html index efb40fa0f6c..ccf1e628ec5 100644 --- a/src/extension/monitor/core/src/main/java/org/geoserver/monitor/web/ActivityChartBasePanel.html +++ b/src/extension/monitor/core/src/main/java/org/geoserver/monitor/web/ActivityChartBasePanel.html @@ -1,22 +1,14 @@ <wicket:message key="title">Activity</wicket:message> - - - -
    - + - + Refresh diff --git a/src/extension/wps/web-wps/src/main/java/org/geoserver/wps/web/WPSAccessRulePage.html b/src/extension/wps/web-wps/src/main/java/org/geoserver/wps/web/WPSAccessRulePage.html index ecb9f50fbd4..d1e49c42a60 100644 --- a/src/extension/wps/web-wps/src/main/java/org/geoserver/wps/web/WPSAccessRulePage.html +++ b/src/extension/wps/web-wps/src/main/java/org/geoserver/wps/web/WPSAccessRulePage.html @@ -3,23 +3,6 @@ - <wicket:message key="title">WPS security</wicket:message> diff --git a/src/main/src/main/java/org/geoserver/GeoServerNodeData.java b/src/main/src/main/java/org/geoserver/GeoServerNodeData.java index ae3ff6e1e7d..995fcbf2e36 100644 --- a/src/main/src/main/java/org/geoserver/GeoServerNodeData.java +++ b/src/main/src/main/java/org/geoserver/GeoServerNodeData.java @@ -13,6 +13,7 @@ import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -37,15 +38,12 @@ public class GeoServerNodeData { /** System property, environment variable, etc... used to specify the node id. */ public static final String GEOSERVER_NODE_OPTS = "GEOSERVER_NODE_OPTS"; - static final String DEFAULT_NODE_ID_TEMPLATE = - "position:absolute; top:12px; left:12px; right:28px; width:auto; background:$background; padding: 1px; border: 1px solid #0076a1; color:$color; font-weight:bold"; - static final Logger LOGGER = Logging.getLogger(GeoServerNodeData.class); final String nodeId; - final String nodeIdStyle; + final Map nodeIdStyle; - public GeoServerNodeData(String nodeId, String nodeIdStyle) { + public GeoServerNodeData(String nodeId, Map nodeIdStyle) { this.nodeId = nodeId; this.nodeIdStyle = nodeIdStyle; } @@ -83,12 +81,9 @@ public GeoServerNodeData(String nodeId, String nodeIdStyle) { /** Creates a node data from a format-options style string. */ public static GeoServerNodeData createFromString(String nodeOpts) { String nodeId = null; - String nodeIdStyle = null; + Map nodeIdStyle = new LinkedHashMap<>(); - if (nodeOpts == null) { - nodeId = null; - nodeIdStyle = null; - } else { + if (nodeOpts != null) { try { Map options = parseProperties(nodeOpts); String id = options.get("id"); @@ -106,15 +101,13 @@ public static GeoServerNodeData createFromString(String nodeOpts) { nodeId = id; String bgcolor = options.get("background"); - if (bgcolor == null) { - bgcolor = "#dadada"; + if (bgcolor != null) { + nodeIdStyle.put("backgroundColor", bgcolor); } - String style = DEFAULT_NODE_ID_TEMPLATE.replace("$background", bgcolor); String color = options.get("color"); - if (color == null) { - color = "#0076a1"; + if (color != null) { + nodeIdStyle.put("color", color); } - nodeIdStyle = style.replace("$color", color); } catch (Exception e) { LOGGER.log( Level.SEVERE, @@ -122,7 +115,7 @@ public static GeoServerNodeData createFromString(String nodeOpts) { + nodeOpts + " instead. Disabling NODE_ID GUI element"); nodeId = null; - nodeIdStyle = null; + nodeIdStyle.clear(); } } @@ -214,7 +207,7 @@ public String getId() { } /** The node id styling. */ - public String getIdStyle() { + public Map getIdStyle() { return nodeIdStyle; } } diff --git a/src/web/core/src/main/java/org/geoserver/web/DefaultGeoServerNodeInfo.java b/src/web/core/src/main/java/org/geoserver/web/DefaultGeoServerNodeInfo.java index 99b69b6fffe..5129346572a 100644 --- a/src/web/core/src/main/java/org/geoserver/web/DefaultGeoServerNodeInfo.java +++ b/src/web/core/src/main/java/org/geoserver/web/DefaultGeoServerNodeInfo.java @@ -5,9 +5,16 @@ */ package org.geoserver.web; -import org.apache.wicket.behavior.AttributeAppender; +import java.io.Serializable; +import java.util.Map; +import java.util.Map.Entry; +import org.apache.commons.text.StringEscapeUtils; +import org.apache.wicket.AttributeModifier; +import org.apache.wicket.Component; +import org.apache.wicket.behavior.Behavior; +import org.apache.wicket.markup.head.IHeaderResponse; +import org.apache.wicket.markup.head.OnLoadHeaderItem; import org.apache.wicket.markup.html.WebMarkupContainer; -import org.apache.wicket.model.Model; import org.geoserver.GeoServerNodeData; import org.geoserver.web.spring.security.GeoServerSession; @@ -21,7 +28,9 @@ * * @author Andrea Aime - GeoSolutions */ -public class DefaultGeoServerNodeInfo implements GeoServerNodeInfo { +public class DefaultGeoServerNodeInfo implements GeoServerNodeInfo, Serializable { + + private static final long serialVersionUID = -8731277645321595181L; static final String GEOSERVER_NODE_OPTS = GeoServerNodeData.GEOSERVER_NODE_OPTS; @@ -38,10 +47,35 @@ public GeoServerNodeData getData() { @Override public void customize(WebMarkupContainer container) { - container.add(new AttributeAppender("style", new Model<>(NODE_DATA.getIdStyle()), ";")); + container.add(AttributeModifier.replace("class", "default-node-info")); + Map properties = NODE_DATA.getIdStyle(); + if (properties != null && !properties.isEmpty()) { + container.add( + new Behavior() { + private static final long serialVersionUID = -7945010069411202354L; + + @Override + public void renderHead(Component component, IHeaderResponse response) { + String script = toJavaScript(properties); + response.render(OnLoadHeaderItem.forScript(script)); + } + }); + } container.setVisible(isNodeIdVisible(container)); } + private static String toJavaScript(Map properties) { + StringBuilder builder = new StringBuilder(); + for (Entry entry : properties.entrySet()) { + builder.append("document.getElementsByClassName('default-node-info')[0].style."); + builder.append(StringEscapeUtils.escapeEcmaScript(entry.getKey())); + builder.append(" = '"); + builder.append(StringEscapeUtils.escapeEcmaScript(entry.getValue())); + builder.append("';\n"); + } + return builder.toString().trim(); + } + protected static void initializeFromEnviroment() { NODE_DATA = GeoServerNodeData.createFromEnvironment(); } diff --git a/src/web/core/src/main/java/org/geoserver/web/GeoServerApplication.java b/src/web/core/src/main/java/org/geoserver/web/GeoServerApplication.java index fdc200941f8..b25d1fb9fd3 100644 --- a/src/web/core/src/main/java/org/geoserver/web/GeoServerApplication.java +++ b/src/web/core/src/main/java/org/geoserver/web/GeoServerApplication.java @@ -28,7 +28,6 @@ import org.apache.wicket.core.request.handler.IPageRequestHandler; import org.apache.wicket.core.request.handler.PageProvider; import org.apache.wicket.csp.CSPDirective; -import org.apache.wicket.csp.CSPDirectiveSrcValue; import org.apache.wicket.protocol.http.CsrfPreventionRequestCycleListener; import org.apache.wicket.protocol.http.WebApplication; import org.apache.wicket.protocol.http.WebSession; @@ -224,6 +223,18 @@ public void clearWicketCaches() { getResourceSettings().getLocalizer().clearCache(); } + /** + * Gets the nonce that will be used in the script-src and style-src directives of the + * Content-Security-Policy header for this request. The nonce attribute can be added to script + * or style elements to allow them to work with Wicket's CSP which blocks inline scripts and + * styles. This method should only be used in cases where it is not possible to refactor the + * code to work with the CSP, such as when the CSP violations are coming directly from + * third-party libraries with built-in support for CSP nonces. + */ + public String getNonce() { + return getCspSettings().getNonce(RequestCycle.get()); + } + /** Initialization override which sets up a locator for i18n resources. */ @SuppressWarnings("deprecation") @Override @@ -231,37 +242,18 @@ protected void init() { // enable GeoServer custom resource locators getResourceSettings().setUseMinifiedResources(false); getResourceSettings().setResourceStreamLocator(new GeoServerResourceStreamLocator()); + // Wicket's default Content-Security-Policy value is: + // default-src 'none'; script-src 'strict-dynamic' 'nonce-XYZ'; style-src 'nonce-XYZ'; + // img-src 'self'; connect-src 'self'; font-src 'self'; manifest-src 'self'; + // child-src 'self'; frame-src 'self'; base-uri 'self'; + // GeoServer adds data: to the img-src directive to allow image data URIs used by + // OpenLayers, CodeMirror, the datetime picker and color picker (primarily for Style Editor) if (CSP_STRICT) { - getCspSettings() - .blocking() - .clear() - .add( - CSPDirective.SCRIPT_SRC, - CSPDirectiveSrcValue.STRICT_DYNAMIC, - CSPDirectiveSrcValue.NONCE) - .add(CSPDirective.STYLE_SRC, CSPDirectiveSrcValue.NONCE) - .add(CSPDirective.IMG_SRC, "'self'", "data:") - .add(CSPDirective.CONNECT_SRC, CSPDirectiveSrcValue.SELF) - .add(CSPDirective.FONT_SRC, CSPDirectiveSrcValue.SELF) - .add(CSPDirective.MANIFEST_SRC, CSPDirectiveSrcValue.SELF) - .add(CSPDirective.CHILD_SRC, CSPDirectiveSrcValue.SELF) - .add(CSPDirective.BASE_URI, CSPDirectiveSrcValue.SELF); + getCspSettings().blocking().strict().add(CSPDirective.IMG_SRC, "data:"); } else { - // More relaxed configuration: report only - getCspSettings() - .reporting() - .clear() - .add( - CSPDirective.SCRIPT_SRC, - CSPDirectiveSrcValue.STRICT_DYNAMIC, - CSPDirectiveSrcValue.NONCE) - .add(CSPDirective.STYLE_SRC, CSPDirectiveSrcValue.NONCE) - .add(CSPDirective.IMG_SRC, "'self'", "data:") - .add(CSPDirective.CONNECT_SRC, CSPDirectiveSrcValue.SELF) - .add(CSPDirective.FONT_SRC, CSPDirectiveSrcValue.SELF) - .add(CSPDirective.MANIFEST_SRC, CSPDirectiveSrcValue.SELF) - .add(CSPDirective.CHILD_SRC, CSPDirectiveSrcValue.SELF) - .add(CSPDirective.BASE_URI, CSPDirectiveSrcValue.SELF); + // More relaxed configuration: disable blocking and enable reporting only + getCspSettings().blocking().disabled(); + getCspSettings().reporting().strict().add(CSPDirective.IMG_SRC, "data:"); } /* * The order string resource loaders are added to IResourceSettings is of importance so we need to add any contributed loader prior to the diff --git a/src/web/core/src/main/java/org/geoserver/web/css/geoserver.css b/src/web/core/src/main/java/org/geoserver/web/css/geoserver.css index f98dce87e41..b397ec3beaf 100644 --- a/src/web/core/src/main/java/org/geoserver/web/css/geoserver.css +++ b/src/web/core/src/main/java/org/geoserver/web/css/geoserver.css @@ -240,63 +240,63 @@ Utility Classes width: 95% !important; } -w-1pct { +.w-1pct { width: 1%; } -w-90pct { +.w-90pct { width: 90%; } -w-99pct { +.w-99pct { width: 99%; } -w-100pct { +.w-100pct { width: 100%; } -w-30px { +.w-30px { width: 30px; } -w-50px { +.w-50px { width: 50px; } -w-80px { +.w-80px { width: 80px; } -w-120px { +.w-120px { width: 120px; } -w-200px { +.w-200px { width: 200px; } -w-240px { +.w-240px { width: 240px; } -w-260px { +.w-260px { width: 260px; } -w-300px { +.w-300px { width: 300px; } -w-600px { +.w-600px { width: 600px; } -min-w-200px { +.min-w-200px { min-width: 200px; } -max-w-100pct { +.max-w-100pct { max-width: 100%; } @@ -326,29 +326,29 @@ max-w-100pct { height: 100em !important; } -h-50px { +.h-50px { height: 50px; } -h-100px { +.h-100px { height: 100px; } -h-100pct { +.h-100pct { height: 100%; } /* more migration utils */ -margin-left-5pct { +.margin-left-5pct { margin-left: 5%; } -visibility-hidden { +.visibility-hidden { visibility: hidden; } -visibility-visible { +.visibility-visible { visibility: visible; } @@ -1740,59 +1740,98 @@ li.select2-results__option { margin: 0px 0.2em; } -traceback { +.traceback { padding-top: 16px; white-space: pre; font-family: monospace; } -gwc-remove-link { +.overflowAuto { + overflow: auto; +} + +.default-node-info { + position: absolute; + top: 12px; + left: 12px; + right: 28px; + width: auto; + padding: 1px; + border: 1px solid #0076a1; + font-weight: bold; + background-color: #dadada; + color: #0076a1; +} + +.sec-removal-dialog { + padding: 2em; +} + +.sec-removal-dialog h3 { + margin-left: auto; + max-width: 90%; +} + +.sec-removal-dialog span { + color: #000; +} + +.sec-removal-dialog ul ul { + padding-left: 0.75em; +} + +.gwc-missing-gridset { + color: red; + text-decoration: line-through; +} + +.gwc-remove-link { position:absolute; right:0; text-align:right; } -codemirror { +.codemirror { border: 1px solid black; padding-bottom: 3px; } -italic { +.italic { font-style: italic; } -padding-1em { +.padding-1em { padding: 1em; } -padding-right-25pct { +.padding-right-25pct { padding-right: 25%; } -bottom-margin-1em { +.bottom-margin-1em { margin-bottom: 1em; } -bottom-padding-0pt5em { +.bottom-padding-0pt5em { padding-bottom: 0.5em; } -bottom-padding-2em { +.bottom-padding-2em { padding-bottom: 2em; } -srs-demo-map-container { +.srs-demo-map-container { width:512px; height:256px; background-color:white; } -reproj-console-geom { +.reproj-console-geom { height: 2em; width: 55em; } -image-chooser-img { +.image-chooser-img { display: block; float:right; max-width: 35px; @@ -1801,4 +1840,26 @@ image-chooser-img { .hidden { display: none; -} \ No newline at end of file +} + +.inline { + display: inline; +} + +#processAccessMode { + display: block; + padding-top: 0.5em; +} + +#processAccessMode input { + display: block; + float: left; + clear: left; + padding-top :0.5em; + margin-bottom: 0.5em; +} + +#processAccessMode label { + clear: right; + margin-bottom: 0.5em; +} diff --git a/src/web/core/src/main/java/org/geoserver/web/data/resource/DimensionEditorBase.java b/src/web/core/src/main/java/org/geoserver/web/data/resource/DimensionEditorBase.java index d5a27abf120..9583de60cfd 100644 --- a/src/web/core/src/main/java/org/geoserver/web/data/resource/DimensionEditorBase.java +++ b/src/web/core/src/main/java/org/geoserver/web/data/resource/DimensionEditorBase.java @@ -265,12 +265,10 @@ protected void onUpdate(AjaxRequestTarget target) { boolean listSelected = presentation.getModelObject() == DimensionPresentation.LIST; - String containerVisible = listSelected ? "none" : "initial"; + String containerVisible = listSelected ? "hidden" : ""; startField .getParent() - .add( - new AttributeModifier( - "style", "display:" + containerVisible + ";")); + .add(AttributeModifier.replace("class", containerVisible)); if (listSelected) { startField.setModelValue(new String[0]); startField.setRequired(false); @@ -470,9 +468,8 @@ protected void onUpdate(AjaxRequestTarget target) { final WebMarkupContainer startEndContainer = new WebMarkupContainer("startEndContainer"); startEndContainer.setOutputMarkupId(true); String containerVisibility = - presentation.getModelObject() == DimensionPresentation.LIST ? "none" : "initial"; - startEndContainer.add( - new AttributeModifier("style", "display:" + containerVisibility + ";")); + presentation.getModelObject() == DimensionPresentation.LIST ? "hidden" : ""; + startEndContainer.add(AttributeModifier.replace("class", containerVisibility)); configs.add(startEndContainer); IModel sfModel = new PropertyModel<>(model, "startValue"); diff --git a/src/web/core/src/main/java/org/geoserver/web/wicket/browser/FileDataView.java b/src/web/core/src/main/java/org/geoserver/web/wicket/browser/FileDataView.java index 8313d2633d0..d648070ef2e 100644 --- a/src/web/core/src/main/java/org/geoserver/web/wicket/browser/FileDataView.java +++ b/src/web/core/src/main/java/org/geoserver/web/wicket/browser/FileDataView.java @@ -11,12 +11,16 @@ import java.util.Date; import java.util.Locale; import java.util.Optional; +import org.apache.commons.text.StringEscapeUtils; import org.apache.wicket.AttributeModifier; +import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.markup.html.AjaxFallbackLink; +import org.apache.wicket.behavior.Behavior; import org.apache.wicket.extensions.ajax.markup.html.IndicatingAjaxFallbackLink; import org.apache.wicket.extensions.markup.html.repeater.data.sort.OrderByBorder; -import org.apache.wicket.markup.ComponentTag; +import org.apache.wicket.markup.head.IHeaderResponse; +import org.apache.wicket.markup.head.OnLoadHeaderItem; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.panel.Panel; @@ -153,16 +157,26 @@ public IConverter getConverter(Class type) { } }; - fileContent = - new WebMarkupContainer("fileContent") { - @Override - protected void onComponentTag(ComponentTag tag) { - if (tableHeight != null) { - tag.getAttributes() - .put("style", "overflow:auto; height:" + tableHeight); + fileContent = new WebMarkupContainer("fileContent"); + if (tableHeight != null) { + fileContent.setOutputMarkupId(true); + fileContent.add(AttributeModifier.replace("class", "overflowAuto")); + fileContent.add( + new Behavior() { + private static final long serialVersionUID = 4820788798632906484L; + + @Override + public void renderHead(Component component, IHeaderResponse response) { + String script = + "document.getElementById('" + + fileContent.getMarkupId() + + "').style.height = '" + + StringEscapeUtils.escapeEcmaScript(tableHeight) + + "';"; + response.render(OnLoadHeaderItem.forScript(script)); } - } - }; + }); + } fileContent.add(fileTable); diff --git a/src/web/core/src/test/java/org/geoserver/web/CustomGeoServerNodeIdTest.java b/src/web/core/src/test/java/org/geoserver/web/CustomGeoServerNodeIdTest.java index 22becded11c..6334afc0a5b 100644 --- a/src/web/core/src/test/java/org/geoserver/web/CustomGeoServerNodeIdTest.java +++ b/src/web/core/src/test/java/org/geoserver/web/CustomGeoServerNodeIdTest.java @@ -6,9 +6,7 @@ package org.geoserver.web; import java.util.List; -import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.markup.html.WebMarkupContainer; -import org.apache.wicket.model.Model; import org.geoserver.GeoServerNodeData; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -48,7 +46,6 @@ public void testNodeInfoVisible() throws Exception { public static class CustomNodeInfo implements GeoServerNodeInfo { static String ID = null; - static String STYLE = null; @Override public String getId() { @@ -57,14 +54,10 @@ public String getId() { @Override public GeoServerNodeData getData() { - return new GeoServerNodeData(ID, STYLE); + return new GeoServerNodeData(ID, null); } @Override - public void customize(WebMarkupContainer nodeInfoContainer) { - if (STYLE != null) { - nodeInfoContainer.add(new AttributeAppender("style", new Model<>(STYLE), ";")); - } - } + public void customize(WebMarkupContainer nodeInfoContainer) {} } } diff --git a/src/web/demo/src/main/resources/org/geoserver/web/demo/demo-requests.css b/src/web/demo/src/main/resources/org/geoserver/web/demo/demo-requests.css index 98a6ce85b7b..ca0a76e210f 100644 --- a/src/web/demo/src/main/resources/org/geoserver/web/demo/demo-requests.css +++ b/src/web/demo/src/main/resources/org/geoserver/web/demo/demo-requests.css @@ -17,7 +17,7 @@ flex-direction: row; } -tdNoWidth { +.tdNoWidth { width: 1%; } diff --git a/src/web/gwc/src/main/java/org/geoserver/gwc/web/diskquota/StatusBar.java b/src/web/gwc/src/main/java/org/geoserver/gwc/web/diskquota/StatusBar.java index 51530d405a1..daa07e883cc 100644 --- a/src/web/gwc/src/main/java/org/geoserver/gwc/web/diskquota/StatusBar.java +++ b/src/web/gwc/src/main/java/org/geoserver/gwc/web/diskquota/StatusBar.java @@ -5,22 +5,21 @@ */ package org.geoserver.gwc.web.diskquota; -import org.apache.wicket.AttributeModifier; -import org.apache.wicket.Component; -import org.apache.wicket.behavior.Behavior; import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; +import org.apache.wicket.markup.head.OnLoadHeaderItem; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; -import org.apache.wicket.model.Model; import org.apache.wicket.request.resource.PackageResourceReference; public class StatusBar extends Panel { private static final long serialVersionUID = 1L; + private final String script; + public StatusBar( final String id, final IModel limitModel, @@ -28,18 +27,6 @@ public StatusBar( final IModel progressMessageModel) { super(id); setOutputMarkupId(true); - add( - new Behavior() { - private static final long serialVersionUID = -8058471260136015254L; - - @Override - public void renderHead(Component component, IHeaderResponse response) { - response.render( - CssHeaderItem.forReference( - new PackageResourceReference( - StatusBar.class, "statusbar.css"))); - } - }); WebMarkupContainer usageBar = new WebMarkupContainer("statusBarProgress"); WebMarkupContainer excessBar = new WebMarkupContainer("statusBarExcess"); @@ -61,17 +48,17 @@ public void renderHead(Component component, IHeaderResponse response) { excessPercentage = 0; } - usageBar.add( - new AttributeModifier( - "style", - new Model<>( - "width: " - + usedPercentage - + "px; left: 5px; border-left: inherit;"))); - - String redStyle = - "width: " + excessPercentage + "px; left: " + (5 + usedPercentage) + "px;"; - excessBar.add(new AttributeModifier("style", new Model<>(redStyle))); + this.script = + "" + + "document.getElementsByClassName('statusBarProgress')[0].style.width = '" + + usedPercentage + + "px';\n" + + "document.getElementsByClassName('statusBarExcess')[0].style.width = '" + + excessPercentage + + "px';\n" + + "document.getElementsByClassName('statusBarExcess')[0].style.left = '" + + (5 + usedPercentage) + + "px';"; add(usageBar); add(excessBar); @@ -80,4 +67,13 @@ public void renderHead(Component component, IHeaderResponse response) { // TODO:make the argument models truly dynamic // add(new AjaxSelfUpdatingTimerBehavior(Duration.seconds(5))); } + + @Override + public void renderHead(IHeaderResponse response) { + super.renderHead(response); + response.render( + CssHeaderItem.forReference( + new PackageResourceReference(StatusBar.class, "statusbar.css"))); + response.render(OnLoadHeaderItem.forScript(this.script)); + } } diff --git a/src/web/gwc/src/main/java/org/geoserver/gwc/web/diskquota/statusbar.css b/src/web/gwc/src/main/java/org/geoserver/gwc/web/diskquota/statusbar.css index 019211ced23..a0c4111384b 100644 --- a/src/web/gwc/src/main/java/org/geoserver/gwc/web/diskquota/statusbar.css +++ b/src/web/gwc/src/main/java/org/geoserver/gwc/web/diskquota/statusbar.css @@ -16,6 +16,8 @@ background-color: green; height: 10px; width: 50px; + left: 5px; + border-left: inherit; } .statusBarExcess { diff --git a/src/web/gwc/src/main/java/org/geoserver/gwc/web/layer/GridSubsetsEditor.java b/src/web/gwc/src/main/java/org/geoserver/gwc/web/layer/GridSubsetsEditor.java index 6207004ec08..f5d02bf6e83 100644 --- a/src/web/gwc/src/main/java/org/geoserver/gwc/web/layer/GridSubsetsEditor.java +++ b/src/web/gwc/src/main/java/org/geoserver/gwc/web/layer/GridSubsetsEditor.java @@ -199,10 +199,7 @@ protected void populateItem(final ListItem item) { new PropertyModel<>(item.getModel(), "gridSetName")); if (!gridsetExists) { gridSetLabel.add( - new AttributeModifier( - "style", - new Model<>( - "color:red;text-decoration:line-through;"))); + AttributeModifier.replace("class", "gwc-missing-gridset")); getPage() .warn( "GridSet " diff --git a/src/web/security/core/src/main/java/org/geoserver/security/web/AbstractConfirmRemovalPanel.html b/src/web/security/core/src/main/java/org/geoserver/security/web/AbstractConfirmRemovalPanel.html index b03a7259b20..cc6e654812f 100644 --- a/src/web/security/core/src/main/java/org/geoserver/security/web/AbstractConfirmRemovalPanel.html +++ b/src/web/security/core/src/main/java/org/geoserver/security/web/AbstractConfirmRemovalPanel.html @@ -1,27 +1,10 @@ - - -
    -
    +

    About to remove

    • diff --git a/src/web/security/core/src/main/java/org/geoserver/security/web/SecurityNamedServiceEditPage$ErrorPanel.html b/src/web/security/core/src/main/java/org/geoserver/security/web/SecurityNamedServiceEditPage$ErrorPanel.html index c55bd313382..cac7c3e3433 100644 --- a/src/web/security/core/src/main/java/org/geoserver/security/web/SecurityNamedServiceEditPage$ErrorPanel.html +++ b/src/web/security/core/src/main/java/org/geoserver/security/web/SecurityNamedServiceEditPage$ErrorPanel.html @@ -1,10 +1,4 @@ - - - - - diff --git a/src/web/wms/src/main/java/org/geoserver/wms/web/data/ExternalGraphicPanel.java b/src/web/wms/src/main/java/org/geoserver/wms/web/data/ExternalGraphicPanel.java index d2fa78e3439..1fc6b6949d5 100644 --- a/src/web/wms/src/main/java/org/geoserver/wms/web/data/ExternalGraphicPanel.java +++ b/src/web/wms/src/main/java/org/geoserver/wms/web/data/ExternalGraphicPanel.java @@ -152,7 +152,7 @@ public void onClick(AjaxRequestTarget target, Form form) { height.setOutputMarkupId(true); table.add(height); - table.add(new AttributeModifier("style", showhideStyleModel)); + table.add(AttributeModifier.replace("class", showhideStyleModel)); container.add(table); @@ -279,12 +279,7 @@ protected String getOnlineResource() { } private void updateVisibility(boolean b) { - if (b) { - showhideStyleModel.setObject(""); - } else { - showhideStyleModel.setObject("display:none"); - } - // table.setVisible(b); + showhideStyleModel.setObject(b ? "" : "hidden"); autoFill.setVisible(b); hide.setVisible(b); show.setVisible(!b); diff --git a/src/web/wms/src/main/java/org/geoserver/wms/web/data/StyleEditPage.java b/src/web/wms/src/main/java/org/geoserver/wms/web/data/StyleEditPage.java index 80d4672241c..5c845dbd598 100644 --- a/src/web/wms/src/main/java/org/geoserver/wms/web/data/StyleEditPage.java +++ b/src/web/wms/src/main/java/org/geoserver/wms/web/data/StyleEditPage.java @@ -8,13 +8,12 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.logging.Level; +import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; import org.apache.wicket.WicketRuntimeException; -import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.behavior.Behavior; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.OnLoadHeaderItem; -import org.apache.wicket.model.Model; import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.geoserver.catalog.SLDHandler; @@ -55,9 +54,8 @@ public StyleEditPage(PageParameters parameters) { if (si.getWorkspace() == null) { styleForm.setEnabled(false); - editor.add(new AttributeAppender("class", new Model<>("disabled"), " ")); - get("validate") - .add(new AttributeAppender("style", new Model<>("display:none;"), " ")); + editor.add(AttributeModifier.append("class", "disabled")); + get("validate").add(AttributeModifier.append("class", "hidden")); add( new Behavior() { diff --git a/src/web/wms/src/main/java/org/geoserver/wms/web/data/StylePage.css b/src/web/wms/src/main/java/org/geoserver/wms/web/data/StylePage.css index 3225466c9c5..70a7600fb63 100644 --- a/src/web/wms/src/main/java/org/geoserver/wms/web/data/StylePage.css +++ b/src/web/wms/src/main/java/org/geoserver/wms/web/data/StylePage.css @@ -1,34 +1,34 @@ /* CSS for the StyleAdminPanel. */ -style-data-span { +.style-data-span { width: 45%; margin-right: 5%; margin-bottom: 0px; } -style-legend-span { +.style-legend-span { width: 45%; margin-right: 5%; } -w-27-em { +.w-27-em { width: 27em; } -display-inline-block { +.display-inline-block { display: inline-block; } -bottom-margin-0em { +.bottom-margin-0em { margin-bottom: 0em } -style-button-group { +.style-button-group { margin-top: 0.5em; margin-left: -1px; clear: left; } -top-padding-2em { +.top-padding-2em { padding-top: 2em; -} \ No newline at end of file +} From 17db84cf7d3a16c4bbff71550d8104a88ea963e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:29:57 +0100 Subject: [PATCH 09/43] Bump org.springframework.security:spring-security-web in /src (#7984) Bumps [org.springframework.security:spring-security-web](https://github.com/spring-projects/spring-security) from 5.8.14 to 5.8.15. - [Release notes](https://github.com/spring-projects/spring-security/releases) - [Changelog](https://github.com/spring-projects/spring-security/blob/main/RELEASE.adoc) - [Commits](https://github.com/spring-projects/spring-security/compare/5.8.14...5.8.15) --- updated-dependencies: - dependency-name: org.springframework.security:spring-security-web dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pom.xml b/src/pom.xml index 6d502dfa01c..b161bef9fde 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -93,7 +93,7 @@ 1.27-SNAPSHOT 1.20.0 5.3.39 - 5.8.14 + 5.8.15 5.5.18 2.5.2.RELEASE 3.1.0 From 40ec7dc601bbd076056b6654c948b88f3e1f4946 Mon Sep 17 00:00:00 2001 From: Jody Garnett Date: Mon, 28 Oct 2024 22:56:07 +0100 Subject: [PATCH 10/43] [GEOS-11587] Update mapfish-print-v2 to version 2.3.2 --- src/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pom.xml b/src/pom.xml index b161bef9fde..abed5dfca00 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -124,7 +124,7 @@ false true 3.7-SNAPSHOT - 2.3.1 + 2.3.2 1.9.3 3.6.9.Final 1.1.0 From 693385e3096f80d1e833ac3755ae1f6da4f944d8 Mon Sep 17 00:00:00 2001 From: Jody Garnett Date: Tue, 29 Oct 2024 14:51:47 +0100 Subject: [PATCH 11/43] [GEOS-11588] Use JDBCConfiguration.validateConfiguration to provide feedback --- doc/en/user/source/installation/upgrade.rst | 10 ++++ src/gwc/pom.xml | 6 ++ .../gwc/JDBCConfigurationStorage.java | 4 ++ .../org/geoserver/gwc/GWCIntegrationTest.java | 60 +++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/doc/en/user/source/installation/upgrade.rst b/doc/en/user/source/installation/upgrade.rst index 820e3b70cbb..7f062fb31bd 100644 --- a/doc/en/user/source/installation/upgrade.rst +++ b/doc/en/user/source/installation/upgrade.rst @@ -103,6 +103,16 @@ If the above did not help, then a full cleanup of the GeoServer configuration is #. Recreate the stores and layers using the known procedures. +Disk Quota validation query (GeoServer 2.25.4 and newer) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When using the JDBC Disk Quota: + +* Validation query for ``H2`` is limited to ``SELECT 1``. +* Validation query for ``Oracle`` is limited to ``SELECT 1 FROM DUAL``. +* Validation query for other JDBC formats receive a warning in the logs if is not one of the common examples above. + +.. note:: If you find your JDBC Disk Quota is no longer loaded on startup: check the logs for message about validation query, edit the configuration, and restart. External Entity Allow List default (GeoServer 2.25 and newer) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/gwc/pom.xml b/src/gwc/pom.xml index e07c8768b18..bf0c17f1aaf 100644 --- a/src/gwc/pom.xml +++ b/src/gwc/pom.xml @@ -144,6 +144,12 @@ tests test + + org.geotools.jdbc + gt-jdbc-h2 + ${gt.version} + test + diff --git a/src/gwc/src/main/java/org/geoserver/gwc/JDBCConfigurationStorage.java b/src/gwc/src/main/java/org/geoserver/gwc/JDBCConfigurationStorage.java index 7d8fe4ffab6..33312ed90c2 100644 --- a/src/gwc/src/main/java/org/geoserver/gwc/JDBCConfigurationStorage.java +++ b/src/gwc/src/main/java/org/geoserver/gwc/JDBCConfigurationStorage.java @@ -57,6 +57,7 @@ public synchronized void saveDiskQuotaConfig( throws ConfigurationException, IOException, InterruptedException { Resource configFile = configDir.get("geowebcache-diskquota-jdbc.xml"); if ("JDBC".equals(config.getQuotaStore())) { + JDBCConfiguration.validateConfiguration(jdbcConfig); JDBCConfiguration encrypted = passwordHelper.encryptPassword(jdbcConfig); try (OutputStream os = configFile.out()) { JDBCConfiguration.store(encrypted, os); @@ -97,6 +98,9 @@ public synchronized JDBCConfiguration getJDBCDiskQuotaConfig() */ public void testQuotaConfiguration(JDBCConfiguration jdbcConfiguration) throws ConfigurationException, IOException { + + JDBCConfiguration.validateConfiguration(jdbcConfiguration); + JDBCQuotaStoreFactory factory = GeoServerExtensions.bean(JDBCQuotaStoreFactory.class); QuotaStore qs = null; try { diff --git a/src/gwc/src/test/java/org/geoserver/gwc/GWCIntegrationTest.java b/src/gwc/src/test/java/org/geoserver/gwc/GWCIntegrationTest.java index 7b8796131ee..44470109d4c 100644 --- a/src/gwc/src/test/java/org/geoserver/gwc/GWCIntegrationTest.java +++ b/src/gwc/src/test/java/org/geoserver/gwc/GWCIntegrationTest.java @@ -1389,6 +1389,66 @@ public void testDiskQuotaStorage() throws Exception { JDBCQuotaStoreFactory.ENABLE_HSQL_AUTO_SHUTDOWN = false; } + @Test + public void testDiskQuotaH2Storage() throws Exception { + // normal state, quota is not enabled by default + GWC gwc = GWC.get(); + ConfigurableQuotaStoreProvider provider = + GeoServerExtensions.bean(ConfigurableQuotaStoreProvider.class); + DiskQuotaConfig quota = gwc.getDiskQuotaConfig(); + JDBCConfiguration jdbc = gwc.getJDBCDiskQuotaConfig(); + assertFalse("Disk quota is enabled??", quota.isEnabled()); + assertNull("jdbc quota config should be missing", jdbc); + assertTrue(getActualStore(provider) instanceof DummyQuotaStore); + + GeoServerDataDirectory dd = GeoServerExtensions.bean(GeoServerDataDirectory.class); + String jdbcConfigPath = "gwc/geowebcache-diskquota-jdbc.xml"; + String h2StorePath = "gwc/diskquota_page_store_h2"; + + // now enable it in JDBC mode, with HSQL local storage + quota.setEnabled(true); + quota.setQuotaStore("JDBC"); + jdbc = new JDBCConfiguration(); + jdbc.setDialect("H2"); + ConnectionPoolConfiguration pool = new ConnectionPoolConfiguration(); + pool.setDriver("org.h2.Driver"); + pool.setUrl("jdbc:h2:file:./target/quota-h2"); + pool.setUsername("sa"); + pool.setPassword(""); + pool.setMinConnections(1); + pool.setMaxConnections(1); + pool.setMaxOpenPreparedStatements(50); + jdbc.setConnectionPool(pool); + + pool.setValidationQuery("SELECT 1"); + gwc.saveDiskQuotaConfig(quota, jdbc); + assertNotNull( + "jdbc config (" + jdbcConfigPath + ") should be there", + dd.findFile(jdbcConfigPath)); + assertNull( + "jdbc store (" + h2StorePath + ") should be there", dd.findDataFile(h2StorePath)); + + File newQuotaStore = new File("./target/quota-h2.data.db"); + assertTrue(newQuotaStore.exists()); + try { + pool.setValidationQuery("SELECT 1 FROM DUAL"); + gwc.saveDiskQuotaConfig(quota, jdbc); + fail("Expect configuration due to incorrect validation query used for H2"); + } catch (ConfigurationException expected) { + } + + File jdbcConfigFile = dd.findFile(jdbcConfigPath); + try (FileInputStream fis = new FileInputStream(jdbcConfigFile)) { + Document dom = dom(fis); + // print(dom); + String storedPassword = + XMLUnit.newXpathEngine() + .evaluate("/gwcJdbcConfiguration/connectionPool/password", dom); + // check the password has been encoded properly + assertTrue(storedPassword.startsWith("crypt1:")); + } + } + private QuotaStore getActualStore(ConfigurableQuotaStoreProvider provider) throws ConfigurationException, IOException { return ((ConfigurableQuotaStore) provider.getQuotaStore()).getStore(); From 2fbb244fbd8d5c6e7bc9fe06af41576e023c4cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mitchell=20B=C3=B6secke?= Date: Sat, 2 Mar 2024 08:26:58 -0700 Subject: [PATCH 12/43] [GEOS-11580] Improve embedded GWC meta-tiling performance --- .../source/geowebcache/webadmin/defaults.rst | 9 + .../v1/tiles/VolatileGeoServerTileLayer.java | 4 +- src/gwc/pom.xml | 11 + .../src/main/java/org/geoserver/gwc/GWC.java | 50 +- .../org/geoserver/gwc/config/GWCConfig.java | 17 + .../gwc/layer/GeoServerMetaTile.java | 4 +- .../gwc/layer/GeoServerTileLayer.java | 422 ++++++++++++---- .../test/java/org/geoserver/gwc/GWCTest.java | 27 ++ .../gwc/WmsMetatileBenchmarkTest.java | 456 ++++++++++++++++++ .../gwc/config/GWCConfigPersisterTest.java | 1 + .../geoserver/gwc/config/GWCConfigTest.java | 4 + .../gwc/layer/GeoServerTileLayerTest.java | 299 ++++++++---- src/pom.xml | 1 + .../gwc/web/CachingOptionsPanel.html | 9 + .../gwc/web/CachingOptionsPanel.java | 12 +- .../resources/GeoServerApplication.properties | 2 + .../java/org/geoserver/wms/RasterCleaner.java | 9 +- .../wms/map/MetatileMapOutputFormat.java | 5 +- 18 files changed, 1133 insertions(+), 209 deletions(-) create mode 100644 src/gwc/src/test/java/org/geoserver/gwc/WmsMetatileBenchmarkTest.java diff --git a/doc/en/user/source/geowebcache/webadmin/defaults.rst b/doc/en/user/source/geowebcache/webadmin/defaults.rst index 322c0c9ede4..3f4713ac8c7 100644 --- a/doc/en/user/source/geowebcache/webadmin/defaults.rst +++ b/doc/en/user/source/geowebcache/webadmin/defaults.rst @@ -107,6 +107,15 @@ The disadvantage of metatiling is that at large sizes, memory consumption can be The size of the default metatile can be adjusted here. By default, GeoServer sets a metatile size of **4x4**, which strikes a balance between performance, memory usage, and rendering accuracy. +Metatiling threads +~~~~~~~~~~~~~~~~~~ + +After a metatile (see above) is produced, it is then split into a total of 16 individual tiles to be encoded and saved to the cache. By default, a user requested tile will be encoded and saved on the main request thread but the remaining tiles will be encoded and saved on asynchronous threads to decrease latency experienced by the user. + +Leaving this value blank will use a default thread pool size, equal to 2 times the number of cores. Setting to 0 will disable concurrency and all tiles belonging to the metatile will be encoded/saved on the main request thread. + +This setting only affects user requests and is not used when seeding (seeding will encode an entire metatile on each seeding thread). + Default gutter size ~~~~~~~~~~~~~~~~~~~ diff --git a/src/community/ogcapi/ogcapi-tiles/src/main/java/org/geoserver/ogcapi/v1/tiles/VolatileGeoServerTileLayer.java b/src/community/ogcapi/ogcapi-tiles/src/main/java/org/geoserver/ogcapi/v1/tiles/VolatileGeoServerTileLayer.java index c153ac7562c..44abdfa51a1 100644 --- a/src/community/ogcapi/ogcapi-tiles/src/main/java/org/geoserver/ogcapi/v1/tiles/VolatileGeoServerTileLayer.java +++ b/src/community/ogcapi/ogcapi-tiles/src/main/java/org/geoserver/ogcapi/v1/tiles/VolatileGeoServerTileLayer.java @@ -29,11 +29,11 @@ public VolatileGeoServerTileLayer(GeoServerTileLayer layer) { } @Override - protected ConveyorTile getMetatilingReponse( + protected ConveyorTile getMetatilingResponse( ConveyorTile tile, boolean tryCache, int metaX, int metaY) throws GeoWebCacheException, IOException { // forces meta tiling factors to 1x1 and disables cache usage - return super.getMetatilingReponse(tile, false, 1, 1); + return super.getMetatilingResponse(tile, false, 1, 1); } @Override diff --git a/src/gwc/pom.xml b/src/gwc/pom.xml index bf0c17f1aaf..fa7af572378 100644 --- a/src/gwc/pom.xml +++ b/src/gwc/pom.xml @@ -142,6 +142,17 @@ gt-main ${gt.version} tests + + + org.openjdk.jmh + jmh-core + ${jmh.version} + test + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} test diff --git a/src/gwc/src/main/java/org/geoserver/gwc/GWC.java b/src/gwc/src/main/java/org/geoserver/gwc/GWC.java index 519d4724a98..ace439b92cd 100644 --- a/src/gwc/src/main/java/org/geoserver/gwc/GWC.java +++ b/src/gwc/src/main/java/org/geoserver/gwc/GWC.java @@ -19,6 +19,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.google.common.collect.Sets.SetView; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.io.IOException; import java.lang.reflect.Proxy; import java.net.URL; @@ -38,6 +39,7 @@ import java.util.Properties; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.*; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.http.Cookie; @@ -220,6 +222,8 @@ public class GWC implements DisposableBean, InitializingBean, ApplicationContext private BlobStoreAggregator blobStoreAggregator; + private ExecutorService metaTilingExecutor; + /** * Constructor for the GWC mediator * @@ -273,6 +277,8 @@ public GWC( this.jdbcConfigurationStorage = jdbcConfigurationStorage; this.blobStoreAggregator = blobStoreAggregator; this.gwcSynchEnv = gwcSynchEnv; + + this.metaTilingExecutor = buildMetaTilingExecutor(getConfig().getMetaTilingThreads()); } /** Updates the configurable lock provider to use the specified bean */ @@ -300,6 +306,29 @@ private void updateLockProvider(String lockProviderName) { lockProvider.setDelegate(delegate); } + private ExecutorService buildMetaTilingExecutor(Integer metaTilingThreads) { + ThreadFactory threadFactory = + new ThreadFactoryBuilder().setNameFormat("GWC MetaTiling Thread-%d").build(); + + if (metaTilingThreads == null) { + metaTilingThreads = Runtime.getRuntime().availableProcessors() * 2; + } + + if (metaTilingThreads == 0) { + return null; + } + ThreadPoolExecutor executor = + new ThreadPoolExecutor( + metaTilingThreads, + metaTilingThreads, + 10, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + threadFactory); + executor.allowCoreThreadTimeOut(true); + return executor; + } + /** * Gets the singleton instance of the GeoServer GWC environment synchronizer * @@ -351,6 +380,9 @@ public void destroy() throws Exception { if (this.catalogStyleChangeListener != null) { catalog.removeListener(this.catalogStyleChangeListener); } + if (this.metaTilingExecutor != null) { + this.metaTilingExecutor.shutdownNow(); + } GWC.set(null, null); } @@ -1235,6 +1267,12 @@ public void saveConfig(GWCConfig gwcConfig) throws IOException { // make sure we switch to the lock provider just configured updateLockProvider(gwcConfig.getLockProviderName()); + + // Reconfigure the metatiling executor because the thread count might have changed + if (this.metaTilingExecutor != null) { + this.metaTilingExecutor.shutdown(); + } + this.metaTilingExecutor = buildMetaTilingExecutor(gwcConfig.getMetaTilingThreads()); } public void saveDiskQuotaConfig(DiskQuotaConfig config, JDBCConfiguration jdbcConfig) @@ -1366,14 +1404,16 @@ public Resource dispatchOwsRequest(final Map params, Cookie[] co try { owsDispatcher.handleRequest(req, resp); } finally { - // reset the old request + // reset thread locals + tx.apply(); + + // reset the old request (after all other thread locals to ensure + // it won't get overriden by the ThreadLocalsTransfer). if (request != null) { Dispatcher.REQUEST.set(request); } else { Dispatcher.REQUEST.remove(); } - // reset thread locals - tx.apply(); } return new ByteArrayResource(resp.getBytes()); } @@ -2373,6 +2413,10 @@ public LockProvider getLockProvider() { return lockProvider; } + public Executor getMetaTilingExecutor() { + return metaTilingExecutor; + } + public JDBCConfiguration getJDBCDiskQuotaConfig() throws IOException, org.geowebcache.config.ConfigurationException { return jdbcConfigurationStorage.getJDBCDiskQuotaConfig(); diff --git a/src/gwc/src/main/java/org/geoserver/gwc/config/GWCConfig.java b/src/gwc/src/main/java/org/geoserver/gwc/config/GWCConfig.java index 1b4793a9272..c1f9dedb153 100644 --- a/src/gwc/src/main/java/org/geoserver/gwc/config/GWCConfig.java +++ b/src/gwc/src/main/java/org/geoserver/gwc/config/GWCConfig.java @@ -72,6 +72,9 @@ public class GWCConfig implements Cloneable, Serializable { /** Default meta-tiling factor for the Y axis */ private int metaTilingY; + /** Number of threads available for concurrent encoding/saving of tiles within a meta-tile */ + private Integer metaTilingThreads; + /** Default gutter size in pixels */ private int gutter; @@ -273,12 +276,16 @@ public GWCConfig saneConfig() { if (!defaultVectorCacheFormats.isEmpty()) { sane.setDefaultVectorCacheFormats(defaultVectorCacheFormats); } + if (metaTilingThreads != null && metaTilingThreads > 0) { + sane.setMetaTilingThreads(metaTilingThreads); + } return sane; } public boolean isSane() { return metaTilingX > 0 && metaTilingY > 0 + && (metaTilingThreads == null || metaTilingThreads >= 0) && gutter >= 0 && !defaultCachingGridSetIds.isEmpty() && !defaultCoverageCacheFormats.isEmpty() @@ -337,6 +344,14 @@ public void setMetaTilingY(int metaFactorY) { this.metaTilingY = metaFactorY; } + public Integer getMetaTilingThreads() { + return metaTilingThreads; + } + + public void setMetaTilingThreads(Integer metaTilingThreads) { + this.metaTilingThreads = metaTilingThreads; + } + public int getGutter() { return gutter; } @@ -396,6 +411,7 @@ public boolean equals(Object o) { && cacheNonDefaultStyles == gwcConfig.cacheNonDefaultStyles && metaTilingX == gwcConfig.metaTilingX && metaTilingY == gwcConfig.metaTilingY + && Objects.equals(metaTilingThreads, gwcConfig.metaTilingThreads) && gutter == gwcConfig.gutter && Objects.equals(version, gwcConfig.version) && Objects.equals(WMTSEnabled, gwcConfig.WMTSEnabled) @@ -428,6 +444,7 @@ public int hashCode() { cacheNonDefaultStyles, metaTilingX, metaTilingY, + metaTilingThreads, gutter, defaultCachingGridSetIds, defaultCoverageCacheFormats, diff --git a/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerMetaTile.java b/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerMetaTile.java index 165cc32edbb..89eb052cf30 100644 --- a/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerMetaTile.java +++ b/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerMetaTile.java @@ -7,7 +7,6 @@ import static com.google.common.base.Preconditions.checkNotNull; -import it.geosolutions.jaiext.BufferedImageAdapter; import java.awt.Point; import java.awt.Rectangle; import java.awt.image.BufferedImage; @@ -213,8 +212,7 @@ public RenderedImage createTile( break; case 2: final BufferedImage image = (BufferedImage) metaTileImage; - final BufferedImage subimage = image.getSubimage(x, y, tileWidth, tileHeight); - tile = new BufferedImageAdapter(subimage); + tile = image.getSubimage(x, y, tileWidth, tileHeight); break; default: throw new IllegalStateException( diff --git a/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerTileLayer.java b/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerTileLayer.java index d69ed12aa5e..1a21d562ffe 100644 --- a/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerTileLayer.java +++ b/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerTileLayer.java @@ -13,41 +13,24 @@ import com.google.common.base.Throwables; import com.google.common.collect.Iterables; -import java.awt.Dimension; +import java.awt.*; import java.io.IOException; import java.lang.reflect.Proxy; import java.net.MalformedURLException; import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; +import java.util.*; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; -import org.geoserver.catalog.Catalog; -import org.geoserver.catalog.FeatureTypeInfo; -import org.geoserver.catalog.KeywordInfo; -import org.geoserver.catalog.LayerGroupInfo; -import org.geoserver.catalog.LayerInfo; -import org.geoserver.catalog.MetadataLinkInfo; -import org.geoserver.catalog.MetadataMap; -import org.geoserver.catalog.PublishedInfo; -import org.geoserver.catalog.PublishedType; -import org.geoserver.catalog.ResourceInfo; -import org.geoserver.catalog.ResourcePool; -import org.geoserver.catalog.StyleInfo; -import org.geoserver.catalog.WorkspaceInfo; +import org.geoserver.catalog.*; import org.geoserver.catalog.impl.ModificationProxy; import org.geoserver.config.GeoServer; import org.geoserver.gwc.GWC; @@ -61,10 +44,7 @@ import org.geoserver.rest.RequestInfo; import org.geoserver.util.DimensionWarning; import org.geoserver.util.HTTPWarningAppender; -import org.geoserver.wms.GetLegendGraphicRequest; -import org.geoserver.wms.GetMapRequest; -import org.geoserver.wms.WMS; -import org.geoserver.wms.WebMap; +import org.geoserver.wms.*; import org.geoserver.wms.capabilities.CapabilityUtil; import org.geoserver.wms.capabilities.LegendSample; import org.geotools.api.feature.type.FeatureType; @@ -83,30 +63,17 @@ import org.geowebcache.filter.parameters.ParameterException; import org.geowebcache.filter.parameters.ParameterFilter; import org.geowebcache.filter.request.RequestFilter; -import org.geowebcache.grid.BoundingBox; -import org.geowebcache.grid.GridSet; -import org.geowebcache.grid.GridSetBroker; -import org.geowebcache.grid.GridSubset; -import org.geowebcache.grid.OutsideCoverageException; -import org.geowebcache.grid.SRS; +import org.geowebcache.grid.*; +import org.geowebcache.io.ByteArrayResource; import org.geowebcache.io.Resource; -import org.geowebcache.layer.ExpirationRule; -import org.geowebcache.layer.LayerListenerList; -import org.geowebcache.layer.MetaTile; -import org.geowebcache.layer.ProxyLayer; -import org.geowebcache.layer.TileJSONProvider; -import org.geowebcache.layer.TileLayer; -import org.geowebcache.layer.TileLayerListener; -import org.geowebcache.layer.meta.ContactInformation; -import org.geowebcache.layer.meta.LayerMetaInformation; -import org.geowebcache.layer.meta.MetadataURL; -import org.geowebcache.layer.meta.TileJSON; -import org.geowebcache.layer.meta.VectorLayerMetadata; +import org.geowebcache.layer.*; +import org.geowebcache.layer.meta.*; import org.geowebcache.layer.updatesource.UpdateSourceDefinition; import org.geowebcache.locks.LockProvider.Lock; import org.geowebcache.mime.FormatModifier; import org.geowebcache.mime.MimeException; import org.geowebcache.mime.MimeType; +import org.geowebcache.storage.TileObject; import org.geowebcache.util.GWCVars; import org.geowebcache.util.ServletUtils; import org.locationtech.jts.geom.Envelope; @@ -296,8 +263,8 @@ public void resetParameterFilters() { *
    • The layer is not errored ({@link #getConfigErrorMessage() == null} *
    * - * The layer is enabled by configuration if: the {@code GWC.enabled} metadata property is set to - * {@code true} in it's corresponding {@link LayerInfo} or {@link LayerGroupInfo} {@link + *

    The layer is enabled by configuration if: the {@code GWC.enabled} metadata property is set + * to {@code true} in it's corresponding {@link LayerInfo} or {@link LayerGroupInfo} {@link * MetadataMap}, or there's no {@code GWC.enabled} property set at all but the global {@link * GWCConfig#isCacheLayersByDefault()} is {@code true}. * @@ -555,7 +522,7 @@ public ConveyorTile getTile(ConveyorTile tile) metaX = metaY = 1; } - ConveyorTile returnTile = getMetatilingReponse(tile, true, metaX, metaY); + ConveyorTile returnTile = getMetatilingResponse(tile, true, metaX, metaY); sendTileRequestedEvent(returnTile); @@ -579,27 +546,49 @@ protected final void sendTileRequestedEvent(ConveyorTile tile) { } } - protected ConveyorTile getMetatilingReponse( - ConveyorTile tile, final boolean tryCache, final int metaX, final int metaY) + protected ConveyorTile getMetatilingResponse( + ConveyorTile conveyorTile, final boolean tryCache, final int metaX, final int metaY) throws GeoWebCacheException, IOException { - if (tryCache && tryCacheFetch(tile)) { - return finalizeTile(tile); + if (tryCache && tryCacheFetch(conveyorTile)) { + return finalizeTile(conveyorTile); } - final GeoServerMetaTile metaTile = createMetaTile(tile, metaX, metaY); - Lock lock = null; + final GeoServerMetaTile metaTile = createMetaTile(conveyorTile, metaX, metaY); + + /* ****************** Acquire lock on metatile ******************* */ + final Lock metaTileLock = + GWC.get().getLockProvider().getLock(buildMetaTileLockKey(conveyorTile, metaTile)); + try { - /* ****************** Acquire lock ******************* */ - lock = GWC.get().getLockProvider().getLock(buildLockKey(tile, metaTile)); - // got the lock on the meta tile, try again - if (tryCache && tryCacheFetch(tile)) { - LOGGER.finest( - "--> " - + Thread.currentThread().getName() - + " returns cache hit for " - + Arrays.toString(metaTile.getMetaGridPos())); - } else { + boolean foundInCache = false; + if (tryCache) { + /* ****************** Acquire lock on individual tile ******************* */ + // Will block here if there is an async thread currently saving this tile + final Lock tileLock = + GWC.get() + .getLockProvider() + .getLock( + buildTileLockKey( + conveyorTile, conveyorTile.getTileIndex())); + try { + // After getting the lock on the meta tile and individual tile, try cache again + foundInCache = tryCacheFetch(conveyorTile); + if (foundInCache) { + LOGGER.finest( + "--> " + + Thread.currentThread().getName() + + " returns cache hit for " + + Arrays.toString(metaTile.getMetaGridPos())); + metaTile.dispose(); + } + } finally { + /* ****************** Release lock on individual tile ******************* */ + tileLock.release(); + } + } + + if (!foundInCache) { LOGGER.finer( "--> " + Thread.currentThread().getName() @@ -610,26 +599,246 @@ protected ConveyorTile getMetatilingReponse( WebMap map; try { long requestTime = System.currentTimeMillis(); - map = dispatchGetMap(tile, metaTile); + + // Actually fetch the metatile data + map = dispatchGetMap(conveyorTile, metaTile); + checkNotNull(map, "Did not obtain a WebMap from GeoServer's Dispatcher"); metaTile.setWebMap(map); - setupCachingStrategy(tile); - saveTiles(metaTile, tile, requestTime); + setupCachingStrategy(conveyorTile); + + final long[][] gridPositions = metaTile.getTilesGridPositions(); + final long[] gridLoc = conveyorTile.getTileIndex(); + final GridSubset gridSubset = getGridSubset(conveyorTile.getGridSetId()); + final int numberOfTiles = gridPositions.length; + + final int zoomLevel = (int) gridLoc[2]; + final boolean store = + this.getExpireCache(zoomLevel) != GWCVars.CACHE_DISABLE_CACHE; + + Executor executor = GWC.get().getMetaTilingExecutor(); + + if (Dispatcher.REQUEST.get() == null) { + // Metatiling concurrency is disabled if this isn't a user request. + // Concurrency reduces the user-experienced latency but isn't + // useful for seeding. In fact, it would be harmful for seeding + // because it makes it more difficult for an administrator to + // control the amount of resource usage for significant seeding jobs. + executor = null; + } + List> completableFutures = new ArrayList<>(); + + // A latch to track whether we've locked all the individual tiles or not, before + // we can release the metatile lock. + CountDownLatch tileLockLatch = new CountDownLatch(numberOfTiles); + + for (int tileIndex = 0; tileIndex < numberOfTiles; tileIndex++) { + final long[] gridPos = gridPositions[tileIndex]; + final int finalTileIndex = tileIndex; + + boolean isConveyorTile = Arrays.equals(gridLoc, gridPos); + + if (isConveyorTile || store) { + if (!gridSubset.covers(gridPos)) { + // edge tile outside coverage, do not store it + tileLockLatch.countDown(); + continue; + } + + Supplier encodeTileTask = encodeTileTask(metaTile, tileIndex); + + if (isConveyorTile) { + // Always encode the conveyor tile on the main thread + Resource resource = encodeTileTask.get(); + conveyorTile.setBlob(resource); + + // Saving the conveyor tile in the cache can either happen + // asynchronously or on the main thread + Runnable saveTileTask = + withTileLock( + conveyorTile, + tileLockLatch, + gridPos, + saveTileTask( + metaTile, + tileIndex, + conveyorTile, + resource, + requestTime)); + if (executor != null) { + CompletableFuture completableFuture = + CompletableFuture.runAsync(saveTileTask, executor); + completableFutures.add(completableFuture); + } else { + // Save in cache on main thread if there's no executor + saveTileTask.run(); + } + } else { + + // For all other tiles, either encode/save fully asynchronously or + // fully on the main thread + Runnable encodeAndSaveTask = + withTileLock( + conveyorTile, + tileLockLatch, + gridPos, + withRasterCleaner( + () -> { + Resource resource = + encodeTileTask.get(); + saveTileTask( + metaTile, + finalTileIndex, + conveyorTile, + resource, + requestTime) + .run(); + })); + + if (executor != null) { + // Fully asynchronous + CompletableFuture completableFuture = + CompletableFuture.runAsync(encodeAndSaveTask, executor); + completableFutures.add(completableFuture); + } else { + // Run on main thread if there's no executor + encodeAndSaveTask.run(); + } + } + } + } + + // Wait until we've obtained locks on all individual tiles before proceeding + tileLockLatch.await(); + + // Dispose of meta-tile when all completable futures are done + if (!completableFutures.isEmpty()) { + runAsyncAfterAllFuturesComplete( + completableFutures, metaTile::dispose, executor); + } else { + // There were no asynchronous tasks, everything was run on the main thread + // so we can dispose of the meta-tile right away + metaTile.dispose(); + } + } catch (Exception e) { Throwables.throwIfInstanceOf(e, GeoWebCacheException.class); throw new GeoWebCacheException("Problem communicating with GeoServer", e); } } - /* ****************** Return lock and response ****** */ + } finally { - if (lock != null) { - lock.release(); - } - metaTile.dispose(); + /* ****************** Release lock on metatile ******************* */ + metaTileLock.release(); } - return finalizeTile(tile); + return finalizeTile(conveyorTile); + } + + private void runAsyncAfterAllFuturesComplete( + List> futures, Runnable runnable, Executor executor) { + CompletableFuture[] futureArray = futures.toArray(new CompletableFuture[0]); + CompletableFuture afterAllFutures = CompletableFuture.allOf(futureArray); + afterAllFutures.thenRunAsync(runnable, executor); + } + + private Runnable withRasterCleaner(Runnable runnable) { + return () -> { + try { + runnable.run(); + } finally { + // Raster cleaner normally runs as a dispatcher callback but in this case + // there is no dispatcher request on this thread. Neglecting to cleanup + // the images from the various RenderedImageMapResponse implementations + // would cause a severe memory leak. + RasterCleaner.cleanup(); + } + }; + } + + /** + * Locks a tile before running the runnable and releases the lock at the end. + * + *

    Also counts down the latch to track how many locks have been acquired. + */ + private Runnable withTileLock( + ConveyorTile conveyorTile, + CountDownLatch tileLockLatch, + long[] gridPosition, + Runnable runnable) { + return () -> { + try { + Lock tileLock = + GWC.get() + .getLockProvider() + .getLock(buildTileLockKey(conveyorTile, gridPosition)); + try { + tileLockLatch.countDown(); + runnable.run(); + } finally { + tileLock.release(); + } + } catch (GeoWebCacheException ex) { + throw new RuntimeException(ex); + } + }; + } + + /** Creates a task for encoding a single tile */ + private Supplier encodeTileTask(GeoServerMetaTile metaTile, int tileIndex) { + return () -> { + ByteArrayResource resource = new ByteArrayResource(16 * 1024); + + boolean completed; + try { + completed = metaTile.writeTileToStream(tileIndex, resource); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Unable to write image tile to ByteArrayOutputStream", e); + throw new RuntimeException(e); + } + + if (!completed) { + LOGGER.severe("metaTile.writeTileToStream returned false, no tiles saved"); + } + return resource; + }; + } + + /** Creates a task for saving a single tile to the cache. */ + private Runnable saveTileTask( + GeoServerMetaTile metaTile, + int tileIndex, + ConveyorTile tileProto, + Resource resource, + long requestTime) { + return () -> { + try { + final long[][] gridPositions = metaTile.getTilesGridPositions(); + long[] gridPosition = gridPositions[tileIndex]; + long[] idx = {gridPosition[0], gridPosition[1], gridPosition[2]}; + + TileObject tile = + TileObject.createCompleteTileObject( + this.getName(), + idx, + tileProto.getGridSetId(), + tileProto.getMimeType().getFormat(), + tileProto.getParameters(), + resource); + tile.setCreated(requestTime); + + // Save tile to storage + if (tileProto.isMetaTileCacheOnly()) { + tileProto.getStorageBroker().putTransient(tile); + } else { + tileProto.getStorageBroker().put(tile); + } + tileProto.getStorageObject().setCreated(tile.getCreated()); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + }; } /** @@ -655,30 +864,45 @@ private void setupCachingStrategy(ConveyorTile tile) { } } - private String buildLockKey(ConveyorTile tile, GeoServerMetaTile metaTile) { - StringBuilder metaKey = new StringBuilder(); + /** + * Builds a unique string for a given metatile. + * + * @param tilePrototype A ConveyorTile that has all the metadata we require. + * @param metaTile The actual metatile we are generating a unique key for. + */ + private String buildMetaTileLockKey(ConveyorTile tilePrototype, GeoServerMetaTile metaTile) { + return buildLockKey(tilePrototype, "gwc_metatile_", metaTile.getMetaGridPos()); + } - final long[] tileIndex; - if (metaTile != null) { - tileIndex = metaTile.getMetaGridPos(); - metaKey.append("gsmeta_"); - } else { - tileIndex = tile.getTileIndex(); - metaKey.append("tile_"); - } - long x = tileIndex[0]; - long y = tileIndex[1]; - long z = tileIndex[2]; + /** + * Builds a unique string for a given tile. + * + * @param tilePrototype A ConveyorTile that has all the metadata we require by may not be the + * actual tile we need a key for. + * @param gridPosition The grid position of the ACTUAL tile we need a key for. + */ + private String buildTileLockKey(ConveyorTile tilePrototype, long[] gridPosition) { + return buildLockKey(tilePrototype, "gwc_tile_", gridPosition); + } + + private String buildLockKey(ConveyorTile tilePrototype, String prefix, long[] position) { + StringBuilder lockKey = new StringBuilder(); + + lockKey.append(prefix); + + long x = position[0]; + long y = position[1]; + long z = position[2]; - metaKey.append(tile.getLayerId()); - metaKey.append("_").append(tile.getGridSetId()); - metaKey.append("_").append(x).append("_").append(y).append("_").append(z); - if (tile.getParametersId() != null) { - metaKey.append("_").append(tile.getParametersId()); + lockKey.append(tilePrototype.getLayerId()); + lockKey.append("_").append(tilePrototype.getGridSetId()); + lockKey.append("_").append(x).append("_").append(y).append("_").append(z); + if (tilePrototype.getParametersId() != null) { + lockKey.append("_").append(tilePrototype.getParametersId()); } - metaKey.append(".").append(tile.getMimeType().getFileExtension()); + lockKey.append(".").append(tilePrototype.getMimeType().getFileExtension()); - return metaKey.toString(); + return lockKey.toString(); } private WebMap dispatchGetMap(final ConveyorTile tile, final MetaTile metaTile) @@ -825,7 +1049,7 @@ private void setTileIndexHeader(ConveyorTile tile) { @Override public ConveyorTile getNoncachedTile(ConveyorTile tile) throws GeoWebCacheException { try { - return getMetatilingReponse(tile, false, 1, 1); + return getMetatilingResponse(tile, false, 1, 1); } catch (IOException e) { throw new GeoWebCacheException(e); } @@ -834,7 +1058,7 @@ public ConveyorTile getNoncachedTile(ConveyorTile tile) throws GeoWebCacheExcept @Override public ConveyorTile doNonMetatilingRequest(ConveyorTile tile) throws GeoWebCacheException { try { - return getMetatilingReponse(tile, true, 1, 1); + return getMetatilingResponse(tile, true, 1, 1); } catch (IOException e) { throw new GeoWebCacheException(e); } @@ -862,7 +1086,7 @@ public void seedTile(ConveyorTile tile, boolean tryCache) if (!tile.getMimeType().supportsTiling()) { metaX = metaY = 1; } - getMetatilingReponse(tile, tryCache, metaX, metaY); + getMetatilingResponse(tile, tryCache, metaX, metaY); } /** @see org.geowebcache.layer.TileLayer#getGridSubsets() */ @@ -1181,9 +1405,9 @@ public List getMimeTypes() { * underlying {@link LayerInfo} or {@link LayerGroupInfo} This calculation can be overridden by * setting {@link GeoServerTileLayerInfo#setExpireClients(int)} * - * @see org.geowebcache.layer.TileLayer#getExpireClients(int) * @param zoomLevel ignored * @return the expiration time + * @see org.geowebcache.layer.TileLayer#getExpireClients(int) */ @Override public int getExpireClients(int zoomLevel) { @@ -1268,9 +1492,9 @@ private int getCacheMaxAge(MetadataMap metadata) { * GeoServerTileLayerInfo#getExpireCacheList()}. If no matching rules are found, defaults to * {@link GeoServerTileLayerInfo#getExpireCache()} * - * @see org.geowebcache.layer.TileLayer#getExpireCache(int) * @param zoomLevel the zoom level used to filter expiration rules * @return the expiration time for tiles at the given zoom level + * @see org.geowebcache.layer.TileLayer#getExpireCache(int) */ @Override public int getExpireCache(int zoomLevel) { diff --git a/src/gwc/src/test/java/org/geoserver/gwc/GWCTest.java b/src/gwc/src/test/java/org/geoserver/gwc/GWCTest.java index 61796e825c3..f9f409e39b3 100644 --- a/src/gwc/src/test/java/org/geoserver/gwc/GWCTest.java +++ b/src/gwc/src/test/java/org/geoserver/gwc/GWCTest.java @@ -66,6 +66,7 @@ import java.util.Optional; import java.util.Properties; import java.util.Set; +import java.util.concurrent.ThreadPoolExecutor; import java.util.stream.Collectors; import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.io.FileUtils; @@ -1634,6 +1635,32 @@ public void testGWCEnvParametrization() { } } + @Test + public void testBuildMetatilingExecutor() throws IOException { + + // If user provides thread count, we expect that to be obeyed + GWCConfig newConfig = new GWCConfig(); + newConfig.setMetaTilingThreads(12); + mediator.saveConfig(newConfig); + assertNotNull(mediator.getMetaTilingExecutor()); + assertEquals(12, ((ThreadPoolExecutor) mediator.getMetaTilingExecutor()).getCorePoolSize()); + + // If thread count is null, we expect a default + newConfig = new GWCConfig(); + newConfig.setMetaTilingThreads(null); + mediator.saveConfig(newConfig); + assertNotNull(mediator.getMetaTilingExecutor()); + assertEquals( + Runtime.getRuntime().availableProcessors() * 2, + ((ThreadPoolExecutor) mediator.getMetaTilingExecutor()).getCorePoolSize()); + + // If thread count is 0, we expect a null executor + newConfig = new GWCConfig(); + newConfig.setMetaTilingThreads(0); + mediator.saveConfig(newConfig); + assertNull(mediator.getMetaTilingExecutor()); + } + @AfterClass public static void destroyAppContext() { GeoServerExtensionsHelper.init(null); diff --git a/src/gwc/src/test/java/org/geoserver/gwc/WmsMetatileBenchmarkTest.java b/src/gwc/src/test/java/org/geoserver/gwc/WmsMetatileBenchmarkTest.java new file mode 100644 index 00000000000..fcf245d1653 --- /dev/null +++ b/src/gwc/src/test/java/org/geoserver/gwc/WmsMetatileBenchmarkTest.java @@ -0,0 +1,456 @@ +/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved + * (c) 2001 - 2013 OpenPlans + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.gwc; + +import static org.geoserver.data.test.MockData.BASIC_POLYGONS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.junit.Assert.assertEquals; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import javax.servlet.ServletResponse; +import org.ehcache.impl.internal.concurrent.ConcurrentHashMap; +import org.geoserver.gwc.config.GWCConfig; +import org.geoserver.test.GeoServerSystemTestSupport; +import org.geowebcache.grid.BoundingBox; +import org.geowebcache.grid.GridSubset; +import org.geowebcache.layer.TileLayer; +import org.junit.Ignore; +import org.junit.Test; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.results.format.ResultFormatType; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.springframework.mock.web.MockHttpServletResponse; + +public class WmsMetatileBenchmarkTest extends GeoServerSystemTestSupport { + + static final String LAYER_NAME = + BASIC_POLYGONS.getPrefix() + ":" + BASIC_POLYGONS.getLocalPart(); + + /** + * This isn't a real test. It's a function that is easy to run a profiler against. The JMH + * benchmark isn't appropriate to run a profiler against because of the way it forks benchmarks + * in a separate JVM. + */ + @Test + @Ignore + public void profileBenchmark() throws Exception { + + GWC.get().getConfig().setDirectWMSIntegrationEnabled(true); + + GWCConfig config = GWC.get().getConfig(); + config.setMetaTilingThreads(2 * Runtime.getRuntime().availableProcessors()); + GWC.get().saveConfig(config); + + long[][] uniqueMetaTileIndices = getTileIndices(LAYER_NAME, null, 10, 1000, 4, 1); + + for (long[] metaTileIndex : uniqueMetaTileIndices) { + + String request = buildGetMap(LAYER_NAME, metaTileIndex); + MockHttpServletResponse response = getAsServletResponse(request); + + assertEquals(200, response.getStatus()); + assertEquals("image/png", response.getContentType()); + assertThat(response.getHeader("geowebcache-cache-result"), equalToIgnoringCase("MISS")); + } + } + + /** + * Runs the JMH benchmark. This isn't a really test so it includes the @Ignore annotation; by + * integrating JMH with Junit it just provides us an easy way to run the benchmark (typically + * through the IDE). + */ + @Test + @Ignore + public void runBenchmark() throws Exception { + + Options options = + new OptionsBuilder() + .include(WmsMetatileBenchmark.class.getSimpleName() + ".*") + .result("./target/benchmark-results.json") + .resultFormat(ResultFormatType.JSON) + .build(); + new Runner(options).run(); + } + + @BenchmarkMode(Mode.Throughput) + @Fork(1) + @Threads(4) + @Warmup(iterations = 2, time = 1) + @Measurement(time = 1) + public static class WmsMetatileBenchmark { + + @State(Scope.Benchmark) + public static class AbstractBenchmarkState { + + long[][] tileIndices; + + AtomicInteger currentIndex = new AtomicInteger(0); + + GeoServerSystemTestSupport geoServerSystemTestSupport = + new GeoServerSystemTestSupport(); + + // Track how many cache hits we get just to help validate correctness of our benchmark + Map cacheHitRate = new ConcurrentHashMap<>(); + + @Setup + public void setup() throws Exception { + geoServerSystemTestSupport.doSetup(); + } + + protected void setMetaTilingThreads(int metaTilingThreads) throws IOException { + GWCConfig config = GWC.get().getConfig(); + config.setMetaTilingThreads(metaTilingThreads); + GWC.get().saveConfig(config); + } + + @TearDown + public void tearDown() throws Exception { + + GeoServerSystemTestSupport.doTearDownClass(); + + // Print information about cache rate just to help validate correctness + int cacheHits = cacheHitRate.getOrDefault("HIT", 0); + int cacheMisses = cacheHitRate.getOrDefault("MISS", 0); + int totalRequests = cacheHits + cacheMisses; + double cacheHitRate = (double) cacheHits / totalRequests; + System.out.println("Total requests: " + totalRequests); + System.out.println("Cache hits: " + cacheHits); + System.out.println("Cache misses: " + cacheMisses); + System.out.println("Cache hit rate: " + cacheHitRate); + } + } + + public static class AbstractGwcWithConcurrency extends AbstractBenchmarkState { + @Override + public void setup() throws Exception { + super.setup(); + GWC.get().getConfig().setDirectWMSIntegrationEnabled(true); + setMetaTilingThreads(2 * Runtime.getRuntime().availableProcessors()); + } + } + + public static class AbstractGwcWithoutConcurrency extends AbstractBenchmarkState { + @Override + public void setup() throws Exception { + super.setup(); + GWC.get().getConfig().setDirectWMSIntegrationEnabled(true); + setMetaTilingThreads(0); + } + } + + public static class GwcWithConcurrencyAndNoCacheHitsState + extends AbstractGwcWithConcurrency { + + public void setup() throws Exception { + super.setup(); + tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 1); + WmsMetatileBenchmarkTest.saveToCsv("./target/all-misses.csv", tileIndices); + } + } + + public static class GwcWithoutConcurrencyAndNoCacheHitsState + extends AbstractGwcWithoutConcurrency { + + public void setup() throws Exception { + super.setup(); + tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 1); + WmsMetatileBenchmarkTest.saveToCsv("./target/all-misses.csv", tileIndices); + } + } + + public static class GwcWithConcurrencyAnd50PercentCacheHitsState + extends AbstractGwcWithConcurrency { + + public void setup() throws Exception { + super.setup(); + tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 2); + WmsMetatileBenchmarkTest.saveToCsv("./target/50-percent-hits.csv", tileIndices); + } + } + + public static class GwcWithoutConcurrencyAnd50PercentCacheHitsState + extends AbstractGwcWithoutConcurrency { + + public void setup() throws Exception { + super.setup(); + tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 2); + WmsMetatileBenchmarkTest.saveToCsv("./target/50-percent-hits.csv", tileIndices); + } + } + + public static class GwcWithConcurrencyAnd75PercentCacheHitsState + extends AbstractGwcWithConcurrency { + + public void setup() throws Exception { + super.setup(); + tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 4); + WmsMetatileBenchmarkTest.saveToCsv("./target/75-percent-hits.csv", tileIndices); + } + } + + public static class GwcWithoutConcurrencyAnd75PercentCacheHitsState + extends AbstractGwcWithoutConcurrency { + + public void setup() throws Exception { + super.setup(); + tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 4); + WmsMetatileBenchmarkTest.saveToCsv("./target/75-percent-hits.csv", tileIndices); + } + } + + public static class GwcWithConcurrencyAnd90PercentCacheHitsState + extends AbstractGwcWithConcurrency { + + public void setup() throws Exception { + super.setup(); + tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 16); + WmsMetatileBenchmarkTest.saveToCsv("./target/90-percent-hits.csv", tileIndices); + } + } + + public static class GwcWithoutConcurrencyAnd90PercentCacheHitsState + extends AbstractGwcWithoutConcurrency { + + public void setup() throws Exception { + super.setup(); + tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 16); + WmsMetatileBenchmarkTest.saveToCsv("./target/90-percent-hits.csv", tileIndices); + } + } + + public static class NoGwcState extends AbstractBenchmarkState { + + public void setup() throws Exception { + super.setup(); + GWC.get().getConfig().setDirectWMSIntegrationEnabled(false); + tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 1); + } + } + + @Benchmark + public ServletResponse runWithGwcWithConcurrencyAndNoCacheHits( + GwcWithConcurrencyAndNoCacheHitsState state) throws Exception { + return run(state); + } + + @Benchmark + public ServletResponse runWithGwcWithoutConcurrencyAndNoCacheHits( + GwcWithoutConcurrencyAndNoCacheHitsState state) throws Exception { + return run(state); + } + + @Benchmark + public ServletResponse runWithGwcWithConcurrencyAnd50PercentCacheHits( + GwcWithConcurrencyAnd50PercentCacheHitsState state) throws Exception { + return run(state); + } + + @Benchmark + public ServletResponse runWithGwcWithoutConcurrencyAnd50PercentCacheHits( + GwcWithoutConcurrencyAnd50PercentCacheHitsState state) throws Exception { + return run(state); + } + + @Benchmark + public ServletResponse runWithGwcWithConcurrencyAnd75PercentCacheHits( + GwcWithConcurrencyAnd75PercentCacheHitsState state) throws Exception { + return run(state); + } + + @Benchmark + public ServletResponse runWithGwcWithoutConcurrencyAnd75PercentCacheHits( + GwcWithoutConcurrencyAnd75PercentCacheHitsState state) throws Exception { + return run(state); + } + + @Benchmark + public ServletResponse runWithGwcWithConcurrencyAnd90PercentCacheHits( + GwcWithConcurrencyAnd90PercentCacheHitsState state) throws Exception { + return run(state); + } + + @Benchmark + public ServletResponse runWithGwcWithoutConcurrencyAnd90PercentCacheHits( + GwcWithoutConcurrencyAnd90PercentCacheHitsState state) throws Exception { + return run(state); + } + + @Benchmark + public ServletResponse runWithoutGwc(NoGwcState state) throws Exception { + return run(state); + } + + private ServletResponse run(AbstractBenchmarkState state) throws Exception { + + int currentIndex = state.currentIndex.getAndIncrement(); + + long[] metaTileIndex = state.tileIndices[currentIndex]; + + String request = buildGetMap(LAYER_NAME, metaTileIndex); + + MockHttpServletResponse response = + state.geoServerSystemTestSupport.getAsServletResponse(request); + + String cacheResult = response.getHeader("geowebcache-cache-result"); + if (cacheResult == null) { // will be null if we aren't even using integrated GWC + cacheResult = "MISS"; + } + state.cacheHitRate.compute(cacheResult, (key, value) -> value == null ? 1 : value + 1); + return response; + } + } + + /** + * For a given layer and zoom level, generate a certain amount of valid tile indices. + * + *

    It will attempt to evenly distribute the tiles across the entire gridset coverage. + * + * @param boundingBox Optional bounding box to constrain tiles to a particular geographical + * area. + * @param tilesPerMetatile How many tiles from each metatile. By specifying "1" we can ensure + * all requests will be cache MISSES, whereas anything greater than 1 will ensure some + * degree of cache HITS. + */ + private static long[][] getTileIndices( + String layerName, + BoundingBox boundingBox, + int zoomLevel, + int amount, + int metaTileSize, + int tilesPerMetatile) { + final GWC gwc = GWC.get(); + final TileLayer tileLayer = gwc.getTileLayerByName(layerName); + final GridSubset gridSubset = tileLayer.getGridSubset("EPSG:4326"); + + long[] coverage; // coverage={minx,miny,max,maxy,zoomlevel} + if (boundingBox == null) { + coverage = gridSubset.getCoverage(zoomLevel); + } else { + coverage = gridSubset.getCoverageIntersection(zoomLevel, boundingBox); + } + + System.out.printf( + "Coverage: %d, %d, %d, %d, %d (minx, miny, maxx, maxy, zoomLevel)\n", + coverage[0], coverage[1], coverage[2], coverage[3], coverage[4]); + + long[][] indices = new long[amount][3]; // each one contains {x,y,zoomLevel} + + long minX, minY, currentX, currentY, maxX, maxY; + minX = currentX = coverage[0]; + minY = currentY = coverage[1]; + maxX = coverage[2]; + maxY = coverage[3]; + + long width = maxX - minX; + long height = maxY - minY; + long maxNumberOfNonOverlappingMetaTilesHorizontally = width / metaTileSize; + long maxNumberOfNonOverlappingMetaTilesVertically = height / metaTileSize; + + System.out.println( + "Max number of metatiles with coverage: " + + maxNumberOfNonOverlappingMetaTilesHorizontally + * maxNumberOfNonOverlappingMetaTilesVertically); + + long horizontalIncrementBetweenMetaTiles = + width / maxNumberOfNonOverlappingMetaTilesHorizontally; + long verticalIncrementBetweenMetaTiles = + height / maxNumberOfNonOverlappingMetaTilesVertically; + + long numberOfMetaTiles = amount / tilesPerMetatile; + + int tileCount = 0; + // Traverse the overall grid to find each metatile + for (int metaTileIndex = 0; metaTileIndex < numberOfMetaTiles; metaTileIndex++) { + + // Traverse each metatile to pull out individual tiles + long currentXWithinMetaTile = 0; + long currentYWithinMetaTile = 0; + for (int tileIndex = 0; tileIndex < tilesPerMetatile; tileIndex++) { + + indices[tileCount] = + new long[] { + currentX + currentXWithinMetaTile, + currentY + currentYWithinMetaTile, + zoomLevel + }; + tileCount++; + + currentXWithinMetaTile += 1; + + // navigate to next row of metatile if we hit the end of this one + if (currentXWithinMetaTile > metaTileSize) { + currentXWithinMetaTile = 0; + currentYWithinMetaTile += 1; + } + } + + currentX += horizontalIncrementBetweenMetaTiles; + + // Navigate to the next row if we hit the end of this one + if (currentX > (maxX - metaTileSize)) { + currentX = minX; + currentY += verticalIncrementBetweenMetaTiles; + } + + if (currentY > maxY) { + throw new RuntimeException( + "Grid subset isn't large enough to generate the desired number of non-conflicting metatiles; try a larger zoom level."); + } + } + return indices; + } + + private static String buildGetMap(final String layerName, long[] tileIndex) { + + String gridsetId = "EPSG:4326"; + final GWC gwc = GWC.get(); + final TileLayer tileLayer = gwc.getTileLayerByName(layerName); + final GridSubset gridSubset = tileLayer.getGridSubset(gridsetId); + + BoundingBox bounds = gridSubset.boundsFromIndex(tileIndex); + + StringBuilder sb = new StringBuilder("wms"); + sb.append("?service=WMS&request=GetMap&version=1.1.1&format=image/png"); + sb.append("&layers=").append(layerName); + sb.append("&srs=").append(gridSubset.getSRS()); + sb.append("&width=").append(gridSubset.getGridSet().getTileWidth()); + sb.append("&height=").append(gridSubset.getGridSet().getTileHeight()); + sb.append("&styles="); + sb.append("&bbox=").append(bounds.toString()); + sb.append("&tilesorigin=-180.0,90.0"); + sb.append("&tiled=true"); + return sb.toString(); + } + /** + * Save the indicies to a CSV which can optionally be loaded into JMeter for alternative + * benchmarking. + * + * @param location Location of the CSV + * @param tileIndices The tile indices + * @throws IOException + */ + static void saveToCsv(String location, long[][] tileIndices) throws IOException { + PrintWriter writer = new PrintWriter(new FileWriter(location)); + String gridsetId = "EPSG:4326"; + final GWC gwc = GWC.get(); + final TileLayer tileLayer = gwc.getTileLayerByName(LAYER_NAME); + final GridSubset gridSubset = tileLayer.getGridSubset(gridsetId); + + for (long[] tileIndex : tileIndices) { + BoundingBox bounds = gridSubset.boundsFromIndex(tileIndex); + writer.println(bounds.toString()); + } + + writer.close(); + } +} diff --git a/src/gwc/src/test/java/org/geoserver/gwc/config/GWCConfigPersisterTest.java b/src/gwc/src/test/java/org/geoserver/gwc/config/GWCConfigPersisterTest.java index 202e8eb1e32..24e2ee7bfc0 100644 --- a/src/gwc/src/test/java/org/geoserver/gwc/config/GWCConfigPersisterTest.java +++ b/src/gwc/src/test/java/org/geoserver/gwc/config/GWCConfigPersisterTest.java @@ -74,6 +74,7 @@ public void testSaveLoad() throws Exception { GWCConfig config = GWCConfig.getOldDefaults(); config.setCacheNonDefaultStyles(true); config.setDirectWMSIntegrationEnabled(true); + config.setMetaTilingThreads(12); config.setCacheWarningSkips( new LinkedHashSet<>(Arrays.asList(WarningType.Default, WarningType.FailedNearest))); diff --git a/src/gwc/src/test/java/org/geoserver/gwc/config/GWCConfigTest.java b/src/gwc/src/test/java/org/geoserver/gwc/config/GWCConfigTest.java index 0d3a38b275d..97e9c35a52a 100644 --- a/src/gwc/src/test/java/org/geoserver/gwc/config/GWCConfigTest.java +++ b/src/gwc/src/test/java/org/geoserver/gwc/config/GWCConfigTest.java @@ -49,6 +49,10 @@ public void testSaneConfig() { assertFalse(config.isSane()); assertTrue((config = config.saneConfig()).isSane()); + config.setMetaTilingThreads(-1); + assertFalse(config.isSane()); + assertTrue((config = config.saneConfig()).isSane()); + config.setGutter(-1); assertFalse(config.isSane()); assertTrue((config = config.saneConfig()).isSane()); diff --git a/src/gwc/src/test/java/org/geoserver/gwc/layer/GeoServerTileLayerTest.java b/src/gwc/src/test/java/org/geoserver/gwc/layer/GeoServerTileLayerTest.java index 1075fb003c0..4658567c80b 100644 --- a/src/gwc/src/test/java/org/geoserver/gwc/layer/GeoServerTileLayerTest.java +++ b/src/gwc/src/test/java/org/geoserver/gwc/layer/GeoServerTileLayerTest.java @@ -26,12 +26,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.awt.Dimension; import java.awt.image.BufferedImage; @@ -53,6 +48,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -878,70 +874,73 @@ public void testGetTilePreconditions() throws Exception { @SuppressWarnings("unchecked") protected class GetTileMockTester { - protected MimeType mimeType; - protected StorageBroker storageBroker; - - public GetTileMockTester() throws Exception { + public GeoServerTileLayer prepareTileLayer() throws Exception { Resource mockResult = mock(Resource.class); ArgumentCaptor argument = ArgumentCaptor.forClass(Map.class); Mockito.when(mockGWC.dispatchOwsRequest(argument.capture(), any())) .thenReturn(mockResult); - BufferedImage image = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB); - RenderedImageMap fakeDispatchedMap = - new RenderedImageMap(new WMSMapContent(), image, "image/png"); + return new GeoServerTileLayer(layerInfo, defaults, gridSetBroker); + } - RenderedImageMapResponse fakeResponseEncoder = mock(RenderedImageMapResponse.class); - this.mimeType = MimeType.createFromFormat("image/png"); - when(mockGWC.getResponseEncoder(eq(mimeType), any())).thenReturn(fakeResponseEncoder); + public RenderedImageMap prepareFakeMap() { + return prepareFakeMap(256, 256); + } - this.storageBroker = mock(StorageBroker.class); - when(storageBroker.get(any())).thenReturn(false); + public RenderedImageMap prepareFakeMap(int width, int height) { + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + return new RenderedImageMap(new WMSMapContent(), image, "image/png"); + } - layerInfoTileLayer = new GeoServerTileLayer(layerInfo, defaults, gridSetBroker); - configureLayer(layerInfoTileLayer); + public ConveyorTile prepareConveyorTile(GeoServerTileLayer tileLayer, long[] tileIndex) + throws Exception { MockHttpServletRequest servletReq = new MockHttpServletRequest(); HttpServletResponse servletResp = new MockHttpServletResponse(); - long[] tileIndex = {0, 0, 0}; - ConveyorTile tile = - new ConveyorTile( - storageBroker, - layerInfoTileLayer.getName(), - "EPSG:4326", - tileIndex, - mimeType, - null, - servletReq, - servletResp); - - GeoServerTileLayer.WEB_MAP.set(fakeDispatchedMap); - ConveyorTile returned = layerInfoTileLayer.getTile(tile); - assertNotNull(returned); - assertNotNull(returned.getBlob()); - assertEquals(CacheResult.MISS, returned.getCacheResult()); - assertEquals(200, returned.getStatus()); + RenderedImageMapResponse fakeResponseEncoder = mock(RenderedImageMapResponse.class); + MimeType mimeType = MimeType.createFromFormat("image/png"); + when(mockGWC.getResponseEncoder(eq(mimeType), any())).thenReturn(fakeResponseEncoder); - performAssertions(); - } + StorageBroker storageBroker = mock(StorageBroker.class); + when(storageBroker.get(any())).thenReturn(false); - protected void configureLayer(GeoServerTileLayer layerInfoTileLayer) { - // do nothing by default + return new ConveyorTile( + storageBroker, + tileLayer.getName(), + "EPSG:4326", + tileIndex, + mimeType, + null, + servletReq, + servletResp); } /** By default, checks that the tile has been cached permanently */ - protected void performAssertions() throws Exception { - verify(storageBroker, atLeastOnce()).get(any()); - verify(storageBroker, times(1)).put(Mockito.any()); - verify(storageBroker, never()).putTransient(Mockito.any()); - verify(mockGWC, times(1)).getResponseEncoder(eq(mimeType), isA(RenderedImageMap.class)); + protected void performAssertions(ConveyorTile result) throws Exception { + + assertNotNull(result); + assertNotNull(result.getBlob()); + assertEquals(CacheResult.MISS, result.getCacheResult()); + assertEquals(200, result.getStatus()); + + verify(result.getStorageBroker(), atLeastOnce()).get(any()); + verify(result.getStorageBroker(), times(1)).put(Mockito.any()); + verify(result.getStorageBroker(), never()).putTransient(Mockito.any()); + verify(mockGWC, times(1)) + .getResponseEncoder(eq(result.getMimeType()), isA(RenderedImageMap.class)); } } @Test public void testGetTile() throws Exception { - new GetTileMockTester(); + long[] tileIndex = new long[] {0, 0, 0}; + GetTileMockTester tester = new GetTileMockTester(); + GeoServerTileLayer tileLayer = tester.prepareTileLayer(); + ConveyorTile conveyorTile = tester.prepareConveyorTile(tileLayer, tileIndex); + GeoServerTileLayer.WEB_MAP.set(tester.prepareFakeMap()); + ConveyorTile result = tileLayer.getTile(conveyorTile); + tester.performAssertions(result); } private FeatureTypeInfo getMockTimeFeatureType() { @@ -957,65 +956,181 @@ private FeatureTypeInfo getMockTimeFeatureType() { @Test public void testGetTileWarningNoSkip() throws Exception { // no skips setup, will cache permanently - new GetTileMockTester() { + long[] tileIndex = new long[] {0, 0, 0}; + GetTileMockTester tester = new GetTileMockTester(); + GeoServerTileLayer tileLayer = tester.prepareTileLayer(); - @Override - protected void configureLayer(GeoServerTileLayer layerInfoTileLayer) { - layerInfoTileLayer.getInfo().setCacheWarningSkips(Collections.emptySet()); + tileLayer.getInfo().setCacheWarningSkips(Collections.emptySet()); - FeatureTypeInfo resource = getMockTimeFeatureType(); - HTTPWarningAppender.addWarning( - DimensionWarning.defaultValue(resource, "time", new Date())); - } - }; + FeatureTypeInfo resource = getMockTimeFeatureType(); + HTTPWarningAppender.addWarning(DimensionWarning.defaultValue(resource, "time", new Date())); + + ConveyorTile conveyorTile = tester.prepareConveyorTile(tileLayer, tileIndex); + GeoServerTileLayer.WEB_MAP.set(tester.prepareFakeMap()); + ConveyorTile result = tileLayer.getTile(conveyorTile); + tester.performAssertions(result); } @Test public void testGetTileWarningMismatchedSkip() throws Exception { // skips on nearest, gets a warning as default, caches permanently - new GetTileMockTester() { + long[] tileIndex = new long[] {0, 0, 0}; + GetTileMockTester tester = new GetTileMockTester(); + GeoServerTileLayer tileLayer = tester.prepareTileLayer(); - @Override - protected void configureLayer(GeoServerTileLayer layerInfoTileLayer) { - layerInfoTileLayer - .getInfo() - .setCacheWarningSkips(Collections.singleton(WarningType.Nearest)); + tileLayer.getInfo().setCacheWarningSkips(Collections.singleton(WarningType.Nearest)); - FeatureTypeInfo resource = getMockTimeFeatureType(); - HTTPWarningAppender.addWarning( - DimensionWarning.defaultValue(resource, "time", new Date())); - } - }; + FeatureTypeInfo resource = getMockTimeFeatureType(); + HTTPWarningAppender.addWarning(DimensionWarning.defaultValue(resource, "time", new Date())); + + ConveyorTile conveyorTile = tester.prepareConveyorTile(tileLayer, tileIndex); + GeoServerTileLayer.WEB_MAP.set(tester.prepareFakeMap()); + ConveyorTile result = tileLayer.getTile(conveyorTile); + tester.performAssertions(result); } @Test public void testGetTileWarningSkip() throws Exception { // skips on nearest and default, gets a warning as default, no persistent cache occurs - new GetTileMockTester() { - - @Override - protected void configureLayer(GeoServerTileLayer layerInfoTileLayer) { - layerInfoTileLayer - .getInfo() - .setCacheWarningSkips( - new HashSet<>( - Arrays.asList(WarningType.Nearest, WarningType.Default))); - - FeatureTypeInfo resource = getMockTimeFeatureType(); - HTTPWarningAppender.addWarning( - DimensionWarning.defaultValue(resource, "time", new Date())); - } + long[] tileIndex = new long[] {0, 0, 0}; + GetTileMockTester tester = new GetTileMockTester(); + GeoServerTileLayer tileLayer = tester.prepareTileLayer(); + + tileLayer + .getInfo() + .setCacheWarningSkips( + new HashSet<>(Arrays.asList(WarningType.Nearest, WarningType.Default))); + + FeatureTypeInfo resource = getMockTimeFeatureType(); + HTTPWarningAppender.addWarning(DimensionWarning.defaultValue(resource, "time", new Date())); + + ConveyorTile conveyorTile = tester.prepareConveyorTile(tileLayer, tileIndex); + GeoServerTileLayer.WEB_MAP.set(tester.prepareFakeMap()); + ConveyorTile result = tileLayer.getTile(conveyorTile); + + // check only transient caching has been performed + verify(result.getStorageBroker(), atLeastOnce()).get(any()); + verify(result.getStorageBroker(), never()).put(Mockito.any()); + verify(result.getStorageBroker(), times(1)).putTransient(Mockito.any()); + verify(mockGWC, times(1)) + .getResponseEncoder(eq(result.getMimeType()), isA(RenderedImageMap.class)); + } - // check only transient caching has been performed - @Override - protected void performAssertions() throws Exception { - verify(storageBroker, atLeastOnce()).get(any()); - verify(storageBroker, never()).put(Mockito.any()); - verify(storageBroker, times(1)).putTransient(Mockito.any()); - verify(mockGWC, times(1)) - .getResponseEncoder(eq(mimeType), isA(RenderedImageMap.class)); - } - }; + @Test + public void testGetTileWithMetaTilingExecutor() throws Exception { + GetTileMockTester tester = new GetTileMockTester(); + GeoServerTileLayer tileLayer = tester.prepareTileLayer(); + + ExecutorService executorServiceSpy = spy(Executors.newFixedThreadPool(2)); + when(mockGWC.getMetaTilingExecutor()).thenReturn(executorServiceSpy); + + // Ensure enough valid coverage to support metatiling + resource.setLatLonBoundingBox(new ReferencedEnvelope(-180, 180, -90, 90, WGS84)); + resource.setNativeBoundingBox(new ReferencedEnvelope(-180, 180, -90, 90, WGS84)); + + int zoomLevel = 4; // pick a zoom level that has enough tiles for at least one meta-tile + long[] coverage = + tileLayer + .getGridSubset("EPSG:4326") + .getCoverage(zoomLevel); // {minx,miny,max,maxy,zoomlevel} + + long[] tileIndex = new long[] {coverage[0], coverage[1], zoomLevel}; + ConveyorTile conveyorTile = tester.prepareConveyorTile(tileLayer, tileIndex); + Dispatcher.REQUEST.set(new Request()); + + GeoServerTileLayer.WEB_MAP.set(tester.prepareFakeMap(1024, 1024)); + ConveyorTile result = tileLayer.getTile(conveyorTile); + + assertNotNull(result); + assertNotNull(result.getBlob()); + assertEquals(CacheResult.MISS, result.getCacheResult()); + assertEquals(200, result.getStatus()); + + executorServiceSpy.awaitTermination(2, TimeUnit.SECONDS); + + // 1 async threads: 1 to save the requested tile, 15 for encoding/saving additional tiles, + // and 1 to dispose of metatile (one of the tiles is performed on main thread) + verify(executorServiceSpy, times(17)).execute(any()); + + // 16 tiles to put in storage + verify(result.getStorageBroker(), times(16)).put(Mockito.any()); + } + + /** + * If there is a metatiling executor service configured, but a conveyor tile comes in that is + * missing the "servlet request" object, this is not a user-initiated request and is likely a + * seed attempt, and therefore it should ignore the executor service and encode/save all tiles + * from a metatile on the main thread. + */ + @Test + public void testGetTileWithMetaTilingExecutorButNoDispatcherRequest() throws Exception { + GetTileMockTester tester = new GetTileMockTester(); + GeoServerTileLayer tileLayer = tester.prepareTileLayer(); + + ExecutorService executorServiceSpy = spy(Executors.newFixedThreadPool(2)); + when(mockGWC.getMetaTilingExecutor()).thenReturn(executorServiceSpy); + + // Ensure enough valid coverage to support metatiling + resource.setLatLonBoundingBox(new ReferencedEnvelope(-180, 180, -90, 90, WGS84)); + resource.setNativeBoundingBox(new ReferencedEnvelope(-180, 180, -90, 90, WGS84)); + + int zoomLevel = 4; // pick a zoom level that has enough tiles for at least one meta-tile + long[] coverage = + tileLayer + .getGridSubset("EPSG:4326") + .getCoverage(zoomLevel); // {minx,miny,max,maxy,zoomlevel} + + long[] tileIndex = new long[] {coverage[0], coverage[1], zoomLevel}; + ConveyorTile conveyorTile = tester.prepareConveyorTile(tileLayer, tileIndex); + + Dispatcher.REQUEST.remove(); + + GeoServerTileLayer.WEB_MAP.set(tester.prepareFakeMap(1024, 1024)); + ConveyorTile result = tileLayer.getTile(conveyorTile); + + assertNotNull(result); + assertNotNull(result.getBlob()); + assertEquals(CacheResult.MISS, result.getCacheResult()); + assertEquals(200, result.getStatus()); + + executorServiceSpy.awaitTermination(2, TimeUnit.SECONDS); + + // There should be no executions on the executor service (it should all be on main thread) + verify(executorServiceSpy, times(0)).execute(any()); + + // 16 tiles to put in storage + verify(result.getStorageBroker(), times(16)).put(Mockito.any()); + } + + @Test + public void testGetTileWithNullMetaTilingExecutor() throws Exception { + GetTileMockTester tester = new GetTileMockTester(); + GeoServerTileLayer tileLayer = tester.prepareTileLayer(); + + when(mockGWC.getMetaTilingExecutor()).thenReturn(null); + + // Ensure enough valid coverage to support metatiling + resource.setLatLonBoundingBox(new ReferencedEnvelope(-180, 180, -90, 90, WGS84)); + resource.setNativeBoundingBox(new ReferencedEnvelope(-180, 180, -90, 90, WGS84)); + + int zoomLevel = 4; // pick a zoom level that has enough tiles for at least one meta-tile + long[] coverage = + tileLayer + .getGridSubset("EPSG:4326") + .getCoverage(zoomLevel); // {minx,miny,max,maxy,zoomlevel} + + long[] tileIndex = new long[] {coverage[0], coverage[1], zoomLevel}; + ConveyorTile conveyorTile = tester.prepareConveyorTile(tileLayer, tileIndex); + GeoServerTileLayer.WEB_MAP.set(tester.prepareFakeMap(1024, 1024)); + ConveyorTile result = tileLayer.getTile(conveyorTile); + + assertNotNull(result); + assertNotNull(result.getBlob()); + assertEquals(CacheResult.MISS, result.getCacheResult()); + assertEquals(200, result.getStatus()); + + // 16 storage puts instead of the typical 1 + verify(result.getStorageBroker(), times(16)).put(Mockito.any()); } /** Test expire web cache without any setup of LayerInfo resource. */ diff --git a/src/pom.xml b/src/pom.xml index abed5dfca00..320357f2b6c 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -164,6 +164,7 @@ false false deprecation,unchecked + 1.37 diff --git a/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.html b/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.html index 1b64621fa58..e5b58830e94 100644 --- a/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.html +++ b/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.html @@ -39,6 +39,15 @@ Tiles high

  • +
  • +
    + +
    +
    + + Threads +
    +
  • diff --git a/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.java b/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.java index 72d4310ed73..18b3dfc0bb7 100644 --- a/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.java +++ b/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.java @@ -15,10 +15,7 @@ import org.apache.wicket.ajax.form.OnChangeAjaxBehavior; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; -import org.apache.wicket.markup.html.form.Check; -import org.apache.wicket.markup.html.form.CheckBox; -import org.apache.wicket.markup.html.form.CheckGroup; -import org.apache.wicket.markup.html.form.DropDownChoice; +import org.apache.wicket.markup.html.form.*; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.panel.Panel; @@ -112,6 +109,13 @@ protected void onUpdate(AjaxRequestTarget target) { metaTilingY.setRequired(true); configs.add(metaTilingY); + IModel metaTilingThreads = + new PropertyModel<>(gwcConfigModel, "metaTilingThreads"); + TextField metaTilingThreadsTextField = + new TextField<>("metaTilingThreads", metaTilingThreads); + metaTilingThreadsTextField.setRequired(false); + configs.add(metaTilingThreadsTextField); + IModel gutterModel = new PropertyModel<>(gwcConfigModel, "gutter"); List gutterChoices = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 50, 100); diff --git a/src/web/gwc/src/main/resources/GeoServerApplication.properties b/src/web/gwc/src/main/resources/GeoServerApplication.properties index e2f9b04061e..b64155079c4 100644 --- a/src/web/gwc/src/main/resources/GeoServerApplication.properties +++ b/src/web/gwc/src/main/resources/GeoServerApplication.properties @@ -37,6 +37,8 @@ GWCSettingsPage.cacheNonDefaultStyles=Automatically cache non-default styles GWCSettingsPage.metaTiling=Default metatile size: GWCSettingsPage.metaTilingX=tiles wide by GWCSettingsPage.metaTilingY=tiles high +GWCSettingsPage.metaTilingThreads=Threads available for concurrent metatiling encoding and saving: +GWCSettingsPage.metaTilingThreadsUnits=Threads GWCSettingsPage.gutter=Default gutter size in pixels: GWCSettingsPage.defaultCacheOptions=Default Caching Options for GeoServer Layers GWCSettingsPage.defaultCacheFormats=Default Tile Image Formats for: diff --git a/src/wms/src/main/java/org/geoserver/wms/RasterCleaner.java b/src/wms/src/main/java/org/geoserver/wms/RasterCleaner.java index 6751226149b..eda50e91050 100644 --- a/src/wms/src/main/java/org/geoserver/wms/RasterCleaner.java +++ b/src/wms/src/main/java/org/geoserver/wms/RasterCleaner.java @@ -53,11 +53,16 @@ public static void addCoverage(GridCoverage2D coverage) { @Override public void finished(Request request) { + cleanup(); + } + + /** Immediately cleans up all images and coverages scheduled for cleanup */ + public static void cleanup() { disposeCoverages(); disposeImages(); } - private void disposeImages() { + private static void disposeImages() { List list = images.get(); if (list != null) { images.remove(); @@ -92,7 +97,7 @@ public static void disposeImage(RenderedImage image) { } } - private void disposeCoverages() { + private static void disposeCoverages() { List list = coverages.get(); if (list != null) { coverages.remove(); diff --git a/src/wms/src/main/java/org/geoserver/wms/map/MetatileMapOutputFormat.java b/src/wms/src/main/java/org/geoserver/wms/map/MetatileMapOutputFormat.java index b58bfb55f0d..bd2515eb722 100644 --- a/src/wms/src/main/java/org/geoserver/wms/map/MetatileMapOutputFormat.java +++ b/src/wms/src/main/java/org/geoserver/wms/map/MetatileMapOutputFormat.java @@ -5,7 +5,6 @@ */ package org.geoserver.wms.map; -import it.geosolutions.jaiext.BufferedImageAdapter; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Point2D; @@ -301,9 +300,7 @@ static RenderedImage[] split(MetaTileKey key, RenderedImage metaTile) { LOGGER.finer("Metatile split on BufferedImage"); } final BufferedImage image = (BufferedImage) metaTile; - final BufferedImage subimage = - image.getSubimage(x, y, tileSize, tileSize); - tile = new BufferedImageAdapter(subimage); + tile = image.getSubimage(x, y, tileSize, tileSize); break; default: throw new IllegalStateException( From 1caac1c2a400938e56c88b91e1a8a1f80700cbb5 Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Fri, 18 Oct 2024 08:40:38 +0200 Subject: [PATCH 13/43] [GEOS-11580] Improve embedded GWC meta-tiling performance: review, QA, docs, GUI --- .../source/geowebcache/webadmin/defaults.rst | 14 + .../webadmin/img/defaults_services.png | Bin 30793 -> 23524 bytes .../org/geoserver/mapml/MapMLWMSTest.java | 8 +- .../src/main/java/org/geoserver/gwc/GWC.java | 16 +- .../gwc/layer/GeoServerTileLayer.java | 331 ++++++++++-------- .../org/geoserver/gwc/GWCIntegrationTest.java | 29 +- .../gwc/WmsMetatileBenchmarkTest.java | 76 ++-- .../gwc/layer/GeoServerTileLayerTest.java | 33 +- .../gwc/web/CachingOptionsPanel.html | 9 - .../gwc/web/CachingOptionsPanel.java | 12 +- .../geoserver/gwc/web/GWCServicesPanel.html | 8 + .../geoserver/gwc/web/GWCServicesPanel.java | 10 + .../resources/GeoServerApplication.properties | 3 +- .../gwc/web/GWCSettingsPageTest.java | 34 ++ 14 files changed, 363 insertions(+), 220 deletions(-) diff --git a/doc/en/user/source/geowebcache/webadmin/defaults.rst b/doc/en/user/source/geowebcache/webadmin/defaults.rst index 3f4713ac8c7..b883e4d4e63 100644 --- a/doc/en/user/source/geowebcache/webadmin/defaults.rst +++ b/doc/en/user/source/geowebcache/webadmin/defaults.rst @@ -77,6 +77,20 @@ Enable Data Security Enables the :ref:`gwc_data_security` in the embedded GeoWebCache. +Metatiling threads count +~~~~~~~~~~~~~~~~~~~~~~~~ + +This setting determines the number of threads that will be used to encode and save metatiles. +By default, a user requested tile will be encoded on main request thread and immediately returned, +but the remaining tiles will be encoded and saved on asynchronous threads to decrease latency +experienced by the user. + +Possible values for this setting: + +* **unset**, which will use a default thread pool size, equal to 2 times the number of cores +* **0**, which will disable concurrency and all tiles belonging to the metatile will be encoded/saved on the main request thread +* **a positive integer**, which will set the number of threads to the specified value + Default Caching Options for GeoServer Layers -------------------------------------------- diff --git a/doc/en/user/source/geowebcache/webadmin/img/defaults_services.png b/doc/en/user/source/geowebcache/webadmin/img/defaults_services.png index 684a03dfa9156b3b7e909f423f4299d96a73c5ba..b73ddf1b79de2ab7305568565cb27c66d3076d2f 100644 GIT binary patch literal 23524 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 literal 30793 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 PerU`FZ1py=vV}SX^c=m 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 diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java index 9daca637230..0a7729636c7 100644 --- a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMSTest.java @@ -113,7 +113,7 @@ protected void registerNamespaces(Map namespaces) { } @Before - public void setup() { + public void setup() throws IOException { xpath = XMLUnit.newXpathEngine(); Catalog catalog = getCatalog(); @@ -123,6 +123,12 @@ public void setup() { layerMeta.getMetadata().put("mapml.useTiles", false); catalog.save(layerMeta); + + // disable background tile saving, some tests check for the presence of the tiles on disk + GWC gwc = GWC.get(); + GWCConfig config = gwc.getConfig(); + config.setMetaTilingThreads(0); + gwc.saveConfig(config); } @After diff --git a/src/gwc/src/main/java/org/geoserver/gwc/GWC.java b/src/gwc/src/main/java/org/geoserver/gwc/GWC.java index ace439b92cd..59a9c26f131 100644 --- a/src/gwc/src/main/java/org/geoserver/gwc/GWC.java +++ b/src/gwc/src/main/java/org/geoserver/gwc/GWC.java @@ -39,7 +39,10 @@ import java.util.Properties; import java.util.Set; import java.util.TreeSet; -import java.util.concurrent.*; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.http.Cookie; @@ -317,16 +320,7 @@ private ExecutorService buildMetaTilingExecutor(Integer metaTilingThreads) { if (metaTilingThreads == 0) { return null; } - ThreadPoolExecutor executor = - new ThreadPoolExecutor( - metaTilingThreads, - metaTilingThreads, - 10, - TimeUnit.SECONDS, - new LinkedBlockingQueue<>(), - threadFactory); - executor.allowCoreThreadTimeOut(true); - return executor; + return Executors.newFixedThreadPool(metaTilingThreads, threadFactory); } /** diff --git a/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerTileLayer.java b/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerTileLayer.java index 1a21d562ffe..a94f8734be2 100644 --- a/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerTileLayer.java +++ b/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerTileLayer.java @@ -13,13 +13,22 @@ import com.google.common.base.Throwables; import com.google.common.collect.Iterables; -import java.awt.*; +import java.awt.Dimension; import java.io.IOException; import java.lang.reflect.Proxy; import java.net.MalformedURLException; import java.net.URL; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; @@ -30,7 +39,19 @@ import java.util.stream.Collectors; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; -import org.geoserver.catalog.*; +import org.geoserver.catalog.Catalog; +import org.geoserver.catalog.FeatureTypeInfo; +import org.geoserver.catalog.KeywordInfo; +import org.geoserver.catalog.LayerGroupInfo; +import org.geoserver.catalog.LayerInfo; +import org.geoserver.catalog.MetadataLinkInfo; +import org.geoserver.catalog.MetadataMap; +import org.geoserver.catalog.PublishedInfo; +import org.geoserver.catalog.PublishedType; +import org.geoserver.catalog.ResourceInfo; +import org.geoserver.catalog.ResourcePool; +import org.geoserver.catalog.StyleInfo; +import org.geoserver.catalog.WorkspaceInfo; import org.geoserver.catalog.impl.ModificationProxy; import org.geoserver.config.GeoServer; import org.geoserver.gwc.GWC; @@ -44,7 +65,11 @@ import org.geoserver.rest.RequestInfo; import org.geoserver.util.DimensionWarning; import org.geoserver.util.HTTPWarningAppender; -import org.geoserver.wms.*; +import org.geoserver.wms.GetLegendGraphicRequest; +import org.geoserver.wms.GetMapRequest; +import org.geoserver.wms.RasterCleaner; +import org.geoserver.wms.WMS; +import org.geoserver.wms.WebMap; import org.geoserver.wms.capabilities.CapabilityUtil; import org.geoserver.wms.capabilities.LegendSample; import org.geotools.api.feature.type.FeatureType; @@ -63,16 +88,32 @@ import org.geowebcache.filter.parameters.ParameterException; import org.geowebcache.filter.parameters.ParameterFilter; import org.geowebcache.filter.request.RequestFilter; -import org.geowebcache.grid.*; +import org.geowebcache.grid.BoundingBox; +import org.geowebcache.grid.GridSet; +import org.geowebcache.grid.GridSetBroker; +import org.geowebcache.grid.GridSubset; +import org.geowebcache.grid.OutsideCoverageException; +import org.geowebcache.grid.SRS; import org.geowebcache.io.ByteArrayResource; import org.geowebcache.io.Resource; -import org.geowebcache.layer.*; -import org.geowebcache.layer.meta.*; +import org.geowebcache.layer.ExpirationRule; +import org.geowebcache.layer.LayerListenerList; +import org.geowebcache.layer.MetaTile; +import org.geowebcache.layer.ProxyLayer; +import org.geowebcache.layer.TileJSONProvider; +import org.geowebcache.layer.TileLayer; +import org.geowebcache.layer.TileLayerListener; +import org.geowebcache.layer.meta.ContactInformation; +import org.geowebcache.layer.meta.LayerMetaInformation; +import org.geowebcache.layer.meta.MetadataURL; +import org.geowebcache.layer.meta.TileJSON; +import org.geowebcache.layer.meta.VectorLayerMetadata; import org.geowebcache.layer.updatesource.UpdateSourceDefinition; import org.geowebcache.locks.LockProvider.Lock; import org.geowebcache.mime.FormatModifier; import org.geowebcache.mime.MimeException; import org.geowebcache.mime.MimeType; +import org.geowebcache.storage.StorageBroker; import org.geowebcache.storage.TileObject; import org.geowebcache.util.GWCVars; import org.geowebcache.util.ServletUtils; @@ -565,21 +606,19 @@ protected ConveyorTile getMetatilingResponse( if (tryCache) { /* ****************** Acquire lock on individual tile ******************* */ // Will block here if there is an async thread currently saving this tile - final Lock tileLock = - GWC.get() - .getLockProvider() - .getLock( - buildTileLockKey( - conveyorTile, conveyorTile.getTileIndex())); + String lockKey = buildTileLockKey(conveyorTile, conveyorTile.getTileIndex()); + final Lock tileLock = GWC.get().getLockProvider().getLock(lockKey); try { // After getting the lock on the meta tile and individual tile, try cache again foundInCache = tryCacheFetch(conveyorTile); if (foundInCache) { - LOGGER.finest( - "--> " - + Thread.currentThread().getName() - + " returns cache hit for " - + Arrays.toString(metaTile.getMetaGridPos())); + LOGGER.log( + Level.FINEST, + () -> + "--> " + + Thread.currentThread().getName() + + " returns cache hit for " + + Arrays.toString(metaTile.getMetaGridPos())); metaTile.dispose(); } } finally { @@ -589,151 +628,156 @@ protected ConveyorTile getMetatilingResponse( } if (!foundInCache) { - LOGGER.finer( - "--> " - + Thread.currentThread().getName() - + " submitting getMap request for meta grid location " - + Arrays.toString(metaTile.getMetaGridPos()) - + " on " - + metaTile); - WebMap map; + LOGGER.log( + Level.FINER, + () -> + "--> " + + Thread.currentThread().getName() + + " submitting getMap request for meta grid location " + + Arrays.toString(metaTile.getMetaGridPos()) + + " on " + + metaTile); try { - long requestTime = System.currentTimeMillis(); - - // Actually fetch the metatile data - map = dispatchGetMap(conveyorTile, metaTile); + computeMetaTile(conveyorTile, metaTile); + } catch (Exception e) { + Throwables.throwIfInstanceOf(e, GeoWebCacheException.class); + throw new GeoWebCacheException("Problem communicating with GeoServer", e); + } + } - checkNotNull(map, "Did not obtain a WebMap from GeoServer's Dispatcher"); - metaTile.setWebMap(map); + } finally { + /* ****************** Release lock on metatile ******************* */ + metaTileLock.release(); + } - setupCachingStrategy(conveyorTile); + return finalizeTile(conveyorTile); + } - final long[][] gridPositions = metaTile.getTilesGridPositions(); - final long[] gridLoc = conveyorTile.getTileIndex(); - final GridSubset gridSubset = getGridSubset(conveyorTile.getGridSetId()); - final int numberOfTiles = gridPositions.length; + private void computeMetaTile(ConveyorTile conveyorTile, GeoServerMetaTile metaTile) + throws Exception { + WebMap map; + long requestTime = System.currentTimeMillis(); - final int zoomLevel = (int) gridLoc[2]; - final boolean store = - this.getExpireCache(zoomLevel) != GWCVars.CACHE_DISABLE_CACHE; + // Actually fetch the metatile data + map = dispatchGetMap(conveyorTile, metaTile); - Executor executor = GWC.get().getMetaTilingExecutor(); + checkNotNull(map, "Did not obtain a WebMap from GeoServer's Dispatcher"); + metaTile.setWebMap(map); - if (Dispatcher.REQUEST.get() == null) { - // Metatiling concurrency is disabled if this isn't a user request. - // Concurrency reduces the user-experienced latency but isn't - // useful for seeding. In fact, it would be harmful for seeding - // because it makes it more difficult for an administrator to - // control the amount of resource usage for significant seeding jobs. - executor = null; - } - List> completableFutures = new ArrayList<>(); + setupCachingStrategy(conveyorTile); - // A latch to track whether we've locked all the individual tiles or not, before - // we can release the metatile lock. - CountDownLatch tileLockLatch = new CountDownLatch(numberOfTiles); + final long[][] gridPositions = metaTile.getTilesGridPositions(); + final long[] gridLoc = conveyorTile.getTileIndex(); + final GridSubset gridSubset = getGridSubset(conveyorTile.getGridSetId()); + final int numberOfTiles = gridPositions.length; - for (int tileIndex = 0; tileIndex < numberOfTiles; tileIndex++) { - final long[] gridPos = gridPositions[tileIndex]; - final int finalTileIndex = tileIndex; + final int zoomLevel = (int) gridLoc[2]; + final boolean store = this.getExpireCache(zoomLevel) != GWCVars.CACHE_DISABLE_CACHE; - boolean isConveyorTile = Arrays.equals(gridLoc, gridPos); + Executor executor = GWC.get().getMetaTilingExecutor(); - if (isConveyorTile || store) { - if (!gridSubset.covers(gridPos)) { - // edge tile outside coverage, do not store it - tileLockLatch.countDown(); - continue; - } + if (Dispatcher.REQUEST.get() == null) { + // Metatiling concurrency is disabled if this isn't a user request. + // Concurrency reduces the user-experienced latency but isn't + // useful for seeding. In fact, it would be harmful for seeding + // because it makes it more difficult for an administrator to + // control the amount of resource usage for significant seeding jobs. + executor = null; + } + List> completableFutures = new ArrayList<>(); - Supplier encodeTileTask = encodeTileTask(metaTile, tileIndex); + // A latch to track whether we've locked all the individual tiles or not, before + // we can release the metatile lock. + CountDownLatch tileLockLatch = new CountDownLatch(numberOfTiles); - if (isConveyorTile) { - // Always encode the conveyor tile on the main thread - Resource resource = encodeTileTask.get(); - conveyorTile.setBlob(resource); + for (int tileIndex = 0; tileIndex < numberOfTiles; tileIndex++) { + final long[] gridPos = gridPositions[tileIndex]; + final int finalTileIndex = tileIndex; - // Saving the conveyor tile in the cache can either happen - // asynchronously or on the main thread - Runnable saveTileTask = - withTileLock( - conveyorTile, - tileLockLatch, - gridPos, - saveTileTask( - metaTile, - tileIndex, - conveyorTile, - resource, - requestTime)); - if (executor != null) { - CompletableFuture completableFuture = - CompletableFuture.runAsync(saveTileTask, executor); - completableFutures.add(completableFuture); - } else { - // Save in cache on main thread if there's no executor - saveTileTask.run(); - } - } else { + boolean isConveyorTile = Arrays.equals(gridLoc, gridPos); + if (isConveyorTile || store) { + if (!gridSubset.covers(gridPos)) { + // edge tile outside coverage, do not store it + tileLockLatch.countDown(); + continue; + } - // For all other tiles, either encode/save fully asynchronously or - // fully on the main thread - Runnable encodeAndSaveTask = - withTileLock( - conveyorTile, - tileLockLatch, - gridPos, - withRasterCleaner( - () -> { - Resource resource = - encodeTileTask.get(); - saveTileTask( - metaTile, - finalTileIndex, - conveyorTile, - resource, - requestTime) - .run(); - })); - - if (executor != null) { - // Fully asynchronous - CompletableFuture completableFuture = - CompletableFuture.runAsync(encodeAndSaveTask, executor); - completableFutures.add(completableFuture); - } else { - // Run on main thread if there's no executor - encodeAndSaveTask.run(); - } - } - } + Supplier encodeTileTask = encodeTileTask(metaTile, tileIndex); + + if (isConveyorTile) { + // Always encode the conveyor tile on the main thread, and set a tentative + // creation time for it (the actual save time will be later, the first + // time modification check from the client will re-fetch the tile + Resource resource = encodeTileTask.get(); + conveyorTile.setBlob(resource); + conveyorTile.getStorageObject().setCreated(requestTime); + + // Saving the conveyor tile in the cache can either happen + // asynchronously or on the main thread + Runnable saveTileTask = + withTileLock( + conveyorTile, + tileLockLatch, + gridPos, + saveTileTask( + metaTile, + tileIndex, + conveyorTile, + resource, + requestTime)); + if (executor != null) { + CompletableFuture completableFuture = + CompletableFuture.runAsync(saveTileTask, executor); + completableFutures.add(completableFuture); + } else { + // Save in cache on main thread if there's no executor + saveTileTask.run(); } - - // Wait until we've obtained locks on all individual tiles before proceeding - tileLockLatch.await(); - - // Dispose of meta-tile when all completable futures are done - if (!completableFutures.isEmpty()) { - runAsyncAfterAllFuturesComplete( - completableFutures, metaTile::dispose, executor); + } else { + // For all other tiles, either encode/save fully asynchronously or + // fully on the main thread + Runnable tileSaver = + () -> { + Resource resource = encodeTileTask.get(); + saveTileTask( + metaTile, + finalTileIndex, + conveyorTile, + resource, + requestTime) + .run(); + }; + Runnable encodeAndSaveTask = + withTileLock( + conveyorTile, + tileLockLatch, + gridPos, + withRasterCleaner(tileSaver)); + + if (executor != null) { + // Fully asynchronous + CompletableFuture completableFuture = + CompletableFuture.runAsync(encodeAndSaveTask, executor); + completableFutures.add(completableFuture); } else { - // There were no asynchronous tasks, everything was run on the main thread - // so we can dispose of the meta-tile right away - metaTile.dispose(); + // Run on main thread if there's no executor + encodeAndSaveTask.run(); } - - } catch (Exception e) { - Throwables.throwIfInstanceOf(e, GeoWebCacheException.class); - throw new GeoWebCacheException("Problem communicating with GeoServer", e); } } - - } finally { - /* ****************** Release lock on metatile ******************* */ - metaTileLock.release(); } - return finalizeTile(conveyorTile); + // Wait until we've obtained locks on all individual tiles before proceeding + tileLockLatch.await(); + + // Dispose of meta-tile when all completable futures are done + if (!completableFutures.isEmpty()) { + runAsyncAfterAllFuturesComplete(completableFutures, metaTile::dispose, executor); + } else { + // There were no asynchronous tasks, everything was run on the main thread + // so we can dispose of the meta-tile right away + metaTile.dispose(); + } } private void runAsyncAfterAllFuturesComplete( @@ -829,10 +873,11 @@ private Runnable saveTileTask( tile.setCreated(requestTime); // Save tile to storage + StorageBroker storageBroker = tileProto.getStorageBroker(); if (tileProto.isMetaTileCacheOnly()) { - tileProto.getStorageBroker().putTransient(tile); + storageBroker.putTransient(tile); } else { - tileProto.getStorageBroker().put(tile); + storageBroker.put(tile); } tileProto.getStorageObject().setCreated(tile.getCreated()); } catch (IOException ex) { diff --git a/src/gwc/src/test/java/org/geoserver/gwc/GWCIntegrationTest.java b/src/gwc/src/test/java/org/geoserver/gwc/GWCIntegrationTest.java index 44470109d4c..54bc5152142 100644 --- a/src/gwc/src/test/java/org/geoserver/gwc/GWCIntegrationTest.java +++ b/src/gwc/src/test/java/org/geoserver/gwc/GWCIntegrationTest.java @@ -42,6 +42,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -234,6 +235,21 @@ public void cleanup() throws Exception { prepareDataDirectory(testData); } + @Before + public void resetMetatileThreads() throws Exception { + // reset to the default, some test are using a different value + setMetatileThreads(null); + } + + private void setMetatileThreads(Integer threadCount) throws IOException { + GWC gwc = GWC.get(); + GWCConfig config = gwc.getConfig(); + if (!Objects.equals(threadCount, config.getMetaTilingThreads())) { + config.setMetaTilingThreads(threadCount); + gwc.saveConfig(config); + } + } + protected GridSet namedGridsetCopy(final String newName, final GridSet oldGridset) { final GridSet newGridset; { @@ -512,9 +528,9 @@ public void testDirectWMSIntegrationIfModifiedSinceSupport() throws Exception { final String layerName = BASIC_POLYGONS.getPrefix() + ":" + BASIC_POLYGONS.getLocalPart(); - final String path = buildGetMap(true, layerName, "EPSG:4326", null) + "&tiled=true"; + final String url = buildGetMap(true, layerName, "EPSG:4326", null) + "&tiled=true"; - MockHttpServletResponse response = getAsServletResponse(path); + MockHttpServletResponse response = getAsServletResponse(url); assertEquals(200, response.getStatus()); assertEquals("image/png", response.getContentType()); @@ -522,7 +538,7 @@ public void testDirectWMSIntegrationIfModifiedSinceSupport() throws Exception { assertNotNull(lastModifiedHeader); Date lastModified = DateUtils.parseDate(lastModifiedHeader); - MockHttpServletRequest httpReq = createGetRequest(path); + MockHttpServletRequest httpReq = createGetRequest(url); httpReq.addHeader("If-Modified-Since", lastModifiedHeader); response = dispatch(httpReq, "UTF-8"); @@ -533,7 +549,7 @@ public void testDirectWMSIntegrationIfModifiedSinceSupport() throws Exception { Date past = new Date(lastModified.getTime() - 5000); String ifModifiedSince = DateUtils.formatDate(past); - httpReq = createGetRequest(path); + httpReq = createGetRequest(url); httpReq.addHeader("If-Modified-Since", ifModifiedSince); response = dispatch(httpReq, "UTF-8"); assertEquals(HttpServletResponse.SC_OK, response.getStatus()); @@ -541,7 +557,7 @@ public void testDirectWMSIntegrationIfModifiedSinceSupport() throws Exception { Date future = new Date(lastModified.getTime() + 5000); ifModifiedSince = DateUtils.formatDate(future); - httpReq = createGetRequest(path); + httpReq = createGetRequest(url); httpReq.addHeader("If-Modified-Since", ifModifiedSince); response = dispatch(httpReq, "UTF-8"); assertEquals(HttpServletResponse.SC_NOT_MODIFIED, response.getStatus()); @@ -1539,6 +1555,9 @@ public void testRenameWorkspace() throws Exception { /** Test that removing a layer from the catalog also removes its tile cache. */ @Test public void testRemoveCachedLayer() throws Exception { + // disable metatile background processing, we want tiles on disk right afer requests + setMetatileThreads(0); + // the prefixed name of the layer under test String layerName = getLayerId(MockData.BASIC_POLYGONS); assertEquals("cite:BasicPolygons", layerName); diff --git a/src/gwc/src/test/java/org/geoserver/gwc/WmsMetatileBenchmarkTest.java b/src/gwc/src/test/java/org/geoserver/gwc/WmsMetatileBenchmarkTest.java index fcf245d1653..03e63e1be0a 100644 --- a/src/gwc/src/test/java/org/geoserver/gwc/WmsMetatileBenchmarkTest.java +++ b/src/gwc/src/test/java/org/geoserver/gwc/WmsMetatileBenchmarkTest.java @@ -24,13 +24,25 @@ import org.geowebcache.layer.TileLayer; import org.junit.Ignore; import org.junit.Test; -import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.results.format.ResultFormatType; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import org.springframework.mock.web.MockHttpServletResponse; +@SuppressWarnings("PMD.SystemPrintln") +@Ignore public class WmsMetatileBenchmarkTest extends GeoServerSystemTestSupport { static final String LAYER_NAME = @@ -70,7 +82,6 @@ public void profileBenchmark() throws Exception { * through the IDE). */ @Test - @Ignore public void runBenchmark() throws Exception { Options options = @@ -82,6 +93,13 @@ public void runBenchmark() throws Exception { new Runner(options).run(); } + private static class GeoServerBenchmarkSuppport extends GeoServerSystemTestSupport { + @Override + public MockHttpServletResponse getAsServletResponse(String path) throws Exception { + return super.getAsServletResponse(path); + } + } + @BenchmarkMode(Mode.Throughput) @Fork(1) @Threads(4) @@ -96,8 +114,8 @@ public static class AbstractBenchmarkState { AtomicInteger currentIndex = new AtomicInteger(0); - GeoServerSystemTestSupport geoServerSystemTestSupport = - new GeoServerSystemTestSupport(); + GeoServerBenchmarkSuppport geoServerSystemTestSupport = + new GeoServerBenchmarkSuppport(); // Track how many cache hits we get just to help validate correctness of our benchmark Map cacheHitRate = new ConcurrentHashMap<>(); @@ -123,10 +141,14 @@ public void tearDown() throws Exception { int cacheMisses = cacheHitRate.getOrDefault("MISS", 0); int totalRequests = cacheHits + cacheMisses; double cacheHitRate = (double) cacheHits / totalRequests; - System.out.println("Total requests: " + totalRequests); - System.out.println("Cache hits: " + cacheHits); - System.out.println("Cache misses: " + cacheMisses); - System.out.println("Cache hit rate: " + cacheHitRate); + extracted("Total requests: " + totalRequests); + extracted("Cache hits: " + cacheHits); + extracted("Cache misses: " + cacheMisses); + extracted("Cache hit rate: " + cacheHitRate); + } + + private static void extracted(String totalRequests) { + System.out.println(totalRequests); } } @@ -151,6 +173,7 @@ public void setup() throws Exception { public static class GwcWithConcurrencyAndNoCacheHitsState extends AbstractGwcWithConcurrency { + @Override public void setup() throws Exception { super.setup(); tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 1); @@ -160,7 +183,7 @@ public void setup() throws Exception { public static class GwcWithoutConcurrencyAndNoCacheHitsState extends AbstractGwcWithoutConcurrency { - + @Override public void setup() throws Exception { super.setup(); tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 1); @@ -170,7 +193,7 @@ public void setup() throws Exception { public static class GwcWithConcurrencyAnd50PercentCacheHitsState extends AbstractGwcWithConcurrency { - + @Override public void setup() throws Exception { super.setup(); tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 2); @@ -180,7 +203,7 @@ public void setup() throws Exception { public static class GwcWithoutConcurrencyAnd50PercentCacheHitsState extends AbstractGwcWithoutConcurrency { - + @Override public void setup() throws Exception { super.setup(); tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 2); @@ -190,7 +213,7 @@ public void setup() throws Exception { public static class GwcWithConcurrencyAnd75PercentCacheHitsState extends AbstractGwcWithConcurrency { - + @Override public void setup() throws Exception { super.setup(); tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 4); @@ -200,7 +223,7 @@ public void setup() throws Exception { public static class GwcWithoutConcurrencyAnd75PercentCacheHitsState extends AbstractGwcWithoutConcurrency { - + @Override public void setup() throws Exception { super.setup(); tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 4); @@ -210,7 +233,7 @@ public void setup() throws Exception { public static class GwcWithConcurrencyAnd90PercentCacheHitsState extends AbstractGwcWithConcurrency { - + @Override public void setup() throws Exception { super.setup(); tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 16); @@ -220,7 +243,7 @@ public void setup() throws Exception { public static class GwcWithoutConcurrencyAnd90PercentCacheHitsState extends AbstractGwcWithoutConcurrency { - + @Override public void setup() throws Exception { super.setup(); tileIndices = getTileIndices(LAYER_NAME, null, 11, 100000, 4, 16); @@ -229,7 +252,7 @@ public void setup() throws Exception { } public static class NoGwcState extends AbstractBenchmarkState { - + @Override public void setup() throws Exception { super.setup(); GWC.get().getConfig().setDirectWMSIntegrationEnabled(false); @@ -440,17 +463,16 @@ private static String buildGetMap(final String layerName, long[] tileIndex) { * @throws IOException */ static void saveToCsv(String location, long[][] tileIndices) throws IOException { - PrintWriter writer = new PrintWriter(new FileWriter(location)); - String gridsetId = "EPSG:4326"; - final GWC gwc = GWC.get(); - final TileLayer tileLayer = gwc.getTileLayerByName(LAYER_NAME); - final GridSubset gridSubset = tileLayer.getGridSubset(gridsetId); - - for (long[] tileIndex : tileIndices) { - BoundingBox bounds = gridSubset.boundsFromIndex(tileIndex); - writer.println(bounds.toString()); + try (PrintWriter writer = new PrintWriter(new FileWriter(location))) { + String gridsetId = "EPSG:4326"; + final GWC gwc = GWC.get(); + final TileLayer tileLayer = gwc.getTileLayerByName(LAYER_NAME); + final GridSubset gridSubset = tileLayer.getGridSubset(gridsetId); + + for (long[] tileIndex : tileIndices) { + BoundingBox bounds = gridSubset.boundsFromIndex(tileIndex); + writer.println(bounds.toString()); + } } - - writer.close(); } } diff --git a/src/gwc/src/test/java/org/geoserver/gwc/layer/GeoServerTileLayerTest.java b/src/gwc/src/test/java/org/geoserver/gwc/layer/GeoServerTileLayerTest.java index 4658567c80b..d04a96679f8 100644 --- a/src/gwc/src/test/java/org/geoserver/gwc/layer/GeoServerTileLayerTest.java +++ b/src/gwc/src/test/java/org/geoserver/gwc/layer/GeoServerTileLayerTest.java @@ -26,7 +26,13 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.awt.Dimension; import java.awt.image.BufferedImage; @@ -576,7 +582,7 @@ public void testGetFeatureInfo() throws Exception { Resource mockResult = mock(Resource.class); ArgumentCaptor argument = ArgumentCaptor.forClass(Map.class); - Mockito.when(mockGWC.dispatchOwsRequest(argument.capture(), any())).thenReturn(mockResult); + when(mockGWC.dispatchOwsRequest(argument.capture(), any())).thenReturn(mockResult); Resource result = layerInfoTileLayer.getFeatureInfo(convTile, bbox, 100, 100, 50, 50); assertSame(mockResult, result); @@ -630,7 +636,7 @@ public void testGetTileJSON() throws Exception { Resource mockResult = mock(Resource.class); ArgumentCaptor argument = ArgumentCaptor.forClass(Map.class); - Mockito.when(mockGWC.dispatchOwsRequest(argument.capture(), any())).thenReturn(mockResult); + when(mockGWC.dispatchOwsRequest(argument.capture(), any())).thenReturn(mockResult); TileJSON result = layerInfoTileLayer.getTileJSON(); assertEquals("test:MockLayerInfoName", result.getName()); @@ -693,7 +699,7 @@ public void testGetTileJSONLayerGroup() throws Exception { Resource mockResult = mock(Resource.class); ArgumentCaptor argument = ArgumentCaptor.forClass(Map.class); - Mockito.when(mockGWC.dispatchOwsRequest(argument.capture(), any())).thenReturn(mockResult); + when(mockGWC.dispatchOwsRequest(argument.capture(), any())).thenReturn(mockResult); TileJSON result = layerGroupInfoVectorTileLayer.getTileJSON(); assertEquals("MockLayerGroupVectors", result.getName()); @@ -770,7 +776,7 @@ public void testGetTileJSONLayerGroupMixed() throws Exception { Resource mockResult = mock(Resource.class); ArgumentCaptor argument = ArgumentCaptor.forClass(Map.class); - Mockito.when(mockGWC.dispatchOwsRequest(argument.capture(), any())).thenReturn(mockResult); + when(mockGWC.dispatchOwsRequest(argument.capture(), any())).thenReturn(mockResult); TileJSON result = layerGroupInfoVectorTileLayer.getTileJSON(); assertEquals("MockLayerGroupMixed", result.getName()); @@ -877,8 +883,7 @@ protected class GetTileMockTester { public GeoServerTileLayer prepareTileLayer() throws Exception { Resource mockResult = mock(Resource.class); ArgumentCaptor argument = ArgumentCaptor.forClass(Map.class); - Mockito.when(mockGWC.dispatchOwsRequest(argument.capture(), any())) - .thenReturn(mockResult); + when(mockGWC.dispatchOwsRequest(argument.capture(), any())).thenReturn(mockResult); return new GeoServerTileLayer(layerInfo, defaults, gridSetBroker); } @@ -934,7 +939,7 @@ protected void performAssertions(ConveyorTile result) throws Exception { @Test public void testGetTile() throws Exception { - long[] tileIndex = new long[] {0, 0, 0}; + long[] tileIndex = {0, 0, 0}; GetTileMockTester tester = new GetTileMockTester(); GeoServerTileLayer tileLayer = tester.prepareTileLayer(); ConveyorTile conveyorTile = tester.prepareConveyorTile(tileLayer, tileIndex); @@ -956,7 +961,7 @@ private FeatureTypeInfo getMockTimeFeatureType() { @Test public void testGetTileWarningNoSkip() throws Exception { // no skips setup, will cache permanently - long[] tileIndex = new long[] {0, 0, 0}; + long[] tileIndex = {0, 0, 0}; GetTileMockTester tester = new GetTileMockTester(); GeoServerTileLayer tileLayer = tester.prepareTileLayer(); @@ -974,7 +979,7 @@ public void testGetTileWarningNoSkip() throws Exception { @Test public void testGetTileWarningMismatchedSkip() throws Exception { // skips on nearest, gets a warning as default, caches permanently - long[] tileIndex = new long[] {0, 0, 0}; + long[] tileIndex = {0, 0, 0}; GetTileMockTester tester = new GetTileMockTester(); GeoServerTileLayer tileLayer = tester.prepareTileLayer(); @@ -992,7 +997,7 @@ public void testGetTileWarningMismatchedSkip() throws Exception { @Test public void testGetTileWarningSkip() throws Exception { // skips on nearest and default, gets a warning as default, no persistent cache occurs - long[] tileIndex = new long[] {0, 0, 0}; + long[] tileIndex = {0, 0, 0}; GetTileMockTester tester = new GetTileMockTester(); GeoServerTileLayer tileLayer = tester.prepareTileLayer(); @@ -1034,7 +1039,7 @@ public void testGetTileWithMetaTilingExecutor() throws Exception { .getGridSubset("EPSG:4326") .getCoverage(zoomLevel); // {minx,miny,max,maxy,zoomlevel} - long[] tileIndex = new long[] {coverage[0], coverage[1], zoomLevel}; + long[] tileIndex = {coverage[0], coverage[1], zoomLevel}; ConveyorTile conveyorTile = tester.prepareConveyorTile(tileLayer, tileIndex); Dispatcher.REQUEST.set(new Request()); @@ -1080,7 +1085,7 @@ public void testGetTileWithMetaTilingExecutorButNoDispatcherRequest() throws Exc .getGridSubset("EPSG:4326") .getCoverage(zoomLevel); // {minx,miny,max,maxy,zoomlevel} - long[] tileIndex = new long[] {coverage[0], coverage[1], zoomLevel}; + long[] tileIndex = {coverage[0], coverage[1], zoomLevel}; ConveyorTile conveyorTile = tester.prepareConveyorTile(tileLayer, tileIndex); Dispatcher.REQUEST.remove(); @@ -1119,7 +1124,7 @@ public void testGetTileWithNullMetaTilingExecutor() throws Exception { .getGridSubset("EPSG:4326") .getCoverage(zoomLevel); // {minx,miny,max,maxy,zoomlevel} - long[] tileIndex = new long[] {coverage[0], coverage[1], zoomLevel}; + long[] tileIndex = {coverage[0], coverage[1], zoomLevel}; ConveyorTile conveyorTile = tester.prepareConveyorTile(tileLayer, tileIndex); GeoServerTileLayer.WEB_MAP.set(tester.prepareFakeMap(1024, 1024)); ConveyorTile result = tileLayer.getTile(conveyorTile); diff --git a/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.html b/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.html index e5b58830e94..1b64621fa58 100644 --- a/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.html +++ b/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.html @@ -39,15 +39,6 @@ Tiles high
  • -
  • -
    - -
    -
    - - Threads -
    -
  • diff --git a/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.java b/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.java index 18b3dfc0bb7..72d4310ed73 100644 --- a/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.java +++ b/src/web/gwc/src/main/java/org/geoserver/gwc/web/CachingOptionsPanel.java @@ -15,7 +15,10 @@ import org.apache.wicket.ajax.form.OnChangeAjaxBehavior; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; -import org.apache.wicket.markup.html.form.*; +import org.apache.wicket.markup.html.form.Check; +import org.apache.wicket.markup.html.form.CheckBox; +import org.apache.wicket.markup.html.form.CheckGroup; +import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.panel.Panel; @@ -109,13 +112,6 @@ protected void onUpdate(AjaxRequestTarget target) { metaTilingY.setRequired(true); configs.add(metaTilingY); - IModel metaTilingThreads = - new PropertyModel<>(gwcConfigModel, "metaTilingThreads"); - TextField metaTilingThreadsTextField = - new TextField<>("metaTilingThreads", metaTilingThreads); - metaTilingThreadsTextField.setRequired(false); - configs.add(metaTilingThreadsTextField); - IModel gutterModel = new PropertyModel<>(gwcConfigModel, "gutter"); List gutterChoices = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 50, 100); diff --git a/src/web/gwc/src/main/java/org/geoserver/gwc/web/GWCServicesPanel.html b/src/web/gwc/src/main/java/org/geoserver/gwc/web/GWCServicesPanel.html index 49250930c24..21937397d76 100644 --- a/src/web/gwc/src/main/java/org/geoserver/gwc/web/GWCServicesPanel.html +++ b/src/web/gwc/src/main/java/org/geoserver/gwc/web/GWCServicesPanel.html @@ -24,6 +24,14 @@
  • +
  • +
    + +
    +
    + +
    +
diff --git a/src/web/gwc/src/main/java/org/geoserver/gwc/web/GWCServicesPanel.java b/src/web/gwc/src/main/java/org/geoserver/gwc/web/GWCServicesPanel.java index c45afe1c95e..d7eab71306a 100644 --- a/src/web/gwc/src/main/java/org/geoserver/gwc/web/GWCServicesPanel.java +++ b/src/web/gwc/src/main/java/org/geoserver/gwc/web/GWCServicesPanel.java @@ -7,9 +7,11 @@ import static org.geoserver.gwc.web.GWCSettingsPage.checkbox; +import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.markup.html.panel.Panel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.PropertyModel; +import org.apache.wicket.validation.validator.RangeValidator; import org.geoserver.gwc.config.GWCConfig; public class GWCServicesPanel extends Panel { @@ -45,5 +47,13 @@ public GWCServicesPanel(final String id, final IModel gwcConfigModel) "enableSecurity", securityEnabledModel, "GWCSettingsPage.enableSecurity.title")); + + IModel metaTilingThreads = + new PropertyModel<>(gwcConfigModel, "metaTilingThreads"); + TextField metaTilingThreadsTextField = + new TextField<>("metaTilingThreads", metaTilingThreads); + metaTilingThreadsTextField.setRequired(false); + metaTilingThreadsTextField.add(RangeValidator.minimum(0)); + add(metaTilingThreadsTextField); } } diff --git a/src/web/gwc/src/main/resources/GeoServerApplication.properties b/src/web/gwc/src/main/resources/GeoServerApplication.properties index b64155079c4..1744ace04d0 100644 --- a/src/web/gwc/src/main/resources/GeoServerApplication.properties +++ b/src/web/gwc/src/main/resources/GeoServerApplication.properties @@ -37,8 +37,7 @@ GWCSettingsPage.cacheNonDefaultStyles=Automatically cache non-default styles GWCSettingsPage.metaTiling=Default metatile size: GWCSettingsPage.metaTilingX=tiles wide by GWCSettingsPage.metaTilingY=tiles high -GWCSettingsPage.metaTilingThreads=Threads available for concurrent metatiling encoding and saving: -GWCSettingsPage.metaTilingThreadsUnits=Threads +GWCSettingsPage.metaTilingThreads=Metatiling threads count (unset for automatic detection) GWCSettingsPage.gutter=Default gutter size in pixels: GWCSettingsPage.defaultCacheOptions=Default Caching Options for GeoServer Layers GWCSettingsPage.defaultCacheFormats=Default Tile Image Formats for: diff --git a/src/web/gwc/src/test/java/org/geoserver/gwc/web/GWCSettingsPageTest.java b/src/web/gwc/src/test/java/org/geoserver/gwc/web/GWCSettingsPageTest.java index 3a414ec6f9d..008f2777217 100644 --- a/src/web/gwc/src/test/java/org/geoserver/gwc/web/GWCSettingsPageTest.java +++ b/src/web/gwc/src/test/java/org/geoserver/gwc/web/GWCSettingsPageTest.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ThreadPoolExecutor; import org.apache.wicket.Component; import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.list.ListView; @@ -574,4 +575,37 @@ public void testSaveWarningSkips() { gwcConfig.getCacheWarningSkips(), Matchers.containsInAnyOrder(WarningType.Default, WarningType.FailedNearest)); } + + @Test + public void testEditMetatilingThreads() { + GWC gwc = GWC.get(); + + // set it to a fixed value + testEditMetatilingThreads("3"); + tester.assertNoErrorMessage(); + assertEquals(3, ((ThreadPoolExecutor) gwc.getMetaTilingExecutor()).getCorePoolSize()); + + // set it an invalid value, error message and no change + testEditMetatilingThreads("-1"); + tester.assertErrorMessages( + "The value of 'Metatiling threads count (unset for automatic detection)' must be at least 0."); + assertEquals(3, ((ThreadPoolExecutor) gwc.getMetaTilingExecutor()).getCorePoolSize()); + + // default is 2 * cores + testEditMetatilingThreads(""); + tester.assertNoErrorMessage(); + int cores = Runtime.getRuntime().availableProcessors(); + assertEquals( + cores * 2, ((ThreadPoolExecutor) gwc.getMetaTilingExecutor()).getCorePoolSize()); + } + + private void testEditMetatilingThreads(String threadCount) { + tester.startPage(GWCSettingsPage.class); + // print(page, true, true); + tester.assertRenderedPage(GWCSettingsPage.class); + + FormTester form = tester.newFormTester("form"); + form.setValue("gwcServicesPanel:metaTilingThreads", threadCount); + form.submit("submit"); + } } From ff7f94d16e86bf97e82761e223aca97a9f4d2d1a Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Fri, 25 Oct 2024 19:30:07 +0200 Subject: [PATCH 14/43] [GEOS-11580] Review feedback --- .../src/main/java/org/geoserver/gwc/GWC.java | 5 +- .../gwc/layer/GeoServerTileLayer.java | 107 ++++++++++-------- 2 files changed, 63 insertions(+), 49 deletions(-) diff --git a/src/gwc/src/main/java/org/geoserver/gwc/GWC.java b/src/gwc/src/main/java/org/geoserver/gwc/GWC.java index 59a9c26f131..e41e83c48f9 100644 --- a/src/gwc/src/main/java/org/geoserver/gwc/GWC.java +++ b/src/gwc/src/main/java/org/geoserver/gwc/GWC.java @@ -1263,10 +1263,9 @@ public void saveConfig(GWCConfig gwcConfig) throws IOException { updateLockProvider(gwcConfig.getLockProviderName()); // Reconfigure the metatiling executor because the thread count might have changed - if (this.metaTilingExecutor != null) { - this.metaTilingExecutor.shutdown(); - } + ExecutorService current = this.metaTilingExecutor; this.metaTilingExecutor = buildMetaTilingExecutor(gwcConfig.getMetaTilingThreads()); + if (current != null) current.shutdown(); } public void saveDiskQuotaConfig(DiskQuotaConfig config, JDBCConfiguration jdbcConfig) diff --git a/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerTileLayer.java b/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerTileLayer.java index a94f8734be2..4ea43d9d3f6 100644 --- a/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerTileLayer.java +++ b/src/gwc/src/main/java/org/geoserver/gwc/layer/GeoServerTileLayer.java @@ -597,33 +597,36 @@ protected ConveyorTile getMetatilingResponse( final GeoServerMetaTile metaTile = createMetaTile(conveyorTile, metaX, metaY); - /* ****************** Acquire lock on metatile ******************* */ - final Lock metaTileLock = - GWC.get().getLockProvider().getLock(buildMetaTileLockKey(conveyorTile, metaTile)); + // should we use the metatile executor? + Executor executor = GWC.get().getMetaTilingExecutor(); + if (Dispatcher.REQUEST.get() == null) { + // Metatiling concurrency is disabled if this isn't a user request. + // Concurrency reduces the user-experienced latency but isn't useful for seeding. + // In fact, it would be harmful for seeding because it makes it more difficult for an + // administrator to control the amount of resource usage for significant seeding jobs. + executor = null; + } + /* ****************** Acquire lock on metatile ******************* */ + final Lock metaTileLock = getLock(buildMetaTileLockKey(conveyorTile, metaTile)); try { boolean foundInCache = false; if (tryCache) { - /* ****************** Acquire lock on individual tile ******************* */ - // Will block here if there is an async thread currently saving this tile - String lockKey = buildTileLockKey(conveyorTile, conveyorTile.getTileIndex()); - final Lock tileLock = GWC.get().getLockProvider().getLock(lockKey); - try { - // After getting the lock on the meta tile and individual tile, try cache again - foundInCache = tryCacheFetch(conveyorTile); - if (foundInCache) { - LOGGER.log( - Level.FINEST, - () -> - "--> " - + Thread.currentThread().getName() - + " returns cache hit for " - + Arrays.toString(metaTile.getMetaGridPos())); - metaTile.dispose(); + // If we have an executor, tiles are saved asynchronously so we need to grab a + // tile lock to wait for the potential tile save to complete. Otherwise just read. + if (executor == null) { + foundInCache = fetchPrimaryTile(conveyorTile, metaTile); + } else { + /* ****************** Acquire lock on individual tile ******************* */ + // Will block here if there is an async thread currently saving this tile + String lockKey = buildTileLockKey(conveyorTile, conveyorTile.getTileIndex()); + final Lock tileLock = getLock(lockKey); + try { + foundInCache = fetchPrimaryTile(conveyorTile, metaTile); + } finally { + /* ****************** Release lock on individual tile ******************* */ + tileLock.release(); } - } finally { - /* ****************** Release lock on individual tile ******************* */ - tileLock.release(); } } @@ -638,7 +641,7 @@ protected ConveyorTile getMetatilingResponse( + " on " + metaTile); try { - computeMetaTile(conveyorTile, metaTile); + computeMetaTile(conveyorTile, metaTile, executor); } catch (Exception e) { Throwables.throwIfInstanceOf(e, GeoWebCacheException.class); throw new GeoWebCacheException("Problem communicating with GeoServer", e); @@ -653,7 +656,32 @@ protected ConveyorTile getMetatilingResponse( return finalizeTile(conveyorTile); } - private void computeMetaTile(ConveyorTile conveyorTile, GeoServerMetaTile metaTile) + /** + * Looks up the primary tile in a given meta-tile (the requested one). If the tile is found it + * means it has been computed since the first check, and the metatile gets disposed in + * preparation for an immediate return. + */ + private boolean fetchPrimaryTile(ConveyorTile conveyorTile, GeoServerMetaTile metaTile) { + // quick return for the simple case + if (!tryCacheFetch(conveyorTile)) return false; + + // otherwise log success, dispose the meta tile and return true + if (LOGGER.isLoggable(Level.FINEST)) { + String threadName = Thread.currentThread().getName(); + String gridPos = Arrays.toString(metaTile.getMetaGridPos()); + LOGGER.finest("--> " + threadName + " returns cache hit for " + gridPos); + } + metaTile.dispose(); + return true; + } + + /** Acquires an exclusive lock for the given key (e.g., for a metatile or individual tile) */ + private Lock getLock(String lockKey) throws GeoWebCacheException { + return GWC.get().getLockProvider().getLock(lockKey); + } + + private void computeMetaTile( + ConveyorTile conveyorTile, GeoServerMetaTile metaTile, Executor executor) throws Exception { WebMap map; long requestTime = System.currentTimeMillis(); @@ -674,16 +702,6 @@ private void computeMetaTile(ConveyorTile conveyorTile, GeoServerMetaTile metaTi final int zoomLevel = (int) gridLoc[2]; final boolean store = this.getExpireCache(zoomLevel) != GWCVars.CACHE_DISABLE_CACHE; - Executor executor = GWC.get().getMetaTilingExecutor(); - - if (Dispatcher.REQUEST.get() == null) { - // Metatiling concurrency is disabled if this isn't a user request. - // Concurrency reduces the user-experienced latency but isn't - // useful for seeding. In fact, it would be harmful for seeding - // because it makes it more difficult for an administrator to - // control the amount of resource usage for significant seeding jobs. - executor = null; - } List> completableFutures = new ArrayList<>(); // A latch to track whether we've locked all the individual tiles or not, before @@ -725,13 +743,13 @@ private void computeMetaTile(ConveyorTile conveyorTile, GeoServerMetaTile metaTi conveyorTile, resource, requestTime)); - if (executor != null) { + if (executor == null) { + // Save in cache on main thread if there's no executor + saveTileTask.run(); + } else { CompletableFuture completableFuture = CompletableFuture.runAsync(saveTileTask, executor); completableFutures.add(completableFuture); - } else { - // Save in cache on main thread if there's no executor - saveTileTask.run(); } } else { // For all other tiles, either encode/save fully asynchronously or @@ -754,14 +772,14 @@ private void computeMetaTile(ConveyorTile conveyorTile, GeoServerMetaTile metaTi gridPos, withRasterCleaner(tileSaver)); - if (executor != null) { + if (executor == null) { + // Run on main thread if there's no executor + encodeAndSaveTask.run(); + } else { // Fully asynchronous CompletableFuture completableFuture = CompletableFuture.runAsync(encodeAndSaveTask, executor); completableFutures.add(completableFuture); - } else { - // Run on main thread if there's no executor - encodeAndSaveTask.run(); } } } @@ -813,10 +831,7 @@ private Runnable withTileLock( Runnable runnable) { return () -> { try { - Lock tileLock = - GWC.get() - .getLockProvider() - .getLock(buildTileLockKey(conveyorTile, gridPosition)); + Lock tileLock = getLock(buildTileLockKey(conveyorTile, gridPosition)); try { tileLockLatch.countDown(); runnable.run(); From aef8bc22b9727ad7f34d8ecfe1d851d37f4eab02 Mon Sep 17 00:00:00 2001 From: etj Date: Wed, 30 Oct 2024 12:07:16 +0100 Subject: [PATCH 15/43] Bump GeoFence to 3.7.2 --- src/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pom.xml b/src/pom.xml index 320357f2b6c..a4ca7d08cab 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -123,7 +123,7 @@ true false true - 3.7-SNAPSHOT + 3.7.2 2.3.2 1.9.3 3.6.9.Final From 54cf2e02cc38a801be21fe43a4747f38292e6191 Mon Sep 17 00:00:00 2001 From: Jody Garnett Date: Wed, 30 Oct 2024 11:35:20 +0100 Subject: [PATCH 16/43] [GEOS-11141] Change docs to workspace admin recommendation The docuemntation was restructured to seperate out administrator from worksapce administrator in response to security vulnerability on log file traversal. --- doc/en/user/source/production/config.rst | 12 ++++++------ doc/en/user/source/webadmin/index.rst | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/en/user/source/production/config.rst b/doc/en/user/source/production/config.rst index 017e1a03930..70bce1349da 100644 --- a/doc/en/user/source/production/config.rst +++ b/doc/en/user/source/production/config.rst @@ -79,21 +79,21 @@ If you would like some users to be able to modify data, set the service level :g If you would like some users to be able to modify some but not all of your data, set the :guilabel:`Service Level` to ``Transactional`` (or ``Complete``), and use :ref:`security_layer` to limit write access to specific layers. Data security can be used to allow write access based on workspace, datastore, or layer security. -GeoServer Data Admin Guidance ------------------------------ +GeoServer Workspace Admin Guidance +---------------------------------- -Establishing a data administrator user is a recommended configuration to privileged users with limited access to the Admin Console to manage the publication of information, but are not intended to be trusted as a GeoServer Administrator with responsibility for the full global settings and system integration controls. +Establishing a workspace administrator user is a recommended configuration providing limited access to the Admin Console to manage the publication of information, but are not intended to be trusted as a GeoServer Administrator with responsibility for the global settings and system integration controls. -1. Create a role to be used for data administration. +1. Create a role to be used for workspace administration. -2. Provide this role to the Users (or Groups) requiring data admin access. +2. Provide this role to the Users (or Groups) requiring workspace admin access. 3. Provide this role :ref:`data security ` admin access ``a`` to: * :ref:`workspace ` administration * :ref:`layer ` administration -4. Recommendation: The combination of data admin permission for a workspace and GROUP_ADMIN access provides a good combination for an individual responsible for a workspace. This provides the ability to manage and control access to the data products in a workspace. +4. Recommendation: The combination of workspace admin permission and GROUP_ADMIN access provides a effective combination for an individual responsible for a workspace. This provides the ability to both manage and control access to the data products in a workspace. GeoServer Administrator Guidance -------------------------------- diff --git a/doc/en/user/source/webadmin/index.rst b/doc/en/user/source/webadmin/index.rst index e30cacfa29d..f211d8e2071 100644 --- a/doc/en/user/source/webadmin/index.rst +++ b/doc/en/user/source/webadmin/index.rst @@ -45,7 +45,7 @@ Data The :ref:`Data` section contains configuration options for all the different data-related settings. -* The :ref:`Layer Preview ` page provides links to layer previews in various output formats, including the common OpenLayers and KML formats. This page helps to visually verify and explore the configuration of a particular layer. +* The :ref:`Layer Preview ` page provides links to layer previews in different output formats, including the frequently used OpenLayers and KML formats. This page helps to visually verify and explore the configuration of a particular layer. **You do not need to be logged into GeoServer to access the Layer Preview.** @@ -61,7 +61,7 @@ The :ref:`Data` section contains configuration options for all the different dat In each of these pages that contain a table, there are three different ways to locate an object: sorting, searching, and paging. To alphabetically sort a data type, click on the column header. For simple searching, enter the search criteria in the search box and hit Enter. And to page through the entries (25 at a time), use the arrow buttons located on the bottom and top of the table. -**These pages are shown to administrators, and users that have data admin permissions.** +**These pages are shown to administrators, and users that have workspace or layer admin permissions.** Services -------- From 5cf0c286a7c8a5b01a9568b8c9ff857bb5a728e9 Mon Sep 17 00:00:00 2001 From: Peter Rushforth Date: Wed, 30 Oct 2024 16:35:03 -0400 Subject: [PATCH 17/43] Use the capabilities getmap / gettile url as the "base" for templates, (#7961) fall back to using the URL of the capabilities document itself. Update MapMLBaseProxyTest.java to check the cascaded request against the request URL from the capabilities document OR if that is null, use the URL OF the capabilities document as the base. Update assertCascading in MapMLWMTSProxyTest.java to compare against resource request URL from capabilities document OR if that is null, use the URL OF the capabilities document as the base. --- .../org/geoserver/mapml/MapMLURLBuilder.java | 43 +++++++++---------- .../geoserver/mapml/MapMLBaseProxyTest.java | 30 +++++++++++-- .../geoserver/mapml/MapMLWMTSProxyTest.java | 31 ++++++++++--- 3 files changed, 73 insertions(+), 31 deletions(-) diff --git a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLURLBuilder.java b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLURLBuilder.java index 23381d82e58..dae8744409f 100644 --- a/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLURLBuilder.java +++ b/src/extension/mapml/src/main/java/org/geoserver/mapml/MapMLURLBuilder.java @@ -67,8 +67,6 @@ public class MapMLURLBuilder { private static final String LAYER = "layer"; private static final String GETMAP = "GETMAP"; private static final String GETFEATUREINFO = "GETFEATUREINFO"; - private static final String WMS = "WMS"; - private static final String GETTILE = "GETTILE"; private static final String SERVICE = "service"; private static final String LAYERS = "layers"; private static final String CRS_PARAM = "crs"; @@ -135,26 +133,9 @@ private boolean canCascade(LayerInfo layerInfo) { if (!MapMLDocumentBuilder.isWMSOrWMTSStore(layerInfo)) return false; if (hasRestrictingAccessLimits(layerInfo)) return false; if (hasVendorParams()) return false; - ResourceInfo resource = layerInfo.getResource(); - StoreInfo storeInfo = resource.getStore(); // Not supporting cross-requests yet: // GetTiles against remote WMS // GetMap against remote WMTS - String service = params.get(SERVICE); - String request = params.get(REQUEST); - if (storeInfo instanceof WMTSStoreInfo) { - if (GETMAP.equalsIgnoreCase(request) - || (GETFEATUREINFO.equalsIgnoreCase(request) - && WMS.equalsIgnoreCase(service))) { - return false; - } - } else if (storeInfo instanceof WMSStoreInfo) { - if (GETTILE.equalsIgnoreCase(request) - || (GETFEATUREINFO.equalsIgnoreCase(request) - && WMTS.equalsIgnoreCase(service))) { - return false; - } - } return TiledCRSConstants.getSupportedOutputCRS(proj) != null; } return false; @@ -184,6 +165,7 @@ private String generateURL(String path, HashMap params, LayerInf boolean isSupportedOutputCRS = outputCRS != null; if (resourceInfo != null) { String capabilitiesURL = null; + URL getResourceURL = null; String tileMatrixSet = null; StoreInfo storeInfo = resourceInfo.getStore(); String requestedCRS = isSupportedOutputCRS ? outputCRS : proj; @@ -195,6 +177,7 @@ private String generateURL(String path, HashMap params, LayerInf try { WMSCapabilities capabilities = wmsStoreInfo.getWebMapServer(null).getCapabilities(); + getResourceURL = capabilities.getRequest().getGetMap().getGet(); version = capabilities.getVersion(); List layerList = capabilities.getLayerList(); // Check on GetFeatureInfo @@ -241,6 +224,7 @@ private String generateURL(String path, HashMap params, LayerInf try { WMTSCapabilities capabilities = wmtsStoreInfo.getWebMapTileServer(null).getCapabilities(); + getResourceURL = capabilities.getRequest().getGetTile().getGet(); version = capabilities.getVersion(); List layerList = capabilities.getLayerList(); // Check on GetFeatureInfo @@ -284,9 +268,24 @@ private String generateURL(String path, HashMap params, LayerInf if (cascadeToRemote) { // if we reach this point, we can finally cascade. // Let's update all the params for the cascading - String[] baseUrlAndPath = getBaseUrlAndPath(capabilitiesURL); - baseUrl = baseUrlAndPath[0]; - path = baseUrlAndPath[1]; + // getResourceURL may be null if the capabilities doc is misconfigured; + if (getResourceURL != null) { + baseUrl = + getResourceURL.getProtocol() + + "://" + + getResourceURL.getHost() + + (getResourceURL.getPort() == -1 + ? "" + : ":" + getResourceURL.getPort()) + + "/"; + + path = getResourceURL.getPath(); + } else { + // if misconfigured capabilites, use cap document URL as base + String[] baseUrlAndPath = getBaseUrlAndPath(capabilitiesURL); + baseUrl = baseUrlAndPath[0]; + path = baseUrlAndPath[1]; + } urlType = URLMangler.URLType.EXTERNAL; updateRequestParams( params, layerName, version, requestedCRS, tileMatrixSet, infoFormats); diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLBaseProxyTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLBaseProxyTest.java index 4d84bb11989..ec497837d7f 100644 --- a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLBaseProxyTest.java +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLBaseProxyTest.java @@ -13,9 +13,13 @@ import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import java.io.ByteArrayInputStream; +import java.net.URL; import java.util.Map; +import java.util.regex.Pattern; import org.custommonkey.xmlunit.XMLUnit; import org.custommonkey.xmlunit.XpathEngine; +import org.geotools.ows.wms.WMSCapabilities; +import org.geotools.ows.wms.WebMapServer; import org.junit.AfterClass; import org.junit.Before; import org.springframework.http.MediaType; @@ -105,11 +109,29 @@ protected void checkCascading( } } - protected void assertCascading(boolean shouldCascade, String url) { + protected void assertCascading(boolean shouldCascade, String url) throws Exception { + if (shouldCascade) { - assertTrue( - url.startsWith( - "http://localhost:" + mockService.port() + MOCK_SERVER + CONTEXT)); + URL getResourceURL = null; + Pattern serviceTypeRE = Pattern.compile(".*SERVICE=WMS.*", Pattern.CASE_INSENSITIVE); + boolean cascadingWMS = serviceTypeRE.matcher(getCapabilitiesURL()).find(); + assertTrue(cascadingWMS); + WebMapServer wms = new WebMapServer(new URL(getCapabilitiesURL())); + WMSCapabilities capabilities = wms.getCapabilities(); + getResourceURL = capabilities.getRequest().getGetMap().getGet(); + URL baseResourceURL = + getResourceURL != null ? getResourceURL : new URL(getCapabilitiesURL()); + URL base = + new URL( + baseResourceURL.getProtocol() + + "://" + + baseResourceURL.getHost() + + (baseResourceURL.getPort() == -1 + ? "" + : ":" + baseResourceURL.getPort()) + + "/"); + String path = baseResourceURL.getPath(); + assertTrue(url.startsWith((new URL(base, path)).toString())); assertTrue(url.contains("layers=topp:states")); } else { assertTrue(url.startsWith("http://localhost:8080/geoserver" + CONTEXT)); diff --git a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMTSProxyTest.java b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMTSProxyTest.java index 63db249b3ed..11ebced9ba3 100644 --- a/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMTSProxyTest.java +++ b/src/extension/mapml/src/test/java/org/geoserver/mapml/MapMLWMTSProxyTest.java @@ -4,11 +4,14 @@ */ package org.geoserver.mapml; +import static org.geoserver.mapml.MapMLBaseProxyTest.getCapabilitiesURL; import static org.geowebcache.grid.GridSubsetFactory.createGridSubSet; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import java.net.URL; +import java.util.regex.Pattern; import org.geoserver.catalog.Catalog; import org.geoserver.catalog.LayerInfo; import org.geoserver.catalog.ResourceInfo; @@ -19,6 +22,8 @@ import org.geoserver.gwc.config.GWCConfig; import org.geoserver.gwc.layer.GeoServerTileLayer; import org.geoserver.mapml.gwc.gridset.MapMLGridsets; +import org.geotools.ows.wmts.WebMapTileServer; +import org.geotools.ows.wmts.model.WMTSCapabilities; import org.geowebcache.grid.GridSubset; import org.geowebcache.mime.TextMime; import org.junit.BeforeClass; @@ -118,15 +123,31 @@ public void testRemoteVsNotRemote() throws Exception { } @Override - protected void assertCascading(boolean shouldCascade, String url) { - assertTrue(url.contains("service=WMTS")); + protected void assertCascading(boolean shouldCascade, String url) throws Exception { + URL getResourceURL = null; + Pattern serviceTypeRE = Pattern.compile(".*SERVICE=WMTS.*", Pattern.CASE_INSENSITIVE); + boolean isWMTSService = serviceTypeRE.matcher(getCapabilitiesURL()).find(); + assertTrue(isWMTSService); if (shouldCascade) { + WebMapTileServer wmts = new WebMapTileServer(new URL(getCapabilitiesURL())); + WMTSCapabilities capabilities = wmts.getCapabilities(); + getResourceURL = capabilities.getRequest().getGetTile().getGet(); + URL baseResourceURL = + getResourceURL != null ? getResourceURL : new URL(getCapabilitiesURL()); + URL base = + new URL( + baseResourceURL.getProtocol() + + "://" + + baseResourceURL.getHost() + + (baseResourceURL.getPort() == -1 + ? "" + : ":" + baseResourceURL.getPort()) + + "/"); + String path = baseResourceURL.getPath(); + assertTrue(url.startsWith((new URL(base, path)).toString())); // The remote capabilities defines a custom GridSet that matches // the OSMTILE with a different name: MATCHING_OSMTILE // Identifiers are also not simple numbers but contain a common prefix. - assertTrue( - url.startsWith( - "http://localhost:" + mockService.port() + MOCK_SERVER + CONTEXT)); assertTrue(url.contains("layer=topp:states")); assertTrue(url.contains("tilematrixset=MATCHING_OSMTILE")); // Common prefix has been pre-pended to the tilematrix z input From c1a4f755be3042efaa04a640461c44b54213a31a Mon Sep 17 00:00:00 2001 From: "Ikeoka, Steve" Date: Tue, 29 Oct 2024 12:33:01 -0700 Subject: [PATCH 18/43] [GEOS-11590] Upgrade log4j to 2.24.1 and slf4j to 2.0.16 --- src/community/netcdf-ghrsst/pom.xml | 2 +- src/community/taskmanager/core/bin/pom.xml | 4 +--- src/community/taskmanager/core/pom.xml | 3 +-- src/extension/importer/core/pom.xml | 2 +- src/extension/netcdf-out/pom.xml | 2 +- src/extension/vectortiles/pom.xml | 2 +- src/platform/pom.xml | 3 +-- src/pom.xml | 9 +++++++-- src/release/pom.xml | 2 +- src/security/ldap/pom.xml | 2 +- src/web/core/pom.xml | 2 +- 11 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/community/netcdf-ghrsst/pom.xml b/src/community/netcdf-ghrsst/pom.xml index 3e1faf9fd38..fe7f44a3fa6 100644 --- a/src/community/netcdf-ghrsst/pom.xml +++ b/src/community/netcdf-ghrsst/pom.xml @@ -30,7 +30,7 @@ org.apache.logging.log4j - log4j-slf4j-impl + log4j-slf4j2-impl diff --git a/src/community/taskmanager/core/bin/pom.xml b/src/community/taskmanager/core/bin/pom.xml index 11caab4305d..a55b3f88f8a 100644 --- a/src/community/taskmanager/core/bin/pom.xml +++ b/src/community/taskmanager/core/bin/pom.xml @@ -66,13 +66,11 @@ org.apache.logging.log4j - log4j-slf4j-impl - ${log4j.version} + log4j-slf4j2-impl org.apache.logging.log4j log4j-jcl - ${log4j.version} diff --git a/src/community/taskmanager/core/pom.xml b/src/community/taskmanager/core/pom.xml index 15d10c65328..95aa7faad05 100644 --- a/src/community/taskmanager/core/pom.xml +++ b/src/community/taskmanager/core/pom.xml @@ -75,8 +75,7 @@ org.apache.logging.log4j - log4j-slf4j-impl - ${log4j.version} + log4j-slf4j2-impl org.geotools.jdbc diff --git a/src/extension/importer/core/pom.xml b/src/extension/importer/core/pom.xml index 16b78d3d8e2..7873303e963 100644 --- a/src/extension/importer/core/pom.xml +++ b/src/extension/importer/core/pom.xml @@ -63,7 +63,7 @@ org.apache.logging.log4j - log4j-slf4j-impl + log4j-slf4j2-impl test diff --git a/src/extension/netcdf-out/pom.xml b/src/extension/netcdf-out/pom.xml index dbb9407cf1c..09131698d63 100644 --- a/src/extension/netcdf-out/pom.xml +++ b/src/extension/netcdf-out/pom.xml @@ -131,7 +131,7 @@ org.slf4j - log4j-slf4j-impl + log4j-slf4j2-impl org.apache.logging.log4j diff --git a/src/extension/vectortiles/pom.xml b/src/extension/vectortiles/pom.xml index f6bdfa30701..0a6f2357f9c 100644 --- a/src/extension/vectortiles/pom.xml +++ b/src/extension/vectortiles/pom.xml @@ -62,7 +62,7 @@ org.apache.logging.log4j - log4j-slf4j-impl + log4j-slf4j2-impl test diff --git a/src/platform/pom.xml b/src/platform/pom.xml index 7635c055651..fc9d01b4deb 100644 --- a/src/platform/pom.xml +++ b/src/platform/pom.xml @@ -81,12 +81,11 @@ org.apache.logging.log4j - log4j-slf4j-impl + log4j-slf4j2-impl org.apache.logging.log4j log4j-1.2-api - ${log4j.version} diff --git a/src/pom.xml b/src/pom.xml index a4ca7d08cab..8485f830a2b 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -131,7 +131,7 @@ 1.1.3.2 0.9 - 2.17.2 + 2.24.1 2.8.1 false true @@ -1003,9 +1003,14 @@ org.apache.logging.log4j - log4j-slf4j-impl + log4j-slf4j2-impl ${log4j.version} + + org.slf4j + slf4j-api + 2.0.16 + org.apache.logging.log4j diff --git a/src/release/pom.xml b/src/release/pom.xml index e240716c6a2..ad17e3ed9c7 100644 --- a/src/release/pom.xml +++ b/src/release/pom.xml @@ -73,7 +73,7 @@ org.apache.logging.log4j - log4j-slf4j-impl + log4j-slf4j2-impl org.apache.logging.log4j diff --git a/src/security/ldap/pom.xml b/src/security/ldap/pom.xml index 287dab5f8ac..65655e8e578 100644 --- a/src/security/ldap/pom.xml +++ b/src/security/ldap/pom.xml @@ -32,7 +32,7 @@ org.apache.logging.log4j - log4j-slf4j-impl + log4j-slf4j2-impl org.springframework.ldap diff --git a/src/web/core/pom.xml b/src/web/core/pom.xml index 3afff32dabc..b462081350d 100644 --- a/src/web/core/pom.xml +++ b/src/web/core/pom.xml @@ -42,7 +42,7 @@ org.apache.logging.log4j - log4j-slf4j-impl + log4j-slf4j2-impl org.geoserver From 9e0f31b7ea886cfd4cbe3938154df8c7e1fbbb47 Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Mon, 28 Oct 2024 11:39:51 +0100 Subject: [PATCH 19/43] More Windows VM tolerance for this test --- .../platform/resource/FileSystemResourceTheoryTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/platform/src/test/java/org/geoserver/platform/resource/FileSystemResourceTheoryTest.java b/src/platform/src/test/java/org/geoserver/platform/resource/FileSystemResourceTheoryTest.java index cc22f16bc3c..bda899c28ed 100644 --- a/src/platform/src/test/java/org/geoserver/platform/resource/FileSystemResourceTheoryTest.java +++ b/src/platform/src/test/java/org/geoserver/platform/resource/FileSystemResourceTheoryTest.java @@ -49,10 +49,10 @@ public class FileSystemResourceTheoryTest extends ResourceTheoryTest { /** - * On a local machine this is a long wait, but Github action VMs are slow and erratic, let's - * give it more time + * On a local machine this would be long wait, but Github action VMs are slow and erratic, let's + * give them more time */ - private static final int MAX_WAIT_SEC = 20; + private static final int MAX_WAIT_SEC = 60; FileSystemResourceStore store; From 95852c258b59c106a2b81898ddd6812663d6e078 Mon Sep 17 00:00:00 2001 From: Gabriel Roldan Date: Tue, 6 Aug 2024 12:56:24 -0300 Subject: [PATCH 20/43] Declare all GeoTools dependencies in the root pom's dependency management section * Centralize the declaration of geotools dependencies to the root pom's dependencyManagement and remove `${gt.version}` in module poms for consistency. * Exclude `xml-apis` and apache `fop` in the root pom dependency management section Of especial interes for the `gt-app-schema` module not to carry over the unnecessary (sice Java 1.6) `xml-apis` jar, whose presence also makes the eclipse IDE complain there are multiple sources for classes in the `javax.xml.namespace`, `org.w3c.dom`, and other packages. commit 397981a4e9dc310219270f82b2e560f6d3de4127 Author: Gabriel Roldan Date: Tue Aug 6 12:05:38 2024 -0300 --- .../app-schema/webservice-test/pom.xml | 4 - src/community/backup-restore/pom.xml | 1 - src/community/cog/pom.xml | 2 - src/community/datadir-catalog-loader/pom.xml | 1 - src/community/dds/pom.xml | 1 - src/community/elasticsearch/pom.xml | 1 - .../features-autopopulate-core/pom.xml | 5 - .../features-templating-core/pom.xml | 11 - .../features-templating-ogcapi/pom.xml | 6 - .../features-templating-ows/pom.xml | 6 - .../features-templating-rest/pom.xml | 1 - .../features-templating-web/pom.xml | 1 - src/community/flatgeobuf/pom.xml | 1 - src/community/geopkg/pom.xml | 4 - src/community/graticule/pom.xml | 1 - src/community/gsr/pom.xml | 2 - src/community/importer-jdbc/pom.xml | 2 - src/community/mbtiles-store/pom.xml | 1 - src/community/mbtiles/pom.xml | 3 - src/community/ncwms/pom.xml | 2 - src/community/netcdf-ghrsst/pom.xml | 1 - .../ogcapi/dggs/dggs-clickhouse/pom.xml | 4 - src/community/ogcapi/dggs/dggs-core/pom.xml | 4 - src/community/ogcapi/ogcapi-changeset/pom.xml | 1 - src/community/ogcapi/ogcapi-core/pom.xml | 4 - src/community/ogcapi/ogcapi-coverages/pom.xml | 1 - src/community/ogcapi/ogcapi-features/pom.xml | 3 - src/community/ogcapi/ogcapi-images/pom.xml | 1 - src/community/ogcapi/ogcapi-styles/pom.xml | 1 - .../ogcapi/ogcapi-tiled-features/pom.xml | 1 - src/community/ogcapi/ogcapi-tiles/pom.xml | 1 - src/community/oseo/oseo-core/pom.xml | 2 - src/community/oseo/oseo-rest/pom.xml | 3 - src/community/oseo/oseo-service/pom.xml | 1 - src/community/oseo/web-oseo/pom.xml | 1 - src/community/s3-geotiff/pom.xml | 1 - .../mongodb-schemaless/pom.xml | 3 - .../schemaless-core/pom.xml | 2 - src/community/smart-data-loader/pom.xml | 5 - src/community/solr/pom.xml | 1 - src/community/stac-datastore/pom.xml | 1 - src/community/taskmanager/core/pom.xml | 1 - src/community/taskmanager/s3/pom.xml | 1 - src/community/vector-mosaic/pom.xml | 1 - src/community/vsi/pom.xml | 1 - src/community/web-ogr/pom.xml | 1 - src/community/wps-sextante/pom.xml | 1 - .../app-schema/app-schema-core/pom.xml | 6 - .../app-schema/app-schema-geopkg-test/pom.xml | 6 - .../app-schema/app-schema-mongo-test/pom.xml | 3 - .../app-schema/app-schema-oracle-test/pom.xml | 6 - .../app-schema-postgis-test/pom.xml | 6 - .../app-schema/app-schema-solr-test/pom.xml | 4 - .../app-schema/app-schema-test/pom.xml | 6 - .../sample-data-access-test/pom.xml | 1 - src/extension/css/pom.xml | 1 - src/extension/csw/api/pom.xml | 4 - src/extension/feature-pregeneralized/pom.xml | 1 - .../geofence/geofence-server/pom.xml | 1 - src/extension/geopkg-output/pom.xml | 1 - src/extension/grib/pom.xml | 2 - src/extension/importer/core/pom.xml | 3 - src/extension/importer/pom.xml | 1 - src/extension/libjpeg-turbo/pom.xml | 1 - src/extension/mapml/pom.xml | 1 - src/extension/mbstyle/pom.xml | 1 - src/extension/netcdf-out/pom.xml | 1 - src/extension/netcdf/pom.xml | 1 - src/extension/rat/pom.xml | 1 - src/extension/wps-download/pom.xml | 2 - src/extension/wps-jdbc/pom.xml | 3 - src/extension/wps/wps-core/pom.xml | 13 -- src/extension/wps/wps-kml-ppio/pom.xml | 1 - src/extension/ysld/pom.xml | 2 - src/gwc/pom.xml | 1 - src/main/pom.xml | 2 - src/pom.xml | 209 ++++++++++++++++++ src/release/pom.xml | 4 - src/restconfig/pom.xml | 1 - src/web/app/pom.xml | 8 - src/web/core/pom.xml | 1 - src/web/wms/pom.xml | 1 - src/wfs/pom.xml | 1 - src/wms/pom.xml | 14 -- 84 files changed, 209 insertions(+), 216 deletions(-) diff --git a/src/community/app-schema/webservice-test/pom.xml b/src/community/app-schema/webservice-test/pom.xml index 9339032858e..7f8f693428e 100644 --- a/src/community/app-schema/webservice-test/pom.xml +++ b/src/community/app-schema/webservice-test/pom.xml @@ -46,12 +46,10 @@ org.geotools gt-webservice - ${gt.version} org.geotools gt-webservice - ${gt.version} tests test @@ -62,7 +60,6 @@ org.geotools gt-jdbc - ${gt.version} test-jar test @@ -73,7 +70,6 @@ org.geotools gt-app-schema - ${gt.version} tests test diff --git a/src/community/backup-restore/pom.xml b/src/community/backup-restore/pom.xml index 7c837b961a6..fa6fb0edf50 100644 --- a/src/community/backup-restore/pom.xml +++ b/src/community/backup-restore/pom.xml @@ -51,7 +51,6 @@ org.geotools.jdbc gt-jdbc-h2 - ${gt.version} test diff --git a/src/community/cog/pom.xml b/src/community/cog/pom.xml index 937b2ec10ed..21bd65235a6 100644 --- a/src/community/cog/pom.xml +++ b/src/community/cog/pom.xml @@ -30,7 +30,6 @@ org.geotools gt-geotiff - ${gt.version} org.geoserver.web @@ -57,7 +56,6 @@ org.geotools.jdbc gt-jdbc-h2 - ${gt.version} test diff --git a/src/community/datadir-catalog-loader/pom.xml b/src/community/datadir-catalog-loader/pom.xml index a6d57bfc929..9e9c558a920 100644 --- a/src/community/datadir-catalog-loader/pom.xml +++ b/src/community/datadir-catalog-loader/pom.xml @@ -33,7 +33,6 @@ org.geotools.jdbc gt-jdbc-postgis - ${gt.version} test diff --git a/src/community/dds/pom.xml b/src/community/dds/pom.xml index f8437bb1d36..9c1e91ccb4a 100644 --- a/src/community/dds/pom.xml +++ b/src/community/dds/pom.xml @@ -60,7 +60,6 @@ org.geotools gt-sample-data - ${gt.version} org.geoserver diff --git a/src/community/elasticsearch/pom.xml b/src/community/elasticsearch/pom.xml index 117e7c9e193..47232d3bc54 100644 --- a/src/community/elasticsearch/pom.xml +++ b/src/community/elasticsearch/pom.xml @@ -20,7 +20,6 @@ org.geotools gt-elasticsearch - ${gt.version} diff --git a/src/community/features-autopopulate/features-autopopulate-core/pom.xml b/src/community/features-autopopulate/features-autopopulate-core/pom.xml index 806b58cbd7f..53ff0507bc6 100644 --- a/src/community/features-autopopulate/features-autopopulate-core/pom.xml +++ b/src/community/features-autopopulate/features-autopopulate-core/pom.xml @@ -33,7 +33,6 @@ org.geotools gt-main - ${gt.version} org.geoserver @@ -57,26 +56,22 @@ org.geotools gt-sample-data - ${gt.version} test org.geotools gt-jdbc - ${gt.version} tests test org.geotools gt-jdbc - ${gt.version} test org.geotools.jdbc gt-jdbc-postgis - ${gt.version} test diff --git a/src/community/features-templating/features-templating-core/pom.xml b/src/community/features-templating/features-templating-core/pom.xml index a0b7dae6949..6df84057e8c 100644 --- a/src/community/features-templating/features-templating-core/pom.xml +++ b/src/community/features-templating/features-templating-core/pom.xml @@ -33,17 +33,14 @@ org.geotools gt-main - ${gt.version} org.geotools gt-complex - ${gt.version} org.geotools gt-geojson-core - ${gt.version} com.github.jsonld-java @@ -53,17 +50,14 @@ org.geotools gt-app-schema - ${gt.version} org.geotools gt-http - ${gt.version} org.geotools gt-referencing - ${gt.version} org.geoserver @@ -88,33 +82,28 @@ org.geotools gt-app-schema - ${gt.version} tests test org.geotools gt-sample-data - ${gt.version} test org.geotools gt-jdbc - ${gt.version} tests test org.geotools gt-jdbc - ${gt.version} test org.geotools.jdbc gt-jdbc-postgis - ${gt.version} test diff --git a/src/community/features-templating/features-templating-ogcapi/pom.xml b/src/community/features-templating/features-templating-ogcapi/pom.xml index 61a213fc49a..305317c1e52 100644 --- a/src/community/features-templating/features-templating-ogcapi/pom.xml +++ b/src/community/features-templating/features-templating-ogcapi/pom.xml @@ -75,13 +75,11 @@ org.geotools gt-app-schema - ${gt.version} test org.geotools gt-app-schema - ${gt.version} tests test @@ -95,26 +93,22 @@ org.geotools gt-sample-data - ${gt.version} test org.geotools gt-jdbc - ${gt.version} tests test org.geotools gt-jdbc - ${gt.version} test org.geotools.jdbc gt-jdbc-postgis - ${gt.version} test diff --git a/src/community/features-templating/features-templating-ows/pom.xml b/src/community/features-templating/features-templating-ows/pom.xml index fc27d6c1828..c158b545915 100644 --- a/src/community/features-templating/features-templating-ows/pom.xml +++ b/src/community/features-templating/features-templating-ows/pom.xml @@ -71,7 +71,6 @@ org.geotools gt-app-schema - ${gt.version} test @@ -91,33 +90,28 @@ org.geotools gt-app-schema - ${gt.version} tests test org.geotools gt-sample-data - ${gt.version} test org.geotools gt-jdbc - ${gt.version} tests test org.geotools gt-jdbc - ${gt.version} test org.geotools.jdbc gt-jdbc-postgis - ${gt.version} test diff --git a/src/community/features-templating/features-templating-rest/pom.xml b/src/community/features-templating/features-templating-rest/pom.xml index 78dc44d4360..0a5fb99931a 100644 --- a/src/community/features-templating/features-templating-rest/pom.xml +++ b/src/community/features-templating/features-templating-rest/pom.xml @@ -37,7 +37,6 @@ org.geotools gt-http - ${gt.version} org.geoserver diff --git a/src/community/features-templating/features-templating-web/pom.xml b/src/community/features-templating/features-templating-web/pom.xml index fc062f9a3ba..6bff437ac36 100644 --- a/src/community/features-templating/features-templating-web/pom.xml +++ b/src/community/features-templating/features-templating-web/pom.xml @@ -32,7 +32,6 @@ org.geotools gt-http - ${gt.version} org.geoserver diff --git a/src/community/flatgeobuf/pom.xml b/src/community/flatgeobuf/pom.xml index 9b28f2e11a3..7304f95541b 100644 --- a/src/community/flatgeobuf/pom.xml +++ b/src/community/flatgeobuf/pom.xml @@ -26,7 +26,6 @@ org.geotools gt-flatgeobuf - ${gt.version} diff --git a/src/community/geopkg/pom.xml b/src/community/geopkg/pom.xml index b4217808393..216cc7e3b7d 100644 --- a/src/community/geopkg/pom.xml +++ b/src/community/geopkg/pom.xml @@ -50,17 +50,14 @@ org.geotools gt-process - ${gt.version} org.geotools gt-geopkg - ${gt.version} org.geotools gt-render - ${gt.version} org.geoserver.extension @@ -96,7 +93,6 @@ org.geotools gt-sample-data - ${gt.version} test diff --git a/src/community/graticule/pom.xml b/src/community/graticule/pom.xml index 96ca91559d9..309a42debfa 100644 --- a/src/community/graticule/pom.xml +++ b/src/community/graticule/pom.xml @@ -47,7 +47,6 @@ org.geotools gt-graticule - ${gt.version} org.apache.wicket diff --git a/src/community/gsr/pom.xml b/src/community/gsr/pom.xml index 1a743f046c4..460383398ff 100644 --- a/src/community/gsr/pom.xml +++ b/src/community/gsr/pom.xml @@ -43,7 +43,6 @@ org.geotools gt-main - ${gt.version} net.sf.json-lib @@ -91,7 +90,6 @@ org.geotools gt-main - ${gt.version} tests test diff --git a/src/community/importer-jdbc/pom.xml b/src/community/importer-jdbc/pom.xml index 605d2b5923b..71ae7e521b4 100644 --- a/src/community/importer-jdbc/pom.xml +++ b/src/community/importer-jdbc/pom.xml @@ -20,7 +20,6 @@ org.geotools gt-transform - ${gt.version} org.geoserver.importer @@ -65,7 +64,6 @@ org.geotools gt-sample-data - ${gt.version} test diff --git a/src/community/mbtiles-store/pom.xml b/src/community/mbtiles-store/pom.xml index b5186b11ecb..86eb88627a6 100644 --- a/src/community/mbtiles-store/pom.xml +++ b/src/community/mbtiles-store/pom.xml @@ -20,7 +20,6 @@ org.geotools gt-mbtiles - ${gt.version} diff --git a/src/community/mbtiles/pom.xml b/src/community/mbtiles/pom.xml index 89a9bd5fef6..2b261e4fdbe 100644 --- a/src/community/mbtiles/pom.xml +++ b/src/community/mbtiles/pom.xml @@ -40,12 +40,10 @@ org.geotools gt-mbtiles - ${gt.version} org.geotools gt-render - ${gt.version} org.geoserver.extension @@ -81,7 +79,6 @@ org.geotools gt-sample-data - ${gt.version} test diff --git a/src/community/ncwms/pom.xml b/src/community/ncwms/pom.xml index 72f32cb2d42..609dfac311c 100644 --- a/src/community/ncwms/pom.xml +++ b/src/community/ncwms/pom.xml @@ -22,7 +22,6 @@ org.geotools gt-brewer - ${gt.version} org.geoserver.community @@ -72,7 +71,6 @@ org.geotools gt-iau-wkt - ${gt.version} test diff --git a/src/community/netcdf-ghrsst/pom.xml b/src/community/netcdf-ghrsst/pom.xml index fe7f44a3fa6..802fe0e8db1 100644 --- a/src/community/netcdf-ghrsst/pom.xml +++ b/src/community/netcdf-ghrsst/pom.xml @@ -42,7 +42,6 @@ org.geotools gt-netcdf - ${gt.version} slf4j-api diff --git a/src/community/ogcapi/dggs/dggs-clickhouse/pom.xml b/src/community/ogcapi/dggs/dggs-clickhouse/pom.xml index d87a4d2c25d..83abbfe6e13 100644 --- a/src/community/ogcapi/dggs/dggs-clickhouse/pom.xml +++ b/src/community/ogcapi/dggs/dggs-clickhouse/pom.xml @@ -31,7 +31,6 @@ org.geotools gt-jdbc - ${gt.version} com.clickhouse @@ -46,20 +45,17 @@ org.geotools gt-jdbc - ${gt.version} tests test org.geotools gt-sample-data - ${gt.version} test org.geotools gt-epsg-hsql - ${gt.version} test diff --git a/src/community/ogcapi/dggs/dggs-core/pom.xml b/src/community/ogcapi/dggs/dggs-core/pom.xml index b955f77602f..ee47db7ec6a 100644 --- a/src/community/ogcapi/dggs/dggs-core/pom.xml +++ b/src/community/ogcapi/dggs/dggs-core/pom.xml @@ -30,12 +30,10 @@ org.geotools gt-main - ${gt.version} org.geotools gt-render - ${gt.version} @@ -52,13 +50,11 @@ org.geotools gt-epsg-hsql - ${gt.version} test org.geotools gt-shapefile - ${gt.version} test diff --git a/src/community/ogcapi/ogcapi-changeset/pom.xml b/src/community/ogcapi/ogcapi-changeset/pom.xml index 2d0283f34ef..6e1b0d67f6e 100644 --- a/src/community/ogcapi/ogcapi-changeset/pom.xml +++ b/src/community/ogcapi/ogcapi-changeset/pom.xml @@ -36,7 +36,6 @@ org.geotools.jdbc gt-jdbc-h2 - ${gt.version} org.geoserver diff --git a/src/community/ogcapi/ogcapi-core/pom.xml b/src/community/ogcapi/ogcapi-core/pom.xml index 2edee21560d..d57df177b4f 100644 --- a/src/community/ogcapi/ogcapi-core/pom.xml +++ b/src/community/ogcapi/ogcapi-core/pom.xml @@ -86,7 +86,6 @@ org.geotools gt-geojson-core - ${gt.version} org.geoserver @@ -129,7 +128,6 @@ org.geotools gt-jdbc - ${gt.version} test-jar test @@ -148,13 +146,11 @@ org.geotools gt-app-schema - ${gt.version} test org.geotools gt-app-schema - ${gt.version} tests test diff --git a/src/community/ogcapi/ogcapi-coverages/pom.xml b/src/community/ogcapi/ogcapi-coverages/pom.xml index 8125c78c4f4..ae2ade03fcd 100644 --- a/src/community/ogcapi/ogcapi-coverages/pom.xml +++ b/src/community/ogcapi/ogcapi-coverages/pom.xml @@ -56,7 +56,6 @@ org.geotools gt-jdbc - ${gt.version} test-jar test diff --git a/src/community/ogcapi/ogcapi-features/pom.xml b/src/community/ogcapi/ogcapi-features/pom.xml index 9500d9c75c6..0cf0e21fe32 100644 --- a/src/community/ogcapi/ogcapi-features/pom.xml +++ b/src/community/ogcapi/ogcapi-features/pom.xml @@ -61,7 +61,6 @@ org.geotools gt-jdbc - ${gt.version} test-jar test @@ -75,13 +74,11 @@ org.geotools gt-app-schema - ${gt.version} test org.geotools gt-app-schema - ${gt.version} tests test diff --git a/src/community/ogcapi/ogcapi-images/pom.xml b/src/community/ogcapi/ogcapi-images/pom.xml index 575b2aa8e9d..294bf6f77d6 100644 --- a/src/community/ogcapi/ogcapi-images/pom.xml +++ b/src/community/ogcapi/ogcapi-images/pom.xml @@ -42,7 +42,6 @@ org.geotools.jdbc gt-jdbc-h2 - ${gt.version} org.geoserver diff --git a/src/community/ogcapi/ogcapi-styles/pom.xml b/src/community/ogcapi/ogcapi-styles/pom.xml index e994c5e2616..1533e0610c0 100644 --- a/src/community/ogcapi/ogcapi-styles/pom.xml +++ b/src/community/ogcapi/ogcapi-styles/pom.xml @@ -61,7 +61,6 @@ org.geotools gt-jdbc - ${gt.version} test-jar test diff --git a/src/community/ogcapi/ogcapi-tiled-features/pom.xml b/src/community/ogcapi/ogcapi-tiled-features/pom.xml index 3130c16acf6..9f8e2dedf44 100644 --- a/src/community/ogcapi/ogcapi-tiled-features/pom.xml +++ b/src/community/ogcapi/ogcapi-tiled-features/pom.xml @@ -55,7 +55,6 @@ org.geotools gt-jdbc - ${gt.version} test-jar test diff --git a/src/community/ogcapi/ogcapi-tiles/pom.xml b/src/community/ogcapi/ogcapi-tiles/pom.xml index 4729b4eb50d..b1b23a26817 100644 --- a/src/community/ogcapi/ogcapi-tiles/pom.xml +++ b/src/community/ogcapi/ogcapi-tiles/pom.xml @@ -73,7 +73,6 @@ org.geotools gt-jdbc - ${gt.version} test-jar test diff --git a/src/community/oseo/oseo-core/pom.xml b/src/community/oseo/oseo-core/pom.xml index a514498ef30..983bf6da3c0 100644 --- a/src/community/oseo/oseo-core/pom.xml +++ b/src/community/oseo/oseo-core/pom.xml @@ -39,7 +39,6 @@ org.geotools gt-jdbc - ${gt.version} org.apache.commons @@ -62,7 +61,6 @@ org.geotools.jdbc gt-jdbc-h2 - ${gt.version} test diff --git a/src/community/oseo/oseo-rest/pom.xml b/src/community/oseo/oseo-rest/pom.xml index d159153569c..edd2345221c 100644 --- a/src/community/oseo/oseo-rest/pom.xml +++ b/src/community/oseo/oseo-rest/pom.xml @@ -33,12 +33,10 @@ org.geotools gt-geojson-core - ${gt.version} org.geotools gt-brewer - ${gt.version} com.fasterxml.jackson.core @@ -80,7 +78,6 @@ org.geotools.jdbc gt-jdbc-h2 - ${gt.version} test diff --git a/src/community/oseo/oseo-service/pom.xml b/src/community/oseo/oseo-service/pom.xml index 73cb3964289..702eb1f4c3b 100644 --- a/src/community/oseo/oseo-service/pom.xml +++ b/src/community/oseo/oseo-service/pom.xml @@ -81,7 +81,6 @@ org.geotools.jdbc gt-jdbc-h2 - ${gt.version} test diff --git a/src/community/oseo/web-oseo/pom.xml b/src/community/oseo/web-oseo/pom.xml index a8a8b95d7a1..6d8c3b5641e 100644 --- a/src/community/oseo/web-oseo/pom.xml +++ b/src/community/oseo/web-oseo/pom.xml @@ -61,7 +61,6 @@ org.geotools.jdbc gt-jdbc-h2 - ${gt.version} test diff --git a/src/community/s3-geotiff/pom.xml b/src/community/s3-geotiff/pom.xml index a1fb8ec7acd..8e98fd0f8d2 100644 --- a/src/community/s3-geotiff/pom.xml +++ b/src/community/s3-geotiff/pom.xml @@ -15,7 +15,6 @@ org.geotools gt-s3-geotiff - ${gt.version} org.geoserver.web diff --git a/src/community/schemaless-features/mongodb-schemaless/pom.xml b/src/community/schemaless-features/mongodb-schemaless/pom.xml index 9a753766a59..452fcf6e1a6 100644 --- a/src/community/schemaless-features/mongodb-schemaless/pom.xml +++ b/src/community/schemaless-features/mongodb-schemaless/pom.xml @@ -24,7 +24,6 @@ org.geotools gt-mongodb - ${gt.version} @@ -49,13 +48,11 @@ org.geotools gt-sample-data - ${gt.version} test org.geotools gt-referencing - ${gt.version} org.geoserver diff --git a/src/community/schemaless-features/schemaless-core/pom.xml b/src/community/schemaless-features/schemaless-core/pom.xml index 4926662fb1f..264b3a62815 100644 --- a/src/community/schemaless-features/schemaless-core/pom.xml +++ b/src/community/schemaless-features/schemaless-core/pom.xml @@ -18,7 +18,6 @@ org.geotools gt-main - ${gt.version} org.geoserver @@ -36,7 +35,6 @@ org.geotools gt-sample-data - ${gt.version} test diff --git a/src/community/smart-data-loader/pom.xml b/src/community/smart-data-loader/pom.xml index 4ae3b53e304..5d53ae96d21 100644 --- a/src/community/smart-data-loader/pom.xml +++ b/src/community/smart-data-loader/pom.xml @@ -35,7 +35,6 @@ org.geotools gt-app-schema - ${gt.version} org.geoserver.extension @@ -58,27 +57,23 @@ org.geotools gt-app-schema - ${gt.version} tests test org.geotools gt-sample-data - ${gt.version} test org.geotools gt-jdbc - ${gt.version} test-jar test org.geotools.jdbc gt-jdbc-postgis - ${gt.version} org.geoserver.web diff --git a/src/community/solr/pom.xml b/src/community/solr/pom.xml index 16416aacb3a..f8625507427 100644 --- a/src/community/solr/pom.xml +++ b/src/community/solr/pom.xml @@ -29,7 +29,6 @@ org.geotools gt-solr - ${gt.version} org.geoserver.web diff --git a/src/community/stac-datastore/pom.xml b/src/community/stac-datastore/pom.xml index f2e5ca87b5a..d1c461d4202 100644 --- a/src/community/stac-datastore/pom.xml +++ b/src/community/stac-datastore/pom.xml @@ -24,7 +24,6 @@ org.geotools gt-stac-store - ${gt.version} diff --git a/src/community/taskmanager/core/pom.xml b/src/community/taskmanager/core/pom.xml index 95aa7faad05..f3c6818455a 100644 --- a/src/community/taskmanager/core/pom.xml +++ b/src/community/taskmanager/core/pom.xml @@ -126,7 +126,6 @@ org.geotools gt-app-schema - ${gt.version} test diff --git a/src/community/taskmanager/s3/pom.xml b/src/community/taskmanager/s3/pom.xml index 934bca9b528..f779ccefdf3 100644 --- a/src/community/taskmanager/s3/pom.xml +++ b/src/community/taskmanager/s3/pom.xml @@ -46,7 +46,6 @@ org.geotools gt-s3-geotiff - ${gt.version} test diff --git a/src/community/vector-mosaic/pom.xml b/src/community/vector-mosaic/pom.xml index 8497b0e381b..d76293a600e 100644 --- a/src/community/vector-mosaic/pom.xml +++ b/src/community/vector-mosaic/pom.xml @@ -22,7 +22,6 @@ org.geotools gt-vector-mosaic - ${gt.version} org.geoserver.web diff --git a/src/community/vsi/pom.xml b/src/community/vsi/pom.xml index 4ddee0a4b4e..7a75a84d995 100644 --- a/src/community/vsi/pom.xml +++ b/src/community/vsi/pom.xml @@ -15,7 +15,6 @@ org.geotools gt-vsi - ${gt.version} org.geoserver.web diff --git a/src/community/web-ogr/pom.xml b/src/community/web-ogr/pom.xml index 1b8fbe08250..9dac50be8fd 100644 --- a/src/community/web-ogr/pom.xml +++ b/src/community/web-ogr/pom.xml @@ -20,7 +20,6 @@ org.geotools gt-ogr-jni - ${gt.version} org.geoserver diff --git a/src/community/wps-sextante/pom.xml b/src/community/wps-sextante/pom.xml index d90bd5adc92..7e8ced3b71c 100644 --- a/src/community/wps-sextante/pom.xml +++ b/src/community/wps-sextante/pom.xml @@ -38,7 +38,6 @@ org.geotools gt-process - ${gt.version} org.geoserver diff --git a/src/extension/app-schema/app-schema-core/pom.xml b/src/extension/app-schema/app-schema-core/pom.xml index 16eb5fbc261..a16b285ab44 100644 --- a/src/extension/app-schema/app-schema-core/pom.xml +++ b/src/extension/app-schema/app-schema-core/pom.xml @@ -63,12 +63,10 @@ org.geotools gt-app-schema - ${gt.version} org.geotools gt-app-schema - ${gt.version} tests test @@ -93,26 +91,22 @@ org.geotools gt-sample-data - ${gt.version} test org.geotools gt-jdbc - ${gt.version} test-jar test org.geotools.jdbc gt-jdbc-postgis - ${gt.version} test org.geotools.jdbc gt-jdbc-oracle - ${gt.version} test diff --git a/src/extension/app-schema/app-schema-geopkg-test/pom.xml b/src/extension/app-schema/app-schema-geopkg-test/pom.xml index a0fc3b10c37..ed9c7be8df2 100644 --- a/src/extension/app-schema/app-schema-geopkg-test/pom.xml +++ b/src/extension/app-schema/app-schema-geopkg-test/pom.xml @@ -74,12 +74,10 @@ org.geotools gt-app-schema - ${gt.version} org.geotools gt-app-schema - ${gt.version} tests test @@ -109,26 +107,22 @@ org.geotools gt-sample-data - ${gt.version} test org.geotools gt-jdbc - ${gt.version} test-jar test org.geotools.jdbc gt-jdbc-postgis - ${gt.version} test org.geotools.jdbc gt-jdbc-oracle - ${gt.version} test diff --git a/src/extension/app-schema/app-schema-mongo-test/pom.xml b/src/extension/app-schema/app-schema-mongo-test/pom.xml index b071f8a7642..429f1d73be4 100644 --- a/src/extension/app-schema/app-schema-mongo-test/pom.xml +++ b/src/extension/app-schema/app-schema-mongo-test/pom.xml @@ -22,7 +22,6 @@ org.geotools gt-mongodb - ${gt.version} org.geoserver @@ -50,12 +49,10 @@ org.geotools gt-app-schema - ${gt.version} org.geotools gt-app-schema - ${gt.version} tests test diff --git a/src/extension/app-schema/app-schema-oracle-test/pom.xml b/src/extension/app-schema/app-schema-oracle-test/pom.xml index 3b2395a0f2c..034cda0ad94 100644 --- a/src/extension/app-schema/app-schema-oracle-test/pom.xml +++ b/src/extension/app-schema/app-schema-oracle-test/pom.xml @@ -74,12 +74,10 @@ org.geotools gt-app-schema - ${gt.version} org.geotools gt-app-schema - ${gt.version} tests test @@ -109,26 +107,22 @@ org.geotools gt-sample-data - ${gt.version} test org.geotools gt-jdbc - ${gt.version} test-jar test org.geotools.jdbc gt-jdbc-oracle - ${gt.version} test org.geotools.jdbc gt-jdbc-postgis - ${gt.version} test diff --git a/src/extension/app-schema/app-schema-postgis-test/pom.xml b/src/extension/app-schema/app-schema-postgis-test/pom.xml index e47e6106dff..b9693126de3 100644 --- a/src/extension/app-schema/app-schema-postgis-test/pom.xml +++ b/src/extension/app-schema/app-schema-postgis-test/pom.xml @@ -74,12 +74,10 @@ org.geotools gt-app-schema - ${gt.version} org.geotools gt-app-schema - ${gt.version} tests test @@ -109,26 +107,22 @@ org.geotools gt-sample-data - ${gt.version} test org.geotools gt-jdbc - ${gt.version} test-jar test org.geotools.jdbc gt-jdbc-postgis - ${gt.version} test org.geotools.jdbc gt-jdbc-oracle - ${gt.version} test diff --git a/src/extension/app-schema/app-schema-solr-test/pom.xml b/src/extension/app-schema/app-schema-solr-test/pom.xml index 65ec05f26a1..b489f4f31bc 100644 --- a/src/extension/app-schema/app-schema-solr-test/pom.xml +++ b/src/extension/app-schema/app-schema-solr-test/pom.xml @@ -16,12 +16,10 @@ org.geotools gt-solr - ${gt.version} org.geotools gt-solr - ${gt.version} tests test @@ -46,12 +44,10 @@ org.geotools gt-app-schema - ${gt.version} org.geotools gt-app-schema - ${gt.version} tests test diff --git a/src/extension/app-schema/app-schema-test/pom.xml b/src/extension/app-schema/app-schema-test/pom.xml index a4ce83ee3ae..be43bfe8821 100644 --- a/src/extension/app-schema/app-schema-test/pom.xml +++ b/src/extension/app-schema/app-schema-test/pom.xml @@ -74,12 +74,10 @@ org.geotools gt-app-schema - ${gt.version} org.geotools gt-app-schema - ${gt.version} tests test @@ -109,26 +107,22 @@ org.geotools gt-sample-data - ${gt.version} test org.geotools gt-jdbc - ${gt.version} test-jar test org.geotools.jdbc gt-jdbc-postgis - ${gt.version} test org.geotools.jdbc gt-jdbc-oracle - ${gt.version} test diff --git a/src/extension/app-schema/sample-data-access-test/pom.xml b/src/extension/app-schema/sample-data-access-test/pom.xml index 1ddce2f11ac..26dbe40f5d8 100644 --- a/src/extension/app-schema/sample-data-access-test/pom.xml +++ b/src/extension/app-schema/sample-data-access-test/pom.xml @@ -39,7 +39,6 @@ org.geotools gt-sample-data-access - ${gt.version} diff --git a/src/extension/css/pom.xml b/src/extension/css/pom.xml index 619a2810130..8d8dc5192ba 100644 --- a/src/extension/css/pom.xml +++ b/src/extension/css/pom.xml @@ -28,7 +28,6 @@ org.geotools gt-css - ${gt.version} diff --git a/src/extension/csw/api/pom.xml b/src/extension/csw/api/pom.xml index ee07c7e655f..f6a48bab42b 100644 --- a/src/extension/csw/api/pom.xml +++ b/src/extension/csw/api/pom.xml @@ -29,23 +29,19 @@ org.geotools.ogc net.opengis.fes - ${gt.version} org.geotools.ogc net.opengis.csw - ${gt.version} org.geotools.xsd gt-xsd-csw - ${gt.version} org.geotools gt-complex - ${gt.version} org.geotools.schemas diff --git a/src/extension/feature-pregeneralized/pom.xml b/src/extension/feature-pregeneralized/pom.xml index c1880bed152..88605242bf3 100644 --- a/src/extension/feature-pregeneralized/pom.xml +++ b/src/extension/feature-pregeneralized/pom.xml @@ -31,7 +31,6 @@ org.geotools gt-feature-pregeneralized - ${gt.version} diff --git a/src/extension/geofence/geofence-server/pom.xml b/src/extension/geofence/geofence-server/pom.xml index d8cae3bb529..ceda6fc3ee9 100644 --- a/src/extension/geofence/geofence-server/pom.xml +++ b/src/extension/geofence/geofence-server/pom.xml @@ -147,7 +147,6 @@ org.geotools.xsd gt-xsd-gml3 - ${gt.version} tests test diff --git a/src/extension/geopkg-output/pom.xml b/src/extension/geopkg-output/pom.xml index 0468ef3e15f..7be7adcf220 100644 --- a/src/extension/geopkg-output/pom.xml +++ b/src/extension/geopkg-output/pom.xml @@ -42,7 +42,6 @@ org.geotools gt-geopkg - ${gt.version} org.geoserver diff --git a/src/extension/grib/pom.xml b/src/extension/grib/pom.xml index e264f122a12..33417971669 100644 --- a/src/extension/grib/pom.xml +++ b/src/extension/grib/pom.xml @@ -19,7 +19,6 @@ org.geotools gt-grib - ${gt.version} slf4j-api @@ -35,7 +34,6 @@ org.geotools gt-sample-data - ${gt.version} test diff --git a/src/extension/importer/core/pom.xml b/src/extension/importer/core/pom.xml index 7873303e963..898c6da333b 100644 --- a/src/extension/importer/core/pom.xml +++ b/src/extension/importer/core/pom.xml @@ -34,7 +34,6 @@ org.geotools.xsd gt-xsd-kml - ${gt.version} org.geotools @@ -52,13 +51,11 @@ org.geotools gt-epsg-wkt - ${gt.version} test org.geotools gt-iau-wkt - ${gt.version} test diff --git a/src/extension/importer/pom.xml b/src/extension/importer/pom.xml index fb4701c723f..c7bd2170f78 100644 --- a/src/extension/importer/pom.xml +++ b/src/extension/importer/pom.xml @@ -33,7 +33,6 @@ org.geotools gt-geojson-core - ${gt.version} org.geoserver diff --git a/src/extension/libjpeg-turbo/pom.xml b/src/extension/libjpeg-turbo/pom.xml index 76873f77e52..35a77bd3b36 100644 --- a/src/extension/libjpeg-turbo/pom.xml +++ b/src/extension/libjpeg-turbo/pom.xml @@ -18,7 +18,6 @@ org.geotools gt-sample-data - ${gt.version} test diff --git a/src/extension/mapml/pom.xml b/src/extension/mapml/pom.xml index 074d9d6cfb8..ef0f9480a66 100644 --- a/src/extension/mapml/pom.xml +++ b/src/extension/mapml/pom.xml @@ -93,7 +93,6 @@ org.geotools gt-iau-wkt - ${gt.version} test diff --git a/src/extension/mbstyle/pom.xml b/src/extension/mbstyle/pom.xml index 12fe46d1ba7..038c81b9817 100644 --- a/src/extension/mbstyle/pom.xml +++ b/src/extension/mbstyle/pom.xml @@ -22,7 +22,6 @@ org.geotools gt-mbstyle - ${gt.version} diff --git a/src/extension/netcdf-out/pom.xml b/src/extension/netcdf-out/pom.xml index 09131698d63..b723979cedc 100644 --- a/src/extension/netcdf-out/pom.xml +++ b/src/extension/netcdf-out/pom.xml @@ -120,7 +120,6 @@ org.geotools gt-netcdf - ${gt.version} slf4j-api diff --git a/src/extension/netcdf/pom.xml b/src/extension/netcdf/pom.xml index 5f1da737724..3fbaa6bd235 100644 --- a/src/extension/netcdf/pom.xml +++ b/src/extension/netcdf/pom.xml @@ -38,7 +38,6 @@ org.geotools gt-sample-data - ${gt.version} test diff --git a/src/extension/rat/pom.xml b/src/extension/rat/pom.xml index 0849af7e843..119bc2ebe21 100644 --- a/src/extension/rat/pom.xml +++ b/src/extension/rat/pom.xml @@ -34,7 +34,6 @@ org.geotools gt-brewer - ${gt.version} org.junit.jupiter diff --git a/src/extension/wps-download/pom.xml b/src/extension/wps-download/pom.xml index 52d91ce0018..cdde7a139fe 100644 --- a/src/extension/wps-download/pom.xml +++ b/src/extension/wps-download/pom.xml @@ -18,12 +18,10 @@ org.geotools gt-process - ${gt.version} org.geotools gt-coverage - ${gt.version} org.geoserver.extension diff --git a/src/extension/wps-jdbc/pom.xml b/src/extension/wps-jdbc/pom.xml index 0dc16b1f489..94b52bb864e 100644 --- a/src/extension/wps-jdbc/pom.xml +++ b/src/extension/wps-jdbc/pom.xml @@ -20,7 +20,6 @@ org.geotools gt-transform - ${gt.version} org.geoserver.extension @@ -53,13 +52,11 @@ org.geotools gt-sample-data - ${gt.version} test org.geotools gt-wps - ${gt.version} diff --git a/src/extension/wps/wps-core/pom.xml b/src/extension/wps/wps-core/pom.xml index fd1224beb4e..e705cdc3ee3 100644 --- a/src/extension/wps/wps-core/pom.xml +++ b/src/extension/wps/wps-core/pom.xml @@ -43,63 +43,51 @@ org.geotools.ogc net.opengis.wps - ${gt.version} org.geotools.xsd gt-xsd-wps - ${gt.version} org.geotools.xsd gt-xsd-sld - ${gt.version} org.geotools gt-process - ${gt.version} org.geotools gt-process-raster - ${gt.version} org.geotools gt-process-geometry - ${gt.version} org.geotools gt-process-feature - ${gt.version} org.geotools gt-arcgrid - ${gt.version} org.geotools gt-geotiff - ${gt.version} org.geotools gt-geojson-core - ${gt.version} org.geotools.jdbc gt-jdbc-h2 - ${gt.version} test org.geotools gt-grid - ${gt.version} com.google.guava @@ -145,7 +133,6 @@ org.geotools gt-csv - ${gt.version} com.github.tomakehurst diff --git a/src/extension/wps/wps-kml-ppio/pom.xml b/src/extension/wps/wps-kml-ppio/pom.xml index 1c310009bc6..4509ad9eff4 100644 --- a/src/extension/wps/wps-kml-ppio/pom.xml +++ b/src/extension/wps/wps-kml-ppio/pom.xml @@ -39,7 +39,6 @@ org.geotools.xsd gt-xsd-kml - ${gt.version} diff --git a/src/extension/ysld/pom.xml b/src/extension/ysld/pom.xml index cb0e17d0713..e37bfbe9e5c 100644 --- a/src/extension/ysld/pom.xml +++ b/src/extension/ysld/pom.xml @@ -18,7 +18,6 @@ org.geotools gt-ysld - ${gt.version} org.geoserver @@ -38,7 +37,6 @@ org.geotools gt-ysld - ${gt.version} tests test diff --git a/src/gwc/pom.xml b/src/gwc/pom.xml index fa7af572378..ed9a2050533 100644 --- a/src/gwc/pom.xml +++ b/src/gwc/pom.xml @@ -134,7 +134,6 @@ org.geotools gt-iau-wkt - ${gt.version} test diff --git a/src/main/pom.xml b/src/main/pom.xml index e5588e981f8..09b16f04fcd 100644 --- a/src/main/pom.xml +++ b/src/main/pom.xml @@ -234,7 +234,6 @@ org.geotools.jdbc gt-jdbc-h2 - ${gt.version} test @@ -244,7 +243,6 @@ org.geotools gt-imagepyramid - ${gt.version} test diff --git a/src/pom.xml b/src/pom.xml index 8485f830a2b..a4d19f2b37d 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -322,6 +322,12 @@ gt-main ${gt.version} + + org.geotools + gt-main + ${gt.version} + tests + org.geotools gt-render @@ -331,6 +337,16 @@ org.geotools gt-svg ${gt.version} + + + fop + org.apache.xmlgraphics + + + xml-apis + xml-apis + + org.geotools @@ -393,6 +409,12 @@ gt-xsd-gml3 ${gt.version} + + org.geotools.xsd + gt-xsd-gml3 + ${gt.version} + tests + org.geotools.xsd gt-xsd-filter @@ -428,6 +450,18 @@ gt-jdbc ${gt.version} + + org.geotools + gt-jdbc + ${gt.version} + test-jar + + + org.geotools + gt-jdbc + ${gt.version} + tests + org.geotools gt-postgis @@ -583,6 +617,181 @@ gt-brewer ${gt.version} + + org.geotools + gt-app-schema + ${gt.version} + + + xml-apis + xml-apis + + + + + org.geotools + gt-app-schema + ${gt.version} + tests + + + xml-apis + xml-apis + + + + + org.geotools + gt-complex + ${gt.version} + + + org.geotools + gt-cql2-text + ${gt.version} + + + org.geotools + gt-css + ${gt.version} + + + org.geotools + gt-elasticsearch + ${gt.version} + + + org.geotools + gt-feature-pregeneralized + ${gt.version} + + + org.geotools + gt-flatgeobuf + ${gt.version} + + + org.geotools + gt-geojson-core + ${gt.version} + + + org.geotools + gt-graticule + ${gt.version} + + + org.geotools + gt-grib + ${gt.version} + + + org.geotools + gt-grid + ${gt.version} + + + org.geotools + gt-http + ${gt.version} + + + org.geotools + gt-mbstyle + ${gt.version} + + + org.geotools + gt-mbtiles + ${gt.version} + + + org.geotools + gt-ogr-jni + ${gt.version} + + + org.geotools + gt-s3-geotiff + ${gt.version} + + + org.geotools + gt-sample-data-access + ${gt.version} + + + org.geotools + gt-solr + ${gt.version} + + + org.geotools + gt-solr + ${gt.version} + tests + + + org.geotools + gt-vsi + ${gt.version} + + + org.geotools + gt-wps + ${gt.version} + + + org.geotools + gt-ysld + ${gt.version} + + + org.geotools + gt-ysld + ${gt.version} + tests + + + org.geotools.ogc + net.opengis.csw + ${gt.version} + + + org.geotools.ogc + net.opengis.fes + ${gt.version} + + + org.geotools.ogc + net.opengis.wps + ${gt.version} + + + org.geotools.xsd + gt-xsd-csw + ${gt.version} + + + org.geotools.xsd + gt-xsd-kml + ${gt.version} + + + org.geotools.xsd + gt-xsd-wms + ${gt.version} + + + org.geotools.xsd + gt-xsd-wps + ${gt.version} + + + org.geotools + gt-epsg-wkt + ${gt.version} + diff --git a/src/release/pom.xml b/src/release/pom.xml index ad17e3ed9c7..3c0bd88807c 100644 --- a/src/release/pom.xml +++ b/src/release/pom.xml @@ -100,22 +100,18 @@ org.geotools gt-imagepyramid - ${gt.version} org.geotools gt-iau-wkt - ${gt.version} org.geotools gt-imageio-ext-gdal - ${gt.version} org.geotools gt-app-schema - ${gt.version} org.geoserver.extension diff --git a/src/restconfig/pom.xml b/src/restconfig/pom.xml index 712525e197f..454beb45d45 100644 --- a/src/restconfig/pom.xml +++ b/src/restconfig/pom.xml @@ -27,7 +27,6 @@ org.geotools gt-geojson-core - ${gt.version} org.apache.commons diff --git a/src/web/app/pom.xml b/src/web/app/pom.xml index 30ccbbffc78..ad8b4f5c10e 100644 --- a/src/web/app/pom.xml +++ b/src/web/app/pom.xml @@ -388,7 +388,6 @@ org.geotools gt-kml - ${gt.version} @@ -542,7 +541,6 @@ org.geotools gt-imagepyramid - ${gt.version} @@ -722,7 +720,6 @@ org.geotools gt-sfs - ${gt.version} @@ -774,7 +771,6 @@ org.geotools gt-ogr-jni - ${gt.version} @@ -1665,7 +1661,6 @@ org.geotools gt-mbtiles - ${gt.version} @@ -1870,7 +1865,6 @@ org.geotools gt-stac-store - ${gt.version} @@ -1880,7 +1874,6 @@ org.geotools gt-vector-mosaic - ${gt.version} @@ -1890,7 +1883,6 @@ org.geotools gt-iau-wkt - ${gt.version} diff --git a/src/web/core/pom.xml b/src/web/core/pom.xml index b462081350d..fdbe68244b1 100644 --- a/src/web/core/pom.xml +++ b/src/web/core/pom.xml @@ -134,7 +134,6 @@ org.geotools.jdbc gt-jdbc-h2 - ${gt.version} test diff --git a/src/web/wms/pom.xml b/src/web/wms/pom.xml index df73978653f..ccda7e4d7fa 100644 --- a/src/web/wms/pom.xml +++ b/src/web/wms/pom.xml @@ -33,7 +33,6 @@ org.geotools.xsd gt-xsd-sld - ${gt.version} javax.servlet diff --git a/src/wfs/pom.xml b/src/wfs/pom.xml index 879f9372003..7ab854c90fb 100644 --- a/src/wfs/pom.xml +++ b/src/wfs/pom.xml @@ -95,7 +95,6 @@ org.geotools gt-main - ${gt.version} tests test diff --git a/src/wms/pom.xml b/src/wms/pom.xml index 719ef115a50..b2ee0d168f0 100644 --- a/src/wms/pom.xml +++ b/src/wms/pom.xml @@ -34,16 +34,6 @@ org.geotools gt-svg - - - fop - org.apache.xmlgraphics - - - xml-apis - xml-apis - - org.geotools @@ -60,7 +50,6 @@ org.geotools gt-geojson-core - ${gt.version} @@ -121,7 +110,6 @@ org.geotools.xsd gt-xsd-wms - ${gt.version} test @@ -141,13 +129,11 @@ org.geotools.jdbc gt-jdbc-h2 - ${gt.version} test org.geotools gt-iau-wkt - ${gt.version} test From ef583aa7f3f453ff600aeb605c38a40cbac2c4ce Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Mon, 4 Nov 2024 19:30:36 +0100 Subject: [PATCH 21/43] Have OGC API features build fully and pass QA --- .../clickhouse/ClickHouseDGGSDataStore.java | 2 +- .../ClickhouseAggregatorCollection.java | 3 +- .../java/org/geotools/dggs/ZoneWrapper.java | 2 +- .../org/geotools/dggs/h3/H3DGGSInstance.java | 3 ++ .../dggs/rhealpix/RHealPixDGGSInstance.java | 5 ++- .../ogcapi/v1/dggs/CollectionsDocument.java | 4 +- .../ogcapi/v1/dggs/DGGSDAPAExtension.java | 16 ++------ .../v1/dggs/GroupMatrixFeatureCollection.java | 2 +- .../ogcapi/v1/changeset/ChangeSet.java | 2 +- .../org/geoserver/ogcapi/APIBBoxParser.java | 2 +- .../org/geoserver/ogcapi/APIDispatcher.java | 2 +- .../geoserver/ogcapi/QueryablesBuilder.java | 2 +- .../org/geoserver/ogcapi/HelloController.java | 6 +-- .../v1/coverages/CollectionsDocument.java | 2 +- .../v1/features/CollectionsDocument.java | 2 +- .../v1/features/FeaturesAPIBuilder.java | 2 +- .../features/RFCGeoJSONFeaturesResponse.java | 4 +- .../v1/images/ImagesCollectionsDocument.java | 2 +- .../ogcapi/v1/images/ImagesService.java | 2 +- .../ogcapi/v1/maps/CollectionsDocument.java | 2 +- .../geoserver/ogcapi/v1/maps/MapsTest.java | 38 ++++++------------- .../v1/styles/StyleAttributeExtractor.java | 5 +-- .../ogcapi/v1/styles/StyleLayer.java | 2 +- .../ogcapi/v1/styles/StylesDocument.java | 2 +- .../ogcapi/v1/styles/StylesService.java | 2 +- .../ogcapi/v1/styles/ThumbnailBuilder.java | 2 +- .../v1/tiles/TiledCollectionsDocument.java | 2 +- .../ogcapi/v1/tiles/TilesService.java | 2 - src/community/ogcapi/web-coverages/pom.xml | 6 +++ src/community/ogcapi/web-features/pom.xml | 6 +++ src/community/ogcapi/web-images/pom.xml | 6 +++ src/community/ogcapi/web-maps/pom.xml | 6 +++ src/community/ogcapi/web-ogcapi/pom.xml | 5 +++ .../OgcApiServiceDescriptionProvider.java | 4 +- .../provider/TestCaseInfoXStreamLoader.java | 2 +- src/community/ogcapi/web-styles/pom.xml | 5 +++ src/community/ogcapi/web-tiles/pom.xml | 5 +++ 37 files changed, 91 insertions(+), 76 deletions(-) diff --git a/src/community/ogcapi/dggs/dggs-clickhouse/src/main/java/org/geotools/dggs/clickhouse/ClickHouseDGGSDataStore.java b/src/community/ogcapi/dggs/dggs-clickhouse/src/main/java/org/geotools/dggs/clickhouse/ClickHouseDGGSDataStore.java index eed2458c18c..0092273f22c 100644 --- a/src/community/ogcapi/dggs/dggs-clickhouse/src/main/java/org/geotools/dggs/clickhouse/ClickHouseDGGSDataStore.java +++ b/src/community/ogcapi/dggs/dggs-clickhouse/src/main/java/org/geotools/dggs/clickhouse/ClickHouseDGGSDataStore.java @@ -196,7 +196,7 @@ public FeatureReader getFeatureReader( DGGSFeatureSource source = getFeatureSource(query.getTypeName()); @SuppressWarnings("PMD.CloseResource") // wrapped and returned SimpleFeatureIterator features = source.getFeatures(query).features(); - return new FeatureReader() { + return new FeatureReader<>() { @Override public SimpleFeatureType getFeatureType() { return source.getSchema(); diff --git a/src/community/ogcapi/dggs/dggs-clickhouse/src/main/java/org/geotools/dggs/clickhouse/ClickhouseAggregatorCollection.java b/src/community/ogcapi/dggs/dggs-clickhouse/src/main/java/org/geotools/dggs/clickhouse/ClickhouseAggregatorCollection.java index 374202dd4f8..121ab93ac80 100644 --- a/src/community/ogcapi/dggs/dggs-clickhouse/src/main/java/org/geotools/dggs/clickhouse/ClickhouseAggregatorCollection.java +++ b/src/community/ogcapi/dggs/dggs-clickhouse/src/main/java/org/geotools/dggs/clickhouse/ClickhouseAggregatorCollection.java @@ -58,9 +58,8 @@ public void accepts(FeatureVisitor visitor, ProgressListener progress) throws IO if (visit((MatrixAggregate) visitor)) return; } else if (visitor instanceof GroupedMatrixAggregate) { if (visit((GroupedMatrixAggregate) visitor)) return; - } else { - delegate.accepts(visitor, progress); } + delegate.accepts(visitor, progress); } private boolean visit(MatrixAggregate visitor) throws IOException { diff --git a/src/community/ogcapi/dggs/dggs-core/src/main/java/org/geotools/dggs/ZoneWrapper.java b/src/community/ogcapi/dggs/dggs-core/src/main/java/org/geotools/dggs/ZoneWrapper.java index 0ff89cd46d5..cfe1c62ecd2 100644 --- a/src/community/ogcapi/dggs/dggs-core/src/main/java/org/geotools/dggs/ZoneWrapper.java +++ b/src/community/ogcapi/dggs/dggs-core/src/main/java/org/geotools/dggs/ZoneWrapper.java @@ -41,7 +41,7 @@ public enum DatelineLocation { West, /** Crossing the dateline, majority of points in the east empisphere */ East - }; + } /** * Wraps a dateline crossing polygon so that its longitudes are all packed on one side. Will not diff --git a/src/community/ogcapi/dggs/dggs-core/src/main/java/org/geotools/dggs/h3/H3DGGSInstance.java b/src/community/ogcapi/dggs/dggs-core/src/main/java/org/geotools/dggs/h3/H3DGGSInstance.java index da5a4ff842b..e55c6e0d63b 100644 --- a/src/community/ogcapi/dggs/dggs-core/src/main/java/org/geotools/dggs/h3/H3DGGSInstance.java +++ b/src/community/ogcapi/dggs/dggs-core/src/main/java/org/geotools/dggs/h3/H3DGGSInstance.java @@ -101,6 +101,7 @@ public H3Zone getZone(double lat, double lon, int resolution) { } @Override + @SuppressWarnings("PMD.UnnecessaryCast") public Iterator zonesFromEnvelope( Envelope envelope, int targetResolution, boolean compact) { Envelope intersection = envelope.intersection(WORLD); @@ -287,6 +288,7 @@ public List getExtraProperties() { } @Override + @SuppressWarnings("PMD.UnnecessaryCast") public Iterator neighbors(String id, int radius) { // Using H3 facilities. Upside fast and accurate (considering dateline and pole neighbors // too), downside, will quickly go OOM, radius should be limited @@ -324,6 +326,7 @@ public Zone point(Point point, int resolution) { } @Override + @SuppressWarnings("PMD.UnnecessaryCast") public Iterator polygon(Polygon polygon, int resolution, boolean compact) { List shell = getGeoCoords(polygon.getExteriorRing()); List> holes = diff --git a/src/community/ogcapi/dggs/dggs-core/src/main/java/org/geotools/dggs/rhealpix/RHealPixDGGSInstance.java b/src/community/ogcapi/dggs/dggs-core/src/main/java/org/geotools/dggs/rhealpix/RHealPixDGGSInstance.java index 48993c6d801..1c9dd75224d 100644 --- a/src/community/ogcapi/dggs/dggs-core/src/main/java/org/geotools/dggs/rhealpix/RHealPixDGGSInstance.java +++ b/src/community/ogcapi/dggs/dggs-core/src/main/java/org/geotools/dggs/rhealpix/RHealPixDGGSInstance.java @@ -140,6 +140,7 @@ private String toZoneId(List idList) { } @Override + @SuppressWarnings("PMD.UnnecessaryCast") public Iterator zonesFromEnvelope( Envelope envelope, int targetResolution, boolean compact) { // WAY USING DIRECT LIBRARY CALLS. Faster, but memory bound. @@ -374,6 +375,7 @@ public List getExtraProperties() { } @Override + @SuppressWarnings("PMD.UnnecessaryCast") public Iterator neighbors(String id, int radius) { Set result = new HashSet<>(); // temporary add to work as an exclusion mask too @@ -417,6 +419,7 @@ public Iterator neighbors(String id, int radius) { } @Override + @SuppressWarnings("PMD.UnnecessaryCast") public Iterator children(String zoneId, int resolution) { Zone parent = getZone(zoneId); if (parent.getResolution() >= resolution) return new EmptyIterator<>(); @@ -480,7 +483,7 @@ public Iterator polygon(Polygon polygon, int resolution, boolean compact) && testContains(prepared, zone.getCenter())) || testContains(prepared, zone.getBoundary()); }, - zone -> (Zone) zone); + zone -> zone); // if compact iteration, we are done if (compact) return compactIterator; // otherwise expand the cells that are at a lower resolution using the fast children diff --git a/src/community/ogcapi/dggs/ogcapi-dggs/src/main/java/org/geoserver/ogcapi/v1/dggs/CollectionsDocument.java b/src/community/ogcapi/dggs/ogcapi-dggs/src/main/java/org/geoserver/ogcapi/v1/dggs/CollectionsDocument.java index b0c086d9acc..3415b02dc1d 100644 --- a/src/community/ogcapi/dggs/ogcapi-dggs/src/main/java/org/geoserver/ogcapi/v1/dggs/CollectionsDocument.java +++ b/src/community/ogcapi/dggs/ogcapi-dggs/src/main/java/org/geoserver/ogcapi/v1/dggs/CollectionsDocument.java @@ -40,11 +40,11 @@ public List getLinks() { } @JacksonXmlProperty(localName = "Collection") - @SuppressWarnings({"PMD.CloseResource", "PMD.EmptyWhileStmt"}) + @SuppressWarnings({"PMD.CloseResource", "PMD.EmptyControlStatement"}) public Iterator getCollections() { CloseableIterator featureTypes = geoServer.getCatalog().list(FeatureTypeInfo.class, Filter.INCLUDE); - return new Iterator() { + return new Iterator<>() { CollectionDocument next; diff --git a/src/community/ogcapi/dggs/ogcapi-dggs/src/main/java/org/geoserver/ogcapi/v1/dggs/DGGSDAPAExtension.java b/src/community/ogcapi/dggs/ogcapi-dggs/src/main/java/org/geoserver/ogcapi/v1/dggs/DGGSDAPAExtension.java index cdf13f9067e..331f1f82ee5 100644 --- a/src/community/ogcapi/dggs/ogcapi-dggs/src/main/java/org/geoserver/ogcapi/v1/dggs/DGGSDAPAExtension.java +++ b/src/community/ogcapi/dggs/ogcapi-dggs/src/main/java/org/geoserver/ogcapi/v1/dggs/DGGSDAPAExtension.java @@ -196,9 +196,7 @@ public SimpleFeatureCollection areaSpaceAggregation( q.getHints().put(VIRTUAL_TABLE_PARAMETERS, singletonMap(VP_RESOLUTION, resolution)); SimpleFeatureSource fs = (SimpleFeatureSource) ft.getFeatureSource(null, null); List expressions = - Arrays.stream(variables) - .map(v -> (Expression) FF.property(v)) - .collect(Collectors.toList()); + Arrays.stream(variables).map(v -> FF.property(v)).collect(Collectors.toList()); // run a full aggregate and build the feature List timeGroupExpressions = getTimeGroup(ft); GroupedMatrixAggregate aggregate = @@ -283,9 +281,7 @@ public SimpleFeatureCollection areaTimeAggregation( q.getHints().put(VIRTUAL_TABLE_PARAMETERS, singletonMap(VP_RESOLUTION, resolution)); SimpleFeatureSource fs = (SimpleFeatureSource) ft.getFeatureSource(null, null); List expressions = - Arrays.stream(variables) - .map(v -> (Expression) FF.property(v)) - .collect(Collectors.toList()); + Arrays.stream(variables).map(v -> FF.property(v)).collect(Collectors.toList()); // run a full aggregate and build the feature GroupedMatrixAggregate aggregate = new GroupedMatrixAggregate( @@ -360,9 +356,7 @@ public SimpleFeatureCollection areaSpaceTimeAggregation( q.getHints().put(VIRTUAL_TABLE_PARAMETERS, singletonMap(VP_RESOLUTION, resolution)); SimpleFeatureSource fs = (SimpleFeatureSource) ft.getFeatureSource(null, null); List expressions = - Arrays.stream(variables) - .map(v -> (Expression) FF.property(v)) - .collect(Collectors.toList()); + Arrays.stream(variables).map(v -> FF.property(v)).collect(Collectors.toList()); // run a full aggregate and build the feature MatrixAggregate aggregate = new MatrixAggregate(expressions, Arrays.asList(functions)); fs.getFeatures(q).accepts(aggregate, null); @@ -476,9 +470,7 @@ public SimpleFeatureCollection positionTimeAggregate( q.getHints().put(VIRTUAL_TABLE_PARAMETERS, singletonMap(VP_RESOLUTION, resolution)); SimpleFeatureSource fs = (SimpleFeatureSource) ft.getFeatureSource(null, null); List expressions = - Arrays.stream(variables) - .map(v -> (Expression) FF.property(v)) - .collect(Collectors.toList()); + Arrays.stream(variables).map(v -> FF.property(v)).collect(Collectors.toList()); // run a full aggregate and build the feature GroupedMatrixAggregate aggregate = new GroupedMatrixAggregate( diff --git a/src/community/ogcapi/dggs/ogcapi-dggs/src/main/java/org/geoserver/ogcapi/v1/dggs/GroupMatrixFeatureCollection.java b/src/community/ogcapi/dggs/ogcapi-dggs/src/main/java/org/geoserver/ogcapi/v1/dggs/GroupMatrixFeatureCollection.java index a35b11bd1e2..30ca97dbbd2 100644 --- a/src/community/ogcapi/dggs/ogcapi-dggs/src/main/java/org/geoserver/ogcapi/v1/dggs/GroupMatrixFeatureCollection.java +++ b/src/community/ogcapi/dggs/ogcapi-dggs/src/main/java/org/geoserver/ogcapi/v1/dggs/GroupMatrixFeatureCollection.java @@ -116,7 +116,7 @@ public boolean containsAll(Collection o) { @Override public boolean isEmpty() { try (CloseableIterator it = result.getIterator()) { - return features().hasNext(); + return it.hasNext(); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/community/ogcapi/ogcapi-changeset/src/main/java/org/geoserver/ogcapi/v1/changeset/ChangeSet.java b/src/community/ogcapi/ogcapi-changeset/src/main/java/org/geoserver/ogcapi/v1/changeset/ChangeSet.java index 57a14b8abc3..9b4b2e6980f 100644 --- a/src/community/ogcapi/ogcapi-changeset/src/main/java/org/geoserver/ogcapi/v1/changeset/ChangeSet.java +++ b/src/community/ogcapi/ogcapi-changeset/src/main/java/org/geoserver/ogcapi/v1/changeset/ChangeSet.java @@ -20,7 +20,7 @@ enum Priority { high, medium, low - }; + } public static class ChangedItem { Priority priority; diff --git a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/APIBBoxParser.java b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/APIBBoxParser.java index a835b85fcda..581b582224d 100644 --- a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/APIBBoxParser.java +++ b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/APIBBoxParser.java @@ -105,7 +105,7 @@ private static Filter toFilter(ReferencedEnvelope[] bboxes) { return FF.bbox(FF.property(""), bboxes[0]); } else if (bboxes instanceof ReferencedEnvelope[]) { List filters = - Stream.of((ReferencedEnvelope[]) bboxes) + Stream.of(bboxes) .map(e -> FF.bbox(FF.property(""), e)) .collect(Collectors.toList()); return FF.or(filters); diff --git a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/APIDispatcher.java b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/APIDispatcher.java index 81e2f008891..1a7a6f0d3b2 100644 --- a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/APIDispatcher.java +++ b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/APIDispatcher.java @@ -352,7 +352,7 @@ private void dispatchService(Request dr, HandlerMethod handler) { public static APIService getApiServiceAnnotation(Class clazz) { APIService annotation = null; while (annotation == null && clazz != null) { - annotation = (APIService) clazz.getAnnotation(APIService.class); + annotation = clazz.getAnnotation(APIService.class); if (annotation == null) { clazz = clazz.getSuperclass(); } diff --git a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/QueryablesBuilder.java b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/QueryablesBuilder.java index d6377467f74..c5a0322f1f8 100644 --- a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/QueryablesBuilder.java +++ b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/QueryablesBuilder.java @@ -90,7 +90,7 @@ public static Schema getSchema(Class binding) { } private static Schema getGeometrySchema(Class binding) { - Schema schema = new Schema(); + Schema schema = new Schema<>(); String ref; String description; if (Point.class.isAssignableFrom(binding)) { diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloController.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloController.java index c6fdba23817..d9d6b3cbab0 100644 --- a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloController.java +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloController.java @@ -30,7 +30,7 @@ @RequestMapping(path = APIDispatcher.ROOT_PATH + "/hello/v1") public class HelloController { - static interface HelloServiceInfo extends ServiceInfo {}; + static interface HelloServiceInfo extends ServiceInfo {} String defaultValue = "hello"; @@ -51,13 +51,13 @@ public Message hello(@RequestParam(name = "message", required = false) String me public ResponseEntity echo(@RequestBody Message message) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); - return new ResponseEntity(message.getMessage(), headers, HttpStatus.CREATED); + return new ResponseEntity<>(message.getMessage(), headers, HttpStatus.CREATED); } @DeleteMapping(path = "delete") @ResponseBody public ResponseEntity delete() { - return new ResponseEntity(HttpStatus.NO_CONTENT); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @PutMapping(path = "default") diff --git a/src/community/ogcapi/ogcapi-coverages/src/main/java/org/geoserver/ogcapi/v1/coverages/CollectionsDocument.java b/src/community/ogcapi/ogcapi-coverages/src/main/java/org/geoserver/ogcapi/v1/coverages/CollectionsDocument.java index 781f1a70395..6055cdfc69c 100644 --- a/src/community/ogcapi/ogcapi-coverages/src/main/java/org/geoserver/ogcapi/v1/coverages/CollectionsDocument.java +++ b/src/community/ogcapi/ogcapi-coverages/src/main/java/org/geoserver/ogcapi/v1/coverages/CollectionsDocument.java @@ -51,7 +51,7 @@ public List getLinks() { public Iterator getCollections() { CloseableIterator coverages = geoServer.getCatalog().list(CoverageInfo.class, Filter.INCLUDE); - return new Iterator() { + return new Iterator<>() { CollectionDocument next; diff --git a/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/CollectionsDocument.java b/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/CollectionsDocument.java index cdf458db8fd..7f0d31b4564 100644 --- a/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/CollectionsDocument.java +++ b/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/CollectionsDocument.java @@ -53,7 +53,7 @@ public List getLinks() { public Iterator getCollections() { CloseableIterator featureTypes = geoServer.getCatalog().list(FeatureTypeInfo.class, Filter.INCLUDE); - return new Iterator() { + return new Iterator<>() { CollectionDocument next; diff --git a/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/FeaturesAPIBuilder.java b/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/FeaturesAPIBuilder.java index 91f3d3a49e3..218b1d8f88b 100644 --- a/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/FeaturesAPIBuilder.java +++ b/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/FeaturesAPIBuilder.java @@ -67,7 +67,7 @@ public OpenAPI build(WFSInfo wfs) throws IOException { // list of valid filter-lang values Parameter filterLang = parameters.get("filter-lang"); - filterLang.getSchema().setEnum(new ArrayList(APIFilterParser.SUPPORTED_ENCODINGS)); + filterLang.getSchema().setEnum(new ArrayList<>(APIFilterParser.SUPPORTED_ENCODINGS)); // provide actual values for limit Parameter limit = parameters.get("limit"); diff --git a/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/RFCGeoJSONFeaturesResponse.java b/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/RFCGeoJSONFeaturesResponse.java index df6378c9e20..1ce0cebc02c 100644 --- a/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/RFCGeoJSONFeaturesResponse.java +++ b/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/RFCGeoJSONFeaturesResponse.java @@ -212,10 +212,10 @@ protected void addLinks( protected FeatureTypeInfo getFeatureType(GetFeatureRequest request) { // OGC API Features always have a collection reference, so one query return Optional.ofNullable(request.getQueries()) - .filter(qs -> qs.size() > 0) + .filter(qs -> !qs.isEmpty()) .map(qs -> qs.get(0)) .map(q -> q.getTypeNames()) - .filter(tns -> tns.size() > 0) + .filter(tns -> !tns.isEmpty()) .map(tns -> tns.get(0)) .map(tn -> new NameImpl(tn.getNamespaceURI(), tn.getLocalPart())) .map(tn -> gs.getCatalog().getFeatureTypeByName(tn)) diff --git a/src/community/ogcapi/ogcapi-images/src/main/java/org/geoserver/ogcapi/v1/images/ImagesCollectionsDocument.java b/src/community/ogcapi/ogcapi-images/src/main/java/org/geoserver/ogcapi/v1/images/ImagesCollectionsDocument.java index 2d5c2397ddf..7a719f794f0 100644 --- a/src/community/ogcapi/ogcapi-images/src/main/java/org/geoserver/ogcapi/v1/images/ImagesCollectionsDocument.java +++ b/src/community/ogcapi/ogcapi-images/src/main/java/org/geoserver/ogcapi/v1/images/ImagesCollectionsDocument.java @@ -45,7 +45,7 @@ public Iterator getCollections() { boolean skipInvalid = gs.getGlobal().getResourceErrorHandling() == ResourceErrorHandling.SKIP_MISCONFIGURED_LAYERS; - return new Iterator() { + return new Iterator<>() { ImagesCollectionDocument next; diff --git a/src/community/ogcapi/ogcapi-images/src/main/java/org/geoserver/ogcapi/v1/images/ImagesService.java b/src/community/ogcapi/ogcapi-images/src/main/java/org/geoserver/ogcapi/v1/images/ImagesService.java index 60be0f9a899..dfb17fe5c78 100644 --- a/src/community/ogcapi/ogcapi-images/src/main/java/org/geoserver/ogcapi/v1/images/ImagesService.java +++ b/src/community/ogcapi/ogcapi-images/src/main/java/org/geoserver/ogcapi/v1/images/ImagesService.java @@ -653,7 +653,7 @@ public ResponseEntity deleteImage( imageListeners.imageRemoved(info, feature); - return new ResponseEntity(HttpStatus.OK); + return new ResponseEntity<>(HttpStatus.OK); } @Override diff --git a/src/community/ogcapi/ogcapi-maps/src/main/java/org/geoserver/ogcapi/v1/maps/CollectionsDocument.java b/src/community/ogcapi/ogcapi-maps/src/main/java/org/geoserver/ogcapi/v1/maps/CollectionsDocument.java index 8e5d6b938af..b1d7acfc6dd 100644 --- a/src/community/ogcapi/ogcapi-maps/src/main/java/org/geoserver/ogcapi/v1/maps/CollectionsDocument.java +++ b/src/community/ogcapi/ogcapi-maps/src/main/java/org/geoserver/ogcapi/v1/maps/CollectionsDocument.java @@ -47,7 +47,7 @@ public Iterator getCollections() { @SuppressWarnings("PMD.CloseResource") // wrapped and returned CloseableIterator publisheds = geoServer.getCatalog().list(PublishedInfo.class, Filter.INCLUDE); - return new Iterator() { + return new Iterator<>() { CollectionDocument next; diff --git a/src/community/ogcapi/ogcapi-maps/src/test/java/org/geoserver/ogcapi/v1/maps/MapsTest.java b/src/community/ogcapi/ogcapi-maps/src/test/java/org/geoserver/ogcapi/v1/maps/MapsTest.java index 0079e6d565e..283ff06db98 100644 --- a/src/community/ogcapi/ogcapi-maps/src/test/java/org/geoserver/ogcapi/v1/maps/MapsTest.java +++ b/src/community/ogcapi/ogcapi-maps/src/test/java/org/geoserver/ogcapi/v1/maps/MapsTest.java @@ -5,13 +5,10 @@ package org.geoserver.ogcapi.v1.maps; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; import com.jayway.jsonpath.DocumentContext; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.jsoup.nodes.DataNode; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; @@ -47,8 +44,7 @@ public void testDatetimeHTMLMapsFormat() throws Exception { Document document = getAsJSoup( "ogc/maps/v1/collections/sf:TimeWithStartEnd/styles/Default/map?f=html&datetime=2012-02-12T00:00:00Z"); - boolean found = searchParameter(document, "\"datetime\": '2012-02-12T00:00:00Z'"); - assertTrue(found); + assertEquals("2012-02-12T00:00:00Z", getParameterValue(document, "datetime")); } @Test @@ -57,27 +53,15 @@ public void testHTMLNoDatetime() throws Exception { // failed here when no datetime provided, FTL processing error, null on js_string Document document = getAsJSoup("ogc/maps/v1/collections/sf:TimeWithStartEnd/styles/Default/map?f=html"); - boolean found = searchParameter(document, "\"datetime\": '2012-02-12T00:00:00Z'"); - assertFalse(found); + assertNull(getParameterValue(document, "datetime")); } - private static boolean searchParameter(Document document, String keyValue) { - Elements scriptsOnPage = document.select("script"); - Matcher matcher = null; - // check that the datetime is in the javascript parameters - String keyToFind = "datetime"; - Pattern pattern = Pattern.compile("\"" + keyToFind + "\":\\s*'(.*?)'"); - boolean found = false; - for (Element element : scriptsOnPage) { - for (DataNode node : element.dataNodes()) { - matcher = pattern.matcher(node.getWholeData()); - while (matcher.find()) { - if (matcher.group().equals(keyValue)) { - found = true; - } - } - } - } - return found; + private static String getParameterValue(Document document, String key) { + Elements parameters = document.select("input[type='hidden'][title='" + key + "']"); + if (parameters.isEmpty()) return null; + if (parameters.size() > 1) + fail("Found more than one element with key " + key + ": " + parameters); + Element parameter = parameters.first(); + return parameter.attr("value"); } } diff --git a/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StyleAttributeExtractor.java b/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StyleAttributeExtractor.java index b78fc5c99ae..73093073581 100644 --- a/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StyleAttributeExtractor.java +++ b/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StyleAttributeExtractor.java @@ -317,10 +317,7 @@ public void visit(TextSymbolizer text) { } } - if (text instanceof TextSymbolizer) { - if (((TextSymbolizer) text).getGraphic() != null) - ((TextSymbolizer) text).getGraphic().accept(this); - } + if (text.getGraphic() != null) text.getGraphic().accept(this); if (text.getFill() != null) { text.getFill().accept(this); diff --git a/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StyleLayer.java b/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StyleLayer.java index 51f01d4ac27..59e02e504ed 100644 --- a/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StyleLayer.java +++ b/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StyleLayer.java @@ -59,7 +59,7 @@ enum LayerType { polygon, geometry, raster - }; + } String id; LayerType type; diff --git a/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StylesDocument.java b/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StylesDocument.java index 03ed66f597a..8dc5fa29cd8 100644 --- a/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StylesDocument.java +++ b/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StylesDocument.java @@ -29,7 +29,7 @@ public StylesDocument(Catalog catalog) { public Iterator getStyles() { // full scan (we might add paging/filtering later) CloseableIterator styles = catalog.list(StyleInfo.class, Filter.INCLUDE); - return new Iterator() { + return new Iterator<>() { StyleDocument next; diff --git a/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StylesService.java b/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StylesService.java index eb37e7ff154..d14000181f9 100644 --- a/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StylesService.java +++ b/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/StylesService.java @@ -335,7 +335,7 @@ public ResponseEntity postStyle( // validation if (validate == only || validate == yes) { validate(mimeType, content, handler); - return new ResponseEntity(HttpStatus.NO_CONTENT); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } else { String styleId = getStyleId(mimeType, handler, content); StyleInfo styleInfo = getStyleInfo(styleId, false); diff --git a/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/ThumbnailBuilder.java b/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/ThumbnailBuilder.java index 36130ce1b56..19e3be7e0ae 100644 --- a/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/ThumbnailBuilder.java +++ b/src/community/ogcapi/ogcapi-styles/src/main/java/org/geoserver/ogcapi/v1/styles/ThumbnailBuilder.java @@ -172,6 +172,6 @@ public boolean canGenerateThumbnail(StyleInfo styleInfo) { LOGGER.log(Level.FINER, "Could not setup thumbnail", e); } // if we have at least a layer, we can work it - return request.getLayers().size() > 0; + return !request.getLayers().isEmpty(); } } diff --git a/src/community/ogcapi/ogcapi-tiles/src/main/java/org/geoserver/ogcapi/v1/tiles/TiledCollectionsDocument.java b/src/community/ogcapi/ogcapi-tiles/src/main/java/org/geoserver/ogcapi/v1/tiles/TiledCollectionsDocument.java index c2d30fd47c3..6ff58aae583 100644 --- a/src/community/ogcapi/ogcapi-tiles/src/main/java/org/geoserver/ogcapi/v1/tiles/TiledCollectionsDocument.java +++ b/src/community/ogcapi/ogcapi-tiles/src/main/java/org/geoserver/ogcapi/v1/tiles/TiledCollectionsDocument.java @@ -53,7 +53,7 @@ public Iterator getCollections() { boolean skipInvalid = gs.getGlobal().getResourceErrorHandling() == ResourceErrorHandling.SKIP_MISCONFIGURED_LAYERS; - return new Iterator() { + return new Iterator<>() { TiledCollectionDocument next; diff --git a/src/community/ogcapi/ogcapi-tiles/src/main/java/org/geoserver/ogcapi/v1/tiles/TilesService.java b/src/community/ogcapi/ogcapi-tiles/src/main/java/org/geoserver/ogcapi/v1/tiles/TilesService.java index c9964a23115..4a4bf6c9f58 100644 --- a/src/community/ogcapi/ogcapi-tiles/src/main/java/org/geoserver/ogcapi/v1/tiles/TilesService.java +++ b/src/community/ogcapi/ogcapi-tiles/src/main/java/org/geoserver/ogcapi/v1/tiles/TilesService.java @@ -12,7 +12,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.channels.Channels; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.HashMap; @@ -516,7 +515,6 @@ private String getETag(byte[] tileBytes) throws NoSuchAlgorithmException { if (tileBytes == null) { return "EMPTY_TILE"; } - final byte[] hash = MessageDigest.getInstance("MD5").digest(tileBytes); return GWC.getETag(tileBytes); } diff --git a/src/community/ogcapi/web-coverages/pom.xml b/src/community/ogcapi/web-coverages/pom.xml index bbe1903cd80..a254873fcff 100644 --- a/src/community/ogcapi/web-coverages/pom.xml +++ b/src/community/ogcapi/web-coverages/pom.xml @@ -32,6 +32,12 @@ compile + + org.junit.jupiter + junit-jupiter + test + + diff --git a/src/community/ogcapi/web-features/pom.xml b/src/community/ogcapi/web-features/pom.xml index 3bf7d642d3b..1f978cfbcb2 100644 --- a/src/community/ogcapi/web-features/pom.xml +++ b/src/community/ogcapi/web-features/pom.xml @@ -33,6 +33,12 @@ compile + + org.junit.jupiter + junit-jupiter + test + + diff --git a/src/community/ogcapi/web-images/pom.xml b/src/community/ogcapi/web-images/pom.xml index 4093ff219b0..f57169edc7b 100644 --- a/src/community/ogcapi/web-images/pom.xml +++ b/src/community/ogcapi/web-images/pom.xml @@ -33,6 +33,12 @@ compile + + org.junit.jupiter + junit-jupiter + test + + diff --git a/src/community/ogcapi/web-maps/pom.xml b/src/community/ogcapi/web-maps/pom.xml index 55dc7a38cb6..19e53084eb3 100644 --- a/src/community/ogcapi/web-maps/pom.xml +++ b/src/community/ogcapi/web-maps/pom.xml @@ -33,6 +33,12 @@ compile + + org.junit.jupiter + junit-jupiter + test + + diff --git a/src/community/ogcapi/web-ogcapi/pom.xml b/src/community/ogcapi/web-ogcapi/pom.xml index 640bd3d0539..294bf404e73 100644 --- a/src/community/ogcapi/web-ogcapi/pom.xml +++ b/src/community/ogcapi/web-ogcapi/pom.xml @@ -42,6 +42,11 @@ ${project.version} test + + org.junit.jupiter + junit-jupiter + test + diff --git a/src/community/ogcapi/web-ogcapi/src/main/java/org/geoserver/web/ogcapi/OgcApiServiceDescriptionProvider.java b/src/community/ogcapi/web-ogcapi/src/main/java/org/geoserver/web/ogcapi/OgcApiServiceDescriptionProvider.java index 8da29764048..bdb0d7b1d8a 100644 --- a/src/community/ogcapi/web-ogcapi/src/main/java/org/geoserver/web/ogcapi/OgcApiServiceDescriptionProvider.java +++ b/src/community/ogcapi/web-ogcapi/src/main/java/org/geoserver/web/ogcapi/OgcApiServiceDescriptionProvider.java @@ -88,10 +88,10 @@ public OgcApiServiceDescriptionProvider( protected SERVICEINFOTYPE info(WorkspaceInfo workspaceInfo, PublishedInfo layerInfo) { SERVICEINFOTYPE info = null; if (workspaceInfo != null) { - info = (SERVICEINFOTYPE) geoserver.getService(workspaceInfo, infoClass); + info = geoserver.getService(workspaceInfo, infoClass); } if (info == null) { - info = (SERVICEINFOTYPE) geoserver.getService(infoClass); + info = geoserver.getService(infoClass); } return info; } diff --git a/src/community/ogcapi/web-ogcapi/src/test/java/org/geoserver/web/ogcapi/provider/TestCaseInfoXStreamLoader.java b/src/community/ogcapi/web-ogcapi/src/test/java/org/geoserver/web/ogcapi/provider/TestCaseInfoXStreamLoader.java index 418746811a4..a442afd3177 100644 --- a/src/community/ogcapi/web-ogcapi/src/test/java/org/geoserver/web/ogcapi/provider/TestCaseInfoXStreamLoader.java +++ b/src/community/ogcapi/web-ogcapi/src/test/java/org/geoserver/web/ogcapi/provider/TestCaseInfoXStreamLoader.java @@ -36,7 +36,7 @@ public static void initXStreamPersister(XStreamPersister xp) { protected TestCaseInfo createServiceFromScratch(GeoServer gs) { TestCaseInfoImpl testCaseInfo = new TestCaseInfoImpl(); testCaseInfo.setName("tc"); - return (TestCaseInfo) testCaseInfo; + return testCaseInfo; } @Override diff --git a/src/community/ogcapi/web-styles/pom.xml b/src/community/ogcapi/web-styles/pom.xml index bb1c0047c99..19526ac38e4 100644 --- a/src/community/ogcapi/web-styles/pom.xml +++ b/src/community/ogcapi/web-styles/pom.xml @@ -33,6 +33,11 @@ compile + + org.junit.jupiter + junit-jupiter + test + diff --git a/src/community/ogcapi/web-tiles/pom.xml b/src/community/ogcapi/web-tiles/pom.xml index 80f5ced01e8..7ac76564bdc 100644 --- a/src/community/ogcapi/web-tiles/pom.xml +++ b/src/community/ogcapi/web-tiles/pom.xml @@ -33,6 +33,11 @@ compile + + org.junit.jupiter + junit-jupiter + test + From f26ece815f05817b95c4ec3daa4a532445b4112c Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Tue, 5 Nov 2024 16:04:40 +0100 Subject: [PATCH 22/43] Increasing OGC API core test coverage, in preparation for graduation --- .../geoserver/ogcapi/APIBBoxParserTest.java | 179 ++++++++++++++++ .../geoserver/ogcapi/APIRequestInfoTest.java | 149 +++++++++++++ .../ogcapi/ApiConfigurationSupportTest.java | 2 +- .../ogcapi/DateTimeConverterTest.java | 102 +++++++++ .../org/geoserver/ogcapi/HelloDocument.java | 17 ++ .../ogcapi/HelloResponseMessageConverter.java | 16 ++ ...HelloController.java => HelloService.java} | 36 +++- ...patcherTest.java => HelloServiceTest.java} | 200 ++++++++++++------ .../org/geoserver/ogcapi/MessageResponse.java | 35 +++ .../ogcapi/PaginationLinksBuilderTest.java | 61 ++++++ .../ogcapi/impl/LinkInfoImplTest.java | 100 +++++++++ .../geoserver/ogcapi/applicationContext.xml | 28 --- .../org/geoserver/ogcapi/message.ftl | 9 + 13 files changed, 837 insertions(+), 97 deletions(-) create mode 100644 src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/APIBBoxParserTest.java create mode 100644 src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/APIRequestInfoTest.java create mode 100644 src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/DateTimeConverterTest.java create mode 100644 src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloDocument.java create mode 100644 src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloResponseMessageConverter.java rename src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/{HelloController.java => HelloService.java} (72%) rename src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/{APIDispatcherTest.java => HelloServiceTest.java} (72%) create mode 100644 src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/MessageResponse.java create mode 100644 src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/PaginationLinksBuilderTest.java create mode 100644 src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/impl/LinkInfoImplTest.java delete mode 100644 src/community/ogcapi/ogcapi-core/src/test/resources/org/geoserver/ogcapi/applicationContext.xml create mode 100644 src/community/ogcapi/ogcapi-core/src/test/resources/org/geoserver/ogcapi/message.ftl diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/APIBBoxParserTest.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/APIBBoxParserTest.java new file mode 100644 index 00000000000..a391bb51953 --- /dev/null +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/APIBBoxParserTest.java @@ -0,0 +1,179 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2 license, available at the root + * application directory. + */ +package org.geoserver.ogcapi; + +import static org.geotools.referencing.crs.DefaultGeographicCRS.WGS84_3D; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.geotools.api.filter.Filter; +import org.geotools.api.filter.Or; +import org.geotools.api.filter.expression.PropertyName; +import org.geotools.api.filter.spatial.BBOX; +import org.geotools.api.geometry.BoundingBox; +import org.geotools.api.referencing.FactoryException; +import org.geotools.factory.CommonFactoryFinder; +import org.geotools.geometry.jts.ReferencedEnvelope; +import org.geotools.geometry.jts.ReferencedEnvelope3D; +import org.geotools.referencing.CRS; +import org.geotools.referencing.crs.DefaultGeographicCRS; +import org.junit.Test; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Polygon; + +public class APIBBoxParserTest { + + private static final double EPS = 0001; + public static final String BBOX_3D_SPEC = "10,20,5,30,40,15"; + public static final String BBOX_2D_SPEC = "10,20,30,40"; + private static PropertyName DEFAULT_GEOMETRY = + CommonFactoryFinder.getFilterFactory().property(""); + + @Test + public void testParse2DBBox() throws FactoryException { + ReferencedEnvelope[] result = APIBBoxParser.parse(BBOX_2D_SPEC); + + assertNotNull(result); + assertEquals(1, result.length); + ReferencedEnvelope envelope = result[0]; + assertEnvelope(envelope, 10, 20, 30, 40); + assertEquals(DefaultGeographicCRS.WGS84, envelope.getCoordinateReferenceSystem()); + } + + @Test + public void testToFilter2DBBox() throws FactoryException { + Filter boxFilter = APIBBoxParser.toFilter(BBOX_2D_SPEC); + + assertNotNull(boxFilter); + assertThat(boxFilter, instanceOf(BBOX.class)); + BBOX bboxFilter = (BBOX) boxFilter; + assertEnvelope(bboxFilter.getBounds(), 10, 20, 30, 40); + assertEquals( + DefaultGeographicCRS.WGS84, bboxFilter.getBounds().getCoordinateReferenceSystem()); + assertEquals(DEFAULT_GEOMETRY, bboxFilter.getExpression1()); + } + + @Test + public void testParse3DBBox() throws FactoryException { + ReferencedEnvelope[] result = APIBBoxParser.parse(BBOX_3D_SPEC); + + assertNotNull(result); + assertEquals(1, result.length); + ReferencedEnvelope envelope = result[0]; + assertEnvelope(envelope, 10, 20, 5, 30, 40, 15); + assertEquals(WGS84_3D, envelope.getCoordinateReferenceSystem()); + } + + @Test + public void testToFilter3DBBox() throws FactoryException { + Filter boxFilter = APIBBoxParser.toFilter(BBOX_3D_SPEC); + + assertNotNull(boxFilter); + assertThat(boxFilter, instanceOf(BBOX.class)); + BBOX bboxFilter = (BBOX) boxFilter; + assertEnvelope(bboxFilter.getBounds(), 10, 20, 5, 30, 40, 15); + assertEquals(WGS84_3D, bboxFilter.getBounds().getCoordinateReferenceSystem()); + assertEquals(DEFAULT_GEOMETRY, bboxFilter.getExpression1()); + } + + @Test + public void testParse2DBBoxWithCRS() throws FactoryException { + String bbox = BBOX_2D_SPEC; + String crs = "EPSG:4326"; + ReferencedEnvelope[] result = APIBBoxParser.parse(bbox, crs); + + assertNotNull(result); + assertEquals(1, result.length); + ReferencedEnvelope envelope = result[0]; + assertEnvelope(envelope, 10, 20, 30, 40); + assertNotNull(envelope.getCoordinateReferenceSystem()); + assertEquals(crs, CRS.lookupIdentifier(envelope.getCoordinateReferenceSystem(), false)); + } + + @Test + public void testParseDatelineSpan() throws FactoryException { + ReferencedEnvelope[] result = APIBBoxParser.parse("160, 20, -160, 40"); + + assertNotNull(result); + assertEquals(2, result.length); + assertEnvelope(result[0], 160, 20, 180, 40); + assertEquals(DefaultGeographicCRS.WGS84, result[0].getCoordinateReferenceSystem()); + assertEnvelope(result[1], -180, 20, -160, 40); + assertEquals(DefaultGeographicCRS.WGS84, result[1].getCoordinateReferenceSystem()); + } + + @Test + public void testFilterDatelineSpan() throws FactoryException { + Filter filter = APIBBoxParser.toFilter("160, 20, -160, 40"); + + assertThat(filter, instanceOf(Or.class)); + Or or = (Or) filter; + assertThat(or.getChildren().get(0), instanceOf(BBOX.class)); + assertThat(or.getChildren().get(1), instanceOf(BBOX.class)); + + BBOX bbox1 = (BBOX) or.getChildren().get(0); + assertEnvelope(bbox1.getBounds(), 160, 20, 180, 40); + assertEquals(DefaultGeographicCRS.WGS84, bbox1.getBounds().getCoordinateReferenceSystem()); + assertEquals(DEFAULT_GEOMETRY, bbox1.getExpression1()); + + BBOX bbox2 = (BBOX) or.getChildren().get(1); + assertEnvelope(bbox2.getBounds(), -180, 20, -160, 40); + assertEquals(DefaultGeographicCRS.WGS84, bbox2.getBounds().getCoordinateReferenceSystem()); + assertEquals(DEFAULT_GEOMETRY, bbox2.getExpression1()); + } + + @Test + public void testToGeometryDatelineSpan() throws FactoryException { + Geometry geometry = APIBBoxParser.toGeometry("160, 20, -160, 40"); + + assertThat(geometry, instanceOf(MultiPolygon.class)); + Polygon p1 = (Polygon) geometry.getGeometryN(0); + assertTrue(p1.isRectangle()); + assertEquals(new Envelope(160, 180, 20, 40), p1.getEnvelopeInternal()); + Polygon p2 = (Polygon) geometry.getGeometryN(1); + assertTrue(p2.isRectangle()); + assertEquals(new Envelope(-180, -160, 20, 40), p2.getEnvelopeInternal()); + } + + @Test + public void testParse3DBBoxWithCRS() throws FactoryException { + String bbox = BBOX_3D_SPEC; + String crs = "EPSG:4326"; + ReferencedEnvelope[] result = APIBBoxParser.parse(bbox, crs); + + assertNotNull(result); + assertEquals(1, result.length); + ReferencedEnvelope envelope = result[0]; + assertEnvelope(envelope, 10, 20, 5, 30, 40, 15); + assertNotNull(envelope.getCoordinateReferenceSystem()); + assertEquals(crs, CRS.lookupIdentifier(envelope.getCoordinateReferenceSystem(), false)); + } + + private void assertEnvelope(BoundingBox bounds, double... expected) { + assertEnvelope(ReferencedEnvelope.reference(bounds), expected); + } + + private static void assertEnvelope(ReferencedEnvelope envelope, double... expected) { + if (expected.length == 6) { + ReferencedEnvelope3D envelope3D = (ReferencedEnvelope3D) envelope; + assertEquals(expected[0], envelope3D.getMinX(), EPS); + assertEquals(expected[1], envelope3D.getMinY(), EPS); + assertEquals(expected[2], envelope3D.getMinZ(), EPS); + assertEquals(expected[3], envelope3D.getMaxX(), EPS); + assertEquals(expected[4], envelope3D.getMaxY(), EPS); + assertEquals(expected[5], envelope3D.getMaxZ(), EPS); + } else { + assertEquals(expected[0], envelope.getMinX(), EPS); + assertEquals(expected[1], envelope.getMinY(), EPS); + assertEquals(expected[2], envelope.getMaxX(), EPS); + assertEquals(expected[3], envelope.getMaxY(), EPS); + } + } +} diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/APIRequestInfoTest.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/APIRequestInfoTest.java new file mode 100644 index 00000000000..044249ddc33 --- /dev/null +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/APIRequestInfoTest.java @@ -0,0 +1,149 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.ogcapi; + +import static org.geoserver.ogcapi.MappingJackson2YAMLMessageConverter.APPLICATION_YAML; +import static org.geoserver.ogcapi.MappingJackson2YAMLMessageConverter.APPLICATION_YAML_VALUE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.springframework.http.MediaType.APPLICATION_JSON; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import net.sf.json.JSONObject; +import org.geoserver.ows.Request; +import org.geoserver.ows.TestDispatcherCallback; +import org.geoserver.platform.Operation; +import org.geoserver.platform.Service; +import org.geoserver.test.GeoServerSystemTestSupport; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.MediaType; + +/** + * Test class for APIRequestInfo, uses custom callbacks to test the APIRequestInfo object (as it's + * available during the request processing) + */ +public class APIRequestInfoTest extends GeoServerSystemTestSupport { + + @Before + public void cleanupCallbacks() throws Exception { + APIDispatcher dispatcher = getAPIDispatcher(); + dispatcher.callbacks.removeIf(c -> c instanceof TestDispatcherCallback); + } + + @Test + public void testProducibleMediaTypes() throws Exception { + testDuringOperationExecuted( + ri -> { + Collection mediaTypes = + ri.getProducibleMediaTypes(Message.class, true); + assertThat(mediaTypes, Matchers.hasItems(APPLICATION_JSON, APPLICATION_YAML)); + }); + } + + @Test + public void testLandingPage() throws Exception { + testDuringOperationExecuted(ri -> assertEquals("ogc/hello/v1", ri.getServiceLandingPage())); + } + + @Test + public void testService() throws Exception { + testDuringOperationExecuted( + ri -> { + Service service = ri.getService(); + assertEquals("Hello", service.getId()); + assertThat(service.getService(), Matchers.instanceOf(HelloService.class)); + }); + } + + @Test + public void testIsFormatRequested() throws Exception { + testDuringOperationExecuted( + ri -> { + assertTrue(ri.isFormatRequested(APPLICATION_JSON, APPLICATION_JSON)); + }); + } + + @Test + public void testQueryMap() throws Exception { + APIDispatcher dispatcher = getAPIDispatcher(); + dispatcher.callbacks.add( + new TestDispatcherCallback() { + @Override + public Object operationExecuted( + Request request, Operation operation, Object result) { + APIRequestInfo ri = APIRequestInfo.get(); + assertThat(ri.getSimpleQueryMap(), Matchers.hasEntry("k1", "v1")); + assertThat(ri.getSimpleQueryMap(), Matchers.hasEntry("k2", "v2")); + + return null; + } + }); + // check no exceptions have been thrown + JSONObject json = (JSONObject) getAsJSON("ogc/hello/v1?k1=v1&k2=v2"); + assertEquals("Landing page", json.get("message")); + } + + @Test + public void getLinks() throws Exception { + testDuringOperationExecuted( + ri -> { + List links = + ri.getLinksFor( + "ogc/hello/v1/echo", + Message.class, + "Message as ", + "alternate", + true, + "test", + null); + assertEquals(4, links.size()); + + // check the links are correctly built + Set mimeTypes = new HashSet<>(); + for (Link link : links) { + assertEquals("alternate", link.getRel()); + assertEquals("test", link.getClassification()); + assertEquals("Message as " + link.getType(), link.getTitle()); + mimeTypes.add(link.getType()); + } + assertThat( + mimeTypes, + Matchers.hasItems( + MediaType.APPLICATION_JSON_VALUE, + MediaType.TEXT_HTML_VALUE, + MediaType.TEXT_PLAIN_VALUE, + APPLICATION_YAML_VALUE)); + }); + } + + private APIDispatcher getAPIDispatcher() { + return applicationContext.getBean(APIDispatcher.class); + } + + private void testDuringOperationExecuted(Consumer consumer) throws Exception { + APIDispatcher dispatcher = getAPIDispatcher(); + dispatcher.callbacks.add( + new TestDispatcherCallback() { + @Override + public Object operationExecuted( + Request request, Operation operation, Object result) { + APIRequestInfo ri = APIRequestInfo.get(); + consumer.accept(ri); + + return null; + } + }); + // check no exceptions have been thrown + JSONObject json = (JSONObject) getAsJSON("ogc/hello/v1/"); + assertEquals("Landing page", json.get("message")); + } +} diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/ApiConfigurationSupportTest.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/ApiConfigurationSupportTest.java index c6e75c7ae24..96d8574cd52 100644 --- a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/ApiConfigurationSupportTest.java +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/ApiConfigurationSupportTest.java @@ -115,7 +115,7 @@ public void testReadWithControllerContext() { return ((GenericHttpMessageConverter) c) .canRead( Message.class, - HelloController.class, + HelloService.class, MediaType.APPLICATION_JSON); else return c.canRead(Message.class, MediaType.APPLICATION_JSON); }); diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/DateTimeConverterTest.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/DateTimeConverterTest.java new file mode 100644 index 00000000000..a2c89b37779 --- /dev/null +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/DateTimeConverterTest.java @@ -0,0 +1,102 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.ogcapi; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; +import org.geotools.util.DateRange; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +public class DateTimeConverterTest { + + private DateTimeConverter dateTimeConverter; + private SimpleDateFormat dateFormat; + + @Before + public void setUp() { + // Ensure tests are not timezone dependent + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + dateTimeConverter = new DateTimeConverter(); + dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + @Test + public void testSingleDate() throws ParseException { + String input = "2023-10-01T12:00:00.000Z"; + Date expected = dateFormat.parse("2023-10-01T12:00:00.000Z"); + DateTimeList result = dateTimeConverter.convert(input); + assertEquals(1, result.size()); + assertTrue(result.get(0) instanceof Date); + assertEquals(expected, result.get(0)); + } + + @Test + public void testIntervalClosed() throws ParseException { + String input = "2023-10-01T12:00:00.000Z/2023-10-01T14:00:00.000Z"; + Date start = dateFormat.parse("2023-10-01T12:00:00.000Z"); + Date end = dateFormat.parse("2023-10-01T14:00:00.000Z"); + DateTimeList result = dateTimeConverter.convert(input); + assertEquals(1, result.size()); + assertTrue(result.get(0) instanceof DateRange); + DateRange range = (DateRange) result.get(0); + assertEquals(start, range.getMinValue()); + assertEquals(end, range.getMaxValue()); + } + + @Test + @Ignore // see TODO in DateTimeConverter + public void testIntervalOpenStart() throws ParseException { + String input = "../2023-10-01T14:00:00.000Z"; + Date end = dateFormat.parse("2023-10-01T14:00:00.000Z"); + DateTimeList result = dateTimeConverter.convert(input); + assertEquals(1, result.size()); + assertTrue(result.get(0) instanceof DateRange); + DateRange range = (DateRange) result.get(0); + assertNull(range.getMinValue()); + assertEquals(end, range.getMaxValue()); + } + + @Test + @Ignore // see TODO in DateTimeConverter + public void testIntervalOpenEnd() throws ParseException { + String input = "2023-10-01T12:00:00.000Z/.."; + Date start = dateFormat.parse("2023-10-01T12:00:00.000Z"); + DateTimeList result = dateTimeConverter.convert(input); + assertEquals(1, result.size()); + assertTrue(result.get(0) instanceof DateRange); + DateRange range = (DateRange) result.get(0); + assertEquals(start, range.getMinValue().toInstant()); + assertNull(range.getMaxValue()); + } + + @Test + public void testSequenceWithSingleTimesAndIntervals() throws ParseException { + String input = + "2023-10-01T11:00:00.000Z,2023-10-01T12:00:00.000Z/2023-10-01T14:00:00.000Z,2023-10-01T15:00:00.000Z"; + Date date1 = dateFormat.parse("2023-10-01T11:00:00.000Z"); + Date date2 = dateFormat.parse("2023-10-01T12:00:00.000Z"); + Date date3 = dateFormat.parse("2023-10-01T14:00:00.000Z"); + Date date4 = dateFormat.parse("2023-10-01T15:00:00.000Z"); + DateTimeList result = dateTimeConverter.convert(input); + assertEquals(3, result.size()); + assertTrue(result.get(0) instanceof Date); + assertEquals(date1, result.get(0)); + assertTrue(result.get(1) instanceof DateRange); + DateRange range = (DateRange) result.get(1); + assertEquals(date2, range.getMinValue()); + assertEquals(date3, range.getMaxValue()); + assertTrue(result.get(2) instanceof Date); + assertEquals(date4, result.get(2)); + } +} diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloDocument.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloDocument.java new file mode 100644 index 00000000000..22e2aec54f9 --- /dev/null +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloDocument.java @@ -0,0 +1,17 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.ogcapi; + +public class HelloDocument extends AbstractDocument { + String message; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloResponseMessageConverter.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloResponseMessageConverter.java new file mode 100644 index 00000000000..76951a6023a --- /dev/null +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloResponseMessageConverter.java @@ -0,0 +1,16 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.ogcapi; + +import org.springframework.stereotype.Component; + +/** Helps testing the bridge between OWS responses and OGC APIs */ +@Component +public class HelloResponseMessageConverter extends MessageConverterResponseAdapter { + + public HelloResponseMessageConverter() { + super(Message.class, Message.class); + } +} diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloController.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloService.java similarity index 72% rename from src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloController.java rename to src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloService.java index d9d6b3cbab0..c9fe5896492 100644 --- a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloController.java +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloService.java @@ -7,6 +7,7 @@ import javax.servlet.http.HttpServletResponse; import org.geoserver.config.ServiceInfo; +import org.geoserver.config.impl.ServiceInfoImpl; import org.geoserver.ows.HttpErrorCodeException; import org.geoserver.platform.ServiceException; import org.springframework.http.HttpHeaders; @@ -26,13 +27,25 @@ service = "Hello", version = "1.0.1", landingPage = "ogc/hello/v1", - serviceClass = HelloController.HelloServiceInfo.class) + serviceClass = HelloService.HelloServiceInfo.class) @RequestMapping(path = APIDispatcher.ROOT_PATH + "/hello/v1") -public class HelloController { +public class HelloService { + + public static final String DEFAULT_GREETING = "hello"; + private final HelloServiceInfoImpl serviceInfo; static interface HelloServiceInfo extends ServiceInfo {} - String defaultValue = "hello"; + static class HelloServiceInfoImpl extends ServiceInfoImpl implements HelloServiceInfo {} + + String defaultValue = DEFAULT_GREETING; + + public HelloService() { + this.serviceInfo = new HelloServiceInfoImpl(); + this.serviceInfo.setName("Hello"); + this.serviceInfo.setTitle("Hello Service"); + this.serviceInfo.setEnabled(true); + } @GetMapping(name = "landingPage") @ResponseBody @@ -40,7 +53,7 @@ public Message landingPage() { return new Message("Landing page"); } - @GetMapping(path = "hello", name = "sayHello") + @GetMapping(path = DEFAULT_GREETING, name = "sayHello") @ResponseBody public Message hello(@RequestParam(name = "message", required = false) String message) { return new Message(message != null ? message : defaultValue); @@ -89,4 +102,19 @@ public void httpErrorCodeExceptionWithContentType() { throw new HttpErrorCodeException(HttpServletResponse.SC_OK, "{\"hello\":\"world\"}") .setContentType("application/json"); } + + @GetMapping(path = "document", name = "document") + @ResponseBody + @HTMLResponseBody(templateName = "message.ftl", fileName = "message.html") + public HelloDocument document() { + HelloDocument doc = new HelloDocument(); + doc.setMessage(defaultValue); + doc.addSelfLinks("ogc/hello/v1/document"); + return doc; + } + + /** Used by the {@link org.geoserver.ows.DisabledServiceCheck} */ + public HelloServiceInfo getServiceInfo() { + return serviceInfo; + } } diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/APIDispatcherTest.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloServiceTest.java similarity index 72% rename from src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/APIDispatcherTest.java rename to src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloServiceTest.java index d903e471e7a..d3055a6544b 100644 --- a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/APIDispatcherTest.java +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/HelloServiceTest.java @@ -6,11 +6,12 @@ package org.geoserver.ogcapi; +import static org.geoserver.ogcapi.MappingJackson2YAMLMessageConverter.APPLICATION_YAML_VALUE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.UnsupportedEncodingException; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; @@ -18,17 +19,17 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import javax.servlet.http.HttpServletResponse; +import net.sf.json.JSONArray; +import net.sf.json.JSONObject; import org.geoserver.ows.Request; import org.geoserver.ows.Response; import org.geoserver.ows.TestDispatcherCallback; import org.geoserver.platform.Operation; import org.geoserver.platform.Service; import org.geoserver.test.CodeExpectingHttpServletResponse; -import org.junit.After; +import org.geoserver.test.GeoServerSystemTestSupport; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; -import org.springframework.context.support.FileSystemXmlApplicationContext; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; @@ -36,25 +37,23 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.servlet.handler.DispatcherServletWebRequest; -@Ignore -public class APIDispatcherTest { - - private FileSystemXmlApplicationContext applicationContext; +public class HelloServiceTest extends GeoServerSystemTestSupport { @Before - public void setup() { - URL url = getClass().getResource("applicationContext.xml"); - this.applicationContext = new FileSystemXmlApplicationContext(url.toString()); + public void cleanupCallbacks() throws Exception { + APIDispatcher dispatcher = getAPIDispatcher(); + dispatcher.callbacks.removeIf(c -> c instanceof TestDispatcherCallback); } - @After - public void teardown() { - this.applicationContext.close(); + @Before + public void cleanupDefaultValue() throws Exception { + HelloService controller = applicationContext.getBean(HelloService.class); + controller.defaultValue = HelloService.DEFAULT_GREETING; } @Test public void testDefaultFormat() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); MockHttpServletRequest request = setupHelloRequest(); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -67,7 +66,7 @@ public void testDefaultFormat() throws Exception { @Test public void testQueryParameters() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); MockHttpServletRequest request = setupHelloRequest("message", "yo", "f", "json"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -79,37 +78,35 @@ public void testQueryParameters() throws Exception { } @Test - @Ignore // restore when XML is back as supported format - public void testXMLFormatQueryParameter() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + public void testYAMLFormatQueryParameter() throws Exception { + APIDispatcher dispatcher = getAPIDispatcher(); - MockHttpServletRequest request = setupHelloRequest("f", "xml"); + MockHttpServletRequest request = setupHelloRequest("f", "yaml"); MockHttpServletResponse response = new MockHttpServletResponse(); dispatcher.handleRequest(request, response); assertEquals(200, response.getStatus()); - assertEquals(MediaType.APPLICATION_XML_VALUE, response.getContentType()); - assertEquals("hello", response.getContentAsString()); + assertEquals(APPLICATION_YAML_VALUE, response.getContentType()); + assertEquals("message: hello\n", response.getContentAsString()); } @Test - @Ignore // restore when XML is back as supported format - public void testXMLFormatAcceptHeader() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + public void testYAMLFormatAcceptHeader() throws Exception { + APIDispatcher dispatcher = getAPIDispatcher(); MockHttpServletRequest request = setupHelloRequest(); - request.addHeader(HttpHeaders.ACCEPT, "application/xml"); + request.addHeader(HttpHeaders.ACCEPT, APPLICATION_YAML_VALUE); MockHttpServletResponse response = new MockHttpServletResponse(); dispatcher.handleRequest(request, response); assertEquals(200, response.getStatus()); - assertEquals(MediaType.APPLICATION_XML_VALUE, response.getContentType()); - assertEquals("hello", response.getContentAsString()); + assertEquals(APPLICATION_YAML_VALUE, response.getContentType()); + assertEquals("message: hello\n", response.getContentAsString()); } @Test public void testPostRequest() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); String message = "{\"message\":\"Is there anyone here?\"}"; MockHttpServletRequest request = setupEchoRequest(message, "f", "json"); @@ -124,7 +121,7 @@ public void testPostRequest() throws Exception { @Test public void testDeleteRequest() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); MockHttpServletRequest request = setupDeleteRequest(); request.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); @@ -136,24 +133,23 @@ public void testDeleteRequest() throws Exception { @Test public void testPutRequest() throws Exception { - try (FileSystemXmlApplicationContext context = this.applicationContext) { - APIDispatcher dispatcher = context.getBean(APIDispatcher.class); - HelloController controller = context.getBean(HelloController.class); - - String newDefault = "ciao"; - MockHttpServletRequest request = setupPutRequest(newDefault); - request.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN); - MockHttpServletResponse response = new MockHttpServletResponse(); - dispatcher.handleRequest(request, response); - - assertEquals(200, response.getStatus()); - assertEquals(newDefault, controller.defaultValue); - } + + APIDispatcher dispatcher = applicationContext.getBean(APIDispatcher.class); + HelloService controller = applicationContext.getBean(HelloService.class); + + String newDefault = "ciao"; + MockHttpServletRequest request = setupPutRequest(newDefault); + request.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN); + MockHttpServletResponse response = new MockHttpServletResponse(); + dispatcher.handleRequest(request, response); + + assertEquals(200, response.getStatus()); + assertEquals(newDefault, controller.defaultValue); } @Test public void testDispatcherCallback() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); TestDispatcherCallback callback = new TestDispatcherCallback(); MockHttpServletRequest request = setupHelloRequest(); @@ -168,7 +164,7 @@ public void testDispatcherCallback() throws Exception { @Test public void testDispatcherCallbackOperationName() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); AtomicReference requestReference = new AtomicReference<>(); TestDispatcherCallback callback = new TestDispatcherCallback() { @@ -186,13 +182,13 @@ public Operation operationDispatched(Request request, Operation operation) { dispatcher.handleRequest(request, response); assertEquals(200, response.getStatus()); assertEquals("Hello", requestReference.get().getService()); - assertEquals("1.0", requestReference.get().getVersion()); + assertEquals("1.0.1", requestReference.get().getVersion()); assertEquals("sayHello", requestReference.get().getRequest()); } @Test public void testDispatcherCallbackFailInit() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); final TestDispatcherCallback callback1 = new TestDispatcherCallback(); final TestDispatcherCallback callback2 = new TestDispatcherCallback(); @@ -224,7 +220,7 @@ public Request init(Request request) { @Test public void testDispatcherCallbackFailServiceDispatched() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); final TestDispatcherCallback callback1 = new TestDispatcherCallback(); final TestDispatcherCallback callback2 = new TestDispatcherCallback(); TestDispatcherCallback callbackFail = @@ -254,7 +250,7 @@ public Service serviceDispatched(Request request, Service service) { @Test public void testDispatcherCallbackFailOperationDispatched() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); final TestDispatcherCallback callback1 = new TestDispatcherCallback(); final TestDispatcherCallback callback2 = new TestDispatcherCallback(); TestDispatcherCallback callbackFail = @@ -283,7 +279,7 @@ public Operation operationDispatched(Request request, Operation operation) { @Test public void testDispatcherCallbackFailOperationExecuted() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); final TestDispatcherCallback callback1 = new TestDispatcherCallback(); final TestDispatcherCallback callback2 = new TestDispatcherCallback(); TestDispatcherCallback callbackFail = @@ -314,7 +310,7 @@ public Object operationExecuted( @Test public void testDispatcherCallbackFailResponseDispatched() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); final TestDispatcherCallback callback1 = new TestDispatcherCallback(); final TestDispatcherCallback callback2 = new TestDispatcherCallback(); TestDispatcherCallback callbackFail = @@ -348,7 +344,7 @@ public Response responseDispatched( @Test public void testDispatcherCallbackFailFinished() throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); final AtomicBoolean firedCallback = new AtomicBoolean(false); TestDispatcherCallback callback1 = new TestDispatcherCallback(); TestDispatcherCallback callback2 = @@ -407,14 +403,90 @@ public void testHttpErrorCodeExceptionWithContentType() throws Exception { assertEquals("application/json", rsp.getContentType()); } + @Test + public void testDocumentDefaultMime() throws Exception { + MockHttpServletResponse response = getAsServletResponse("ogc/hello/v1/document"); + assertEquals(MediaType.APPLICATION_JSON_VALUE, response.getContentType()); + JSONObject json = (JSONObject) json(response); + // links to self and alternate representations + assertEquals("hello", json.get("message")); + JSONArray links = json.getJSONArray("links"); + assertEquals(3, links.size()); + for (int i = 0; i < links.size(); i++) { + JSONObject link = links.getJSONObject(i); + if ("self".equals(link.getString("rel"))) { + assertEquals("This document", link.getString("title")); + assertEquals("application/json", link.getString("type")); + assertEquals( + "http://localhost:8080/geoserver/ogc/hello/v1/document?f=application%2Fjson", + link.getString("href")); + } else if ("alternate".equals(link.getString("rel")) + && "application/x-yaml".equals(link.getString("type"))) { + assertEquals("This document as application/x-yaml", link.getString("title")); + assertEquals( + "http://localhost:8080/geoserver/ogc/hello/v1/document?f=application%2Fx-yaml", + link.getString("href")); + } else if ("alternate".equals(link.getString("rel"))) { + assertEquals("This document as text/html", link.getString("title")); + assertEquals("text/html", link.getString("type")); + assertEquals( + "http://localhost:8080/geoserver/ogc/hello/v1/document?f=text%2Fhtml", + link.getString("href")); + } else { + fail("Unexpected link: " + link); + } + } + } + + @Test + public void testDocumentHTML() throws Exception { + MockHttpServletResponse response = getAsServletResponse("ogc/hello/v1/document?f=html"); + assertEquals(MediaType.TEXT_HTML_VALUE, response.getContentType()); + // Testing: + // - the message is in the body + // - service link generation + // - resource link generation + String expected = + "\n" + + "\n" + + " \n" + + "\n" + + "\n" + + "

The message: hello

\n" + + "

Capabilities URL

\n" + + "\n" + + ""; + assertEquals(expected, response.getContentAsString()); + } + + @Test + public void testHelloPlainText() throws Exception { + MockHttpServletResponse response = getAsServletResponse("ogc/hello/v1/hello?f=text/plain"); + assertEquals(MediaType.TEXT_PLAIN_VALUE, response.getContentType()); + assertEquals("hello", response.getContentAsString()); + } + + @Test + public void testeServiceDisabled() throws Exception { + HelloService hs = applicationContext.getBean(HelloService.class); + try { + hs.getServiceInfo().setEnabled(false); + MockHttpServletResponse response = getAsServletResponse("ogc/hello/v1"); + assertEquals(404, response.getStatus()); + assertEquals("Service Hello is disabled", response.getErrorMessage()); + } finally { + hs.getServiceInfo().setEnabled(true); + } + } + private CodeExpectingHttpServletResponse assertHttpErrorCode(String path, int expectedCode) throws Exception { - APIDispatcher dispatcher = getDispatcher(); + APIDispatcher dispatcher = getAPIDispatcher(); MockHttpServletRequest request = setupRequestBase(); request.setMethod("GET"); - request.setPathInfo("/geoserver/ogc/" + path); - request.setRequestURI("/geoserver/ogc/" + path); + request.setPathInfo("/geoserver/ogc/hello/v1/" + path); + request.setRequestURI("/geoserver/ogc/hello/v1/" + path); CodeExpectingHttpServletResponse response = new CodeExpectingHttpServletResponse(new MockHttpServletResponse()); @@ -426,14 +498,14 @@ private CodeExpectingHttpServletResponse assertHttpErrorCode(String path, int ex return response; } - private APIDispatcher getDispatcher() { + private APIDispatcher getAPIDispatcher() { return applicationContext.getBean(APIDispatcher.class); } private MockHttpServletRequest setupDeleteRequest() { MockHttpServletRequest request = setupRequestBase(); - request.setPathInfo("/geoserver/ogc/delete"); - request.setRequestURI("/geoserver/ogc/delete"); + request.setPathInfo("/geoserver/ogc/hello/v1/delete"); + request.setRequestURI("/geoserver/ogc/hello/v1/delete"); request.setMethod("DELETE"); return request; @@ -441,10 +513,10 @@ private MockHttpServletRequest setupDeleteRequest() { private MockHttpServletRequest setupPutRequest(String message, String... params) { MockHttpServletRequest request = setupRequestBase(params); - request.setPathInfo("/geoserver/ogc/default"); + request.setPathInfo("/geoserver/ogc/hello/v1/default"); request.setMethod("PUT"); - request.setRequestURI("/geoserver/ogc/default"); + request.setRequestURI("/geoserver/ogc/hello/v1/default"); request.setContent(message.getBytes(StandardCharsets.UTF_8)); return request; @@ -452,10 +524,10 @@ private MockHttpServletRequest setupPutRequest(String message, String... params) private MockHttpServletRequest setupEchoRequest(String message, String... params) { MockHttpServletRequest request = setupRequestBase(params); - request.setPathInfo("/geoserver/ogc/hello"); + request.setPathInfo("/geoserver/ogc/hello/v1/hello"); request.setMethod("POST"); - request.setRequestURI("/geoserver/ogc/echo"); + request.setRequestURI("/geoserver/ogc/hello/v1/echo"); request.setContent(message.getBytes(StandardCharsets.UTF_8)); return request; @@ -463,9 +535,9 @@ private MockHttpServletRequest setupEchoRequest(String message, String... params private MockHttpServletRequest setupHelloRequest(String... params) { MockHttpServletRequest request = setupRequestBase(params); - request.setPathInfo("/geoserver/ogc/hello"); + request.setPathInfo("/geoserver/ogc/hello/v1/hello"); request.setMethod("GET"); - request.setRequestURI("/geoserver/ogc/hello"); + request.setRequestURI("/geoserver/ogc/hello/v1/hello"); return request; } diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/MessageResponse.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/MessageResponse.java new file mode 100644 index 00000000000..70be5d13cd3 --- /dev/null +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/MessageResponse.java @@ -0,0 +1,35 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.ogcapi; + +import java.io.IOException; +import java.io.OutputStream; +import org.geoserver.ows.Response; +import org.geoserver.platform.Operation; +import org.springframework.stereotype.Component; + +/** + * A response that writes a message to the output stream, used to test the bridge between OWS + * responses and OGC APIs + */ +@Component +public class MessageResponse extends Response { + public MessageResponse() { + super(Message.class, "text/plain"); + } + + @Override + public String getMimeType(Object value, Operation operation) { + return "text/plain"; + } + + @Override + public void write(Object value, OutputStream output, Operation operation) throws IOException { + Message message = (Message) value; + output.write(message.message.getBytes()); + } + + public void abort(Object value, OutputStream output, Operation operation) throws IOException {} +} diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/PaginationLinksBuilderTest.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/PaginationLinksBuilderTest.java new file mode 100644 index 00000000000..410a05c8f92 --- /dev/null +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/PaginationLinksBuilderTest.java @@ -0,0 +1,61 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.ogcapi; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.easymock.EasyMock; +import org.junit.BeforeClass; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; + +public class PaginationLinksBuilderTest { + + public static final String PATH = "service/v1/resource"; + public static final String URL_BASE = "http://localhost/service/v1/resource"; + + @BeforeClass + public static void setupRequestInfo() { + RequestAttributes attributes = EasyMock.niceMock(RequestAttributes.class); + RequestContextHolder.setRequestAttributes(attributes); + APIRequestInfo requestInfo = + new APIRequestInfo( + new MockHttpServletRequest(), + new MockHttpServletResponse(), + EasyMock.mock(APIDispatcher.class)); + APIRequestInfo.set(requestInfo); + EasyMock.expect( + attributes.getAttribute( + APIRequestInfo.KEY, RequestAttributes.SCOPE_REQUEST)) + .andReturn(requestInfo) + .anyTimes(); + EasyMock.replay(attributes); + } + + @Test + public void testFirst() { + PaginationLinksBuilder builder = new PaginationLinksBuilder(PATH, 0, 5, 5, 250); + assertNull(builder.getPrevious()); + assertEquals(URL_BASE + "?startIndex=5&limit=5", builder.getNext()); + } + + @Test + public void testMid() { + PaginationLinksBuilder builder = new PaginationLinksBuilder(PATH, 5, 5, 5, 250); + assertEquals(URL_BASE + "?startIndex=0&limit=5", builder.getPrevious()); + assertEquals(URL_BASE + "?startIndex=10&limit=5", builder.getNext()); + } + + @Test + public void testLast() { + PaginationLinksBuilder builder = new PaginationLinksBuilder(PATH, 245, 5, 5, 250); + assertEquals(URL_BASE + "?startIndex=240&limit=5", builder.getPrevious()); + assertNull(builder.getNext()); + } +} diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/impl/LinkInfoImplTest.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/impl/LinkInfoImplTest.java new file mode 100644 index 00000000000..86baf07f72d --- /dev/null +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/impl/LinkInfoImplTest.java @@ -0,0 +1,100 @@ +/* (c) 2024 Open Source Geospatial Foundation - all rights reserved + * This code is licensed under the GPL 2.0 license, available at the root + * application directory. + */ +package org.geoserver.ogcapi.impl; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +public class LinkInfoImplTest { + + private LinkInfoImpl linkInfo; + + @Before + public void setUp() { + linkInfo = new LinkInfoImpl(); + } + + @Test + public void testRel() { + linkInfo.setRel("self"); + assertEquals("self", linkInfo.getRel()); + } + + @Test + public void testType() { + linkInfo.setType("application/json"); + assertEquals("application/json", linkInfo.getType()); + } + + @Test + public void testTitle() { + linkInfo.setTitle("Example Title"); + assertEquals("Example Title", linkInfo.getTitle()); + } + + @Test + public void testHref() { + linkInfo.setHref("http://example.com"); + assertEquals("http://example.com", linkInfo.getHref()); + } + + @Test + public void testService() { + linkInfo.setService("Example Service"); + assertEquals("Example Service", linkInfo.getService()); + } + + @Test + public void testEquals() { + LinkInfoImpl anotherLinkInfo = new LinkInfoImpl(); + anotherLinkInfo.setRel("self"); + anotherLinkInfo.setType("application/json"); + anotherLinkInfo.setTitle("Example Title"); + anotherLinkInfo.setHref("http://example.com"); + anotherLinkInfo.setService("Example Service"); + + linkInfo.setRel("self"); + linkInfo.setType("application/json"); + linkInfo.setTitle("Example Title"); + linkInfo.setHref("http://example.com"); + linkInfo.setService("Example Service"); + + assertEquals(linkInfo, anotherLinkInfo); + } + + @Test + public void testHashCode() { + LinkInfoImpl anotherLinkInfo = new LinkInfoImpl(); + anotherLinkInfo.setRel("self"); + anotherLinkInfo.setType("application/json"); + anotherLinkInfo.setTitle("Example Title"); + anotherLinkInfo.setHref("http://example.com"); + anotherLinkInfo.setService("Example Service"); + + linkInfo.setRel("self"); + linkInfo.setType("application/json"); + linkInfo.setTitle("Example Title"); + linkInfo.setHref("http://example.com"); + linkInfo.setService("Example Service"); + + assertEquals(linkInfo.hashCode(), anotherLinkInfo.hashCode()); + } + + @Test + public void testClone() throws CloneNotSupportedException { + linkInfo.setRel("self"); + linkInfo.setType("application/json"); + linkInfo.setTitle("Example Title"); + linkInfo.setHref("http://example.com"); + linkInfo.setService("Example Service"); + + LinkInfoImpl clonedLinkInfo = (LinkInfoImpl) linkInfo.clone(); + + assertEquals(linkInfo, clonedLinkInfo); + assertEquals(linkInfo.hashCode(), clonedLinkInfo.hashCode()); + } +} diff --git a/src/community/ogcapi/ogcapi-core/src/test/resources/org/geoserver/ogcapi/applicationContext.xml b/src/community/ogcapi/ogcapi-core/src/test/resources/org/geoserver/ogcapi/applicationContext.xml deleted file mode 100644 index 0a9884e65cd..00000000000 --- a/src/community/ogcapi/ogcapi-core/src/test/resources/org/geoserver/ogcapi/applicationContext.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/community/ogcapi/ogcapi-core/src/test/resources/org/geoserver/ogcapi/message.ftl b/src/community/ogcapi/ogcapi-core/src/test/resources/org/geoserver/ogcapi/message.ftl new file mode 100644 index 00000000000..8105c7baa6f --- /dev/null +++ b/src/community/ogcapi/ogcapi-core/src/test/resources/org/geoserver/ogcapi/message.ftl @@ -0,0 +1,9 @@ + + + + + +

The message: ${model.message}

+

Capabilities URL

+ + \ No newline at end of file From fb78bd97ede38df20dfbd5ad930e8f59cc1cb7ea Mon Sep 17 00:00:00 2001 From: Gabriel Roldan Date: Tue, 5 Nov 2024 17:08:49 -0300 Subject: [PATCH 23/43] Remove duplicate dependency declaration for gt-cql2-text in the root pom --- src/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pom.xml b/src/pom.xml index a4d19f2b37d..57154e234a7 100644 --- a/src/pom.xml +++ b/src/pom.xml @@ -645,11 +645,6 @@ gt-complex ${gt.version} - - org.geotools - gt-cql2-text - ${gt.version} - org.geotools gt-css From b99c2950e829c3e85cd464340e46aa3f051713ab Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Wed, 6 Nov 2024 11:00:21 +0100 Subject: [PATCH 24/43] Small refresh of OGC API - Features documentation --- .../ogc-api/features/img/features.png | Bin 63842 -> 64667 bytes .../community/ogc-api/features/index.rst | 80 ++++++++---------- 2 files changed, 34 insertions(+), 46 deletions(-) diff --git a/doc/en/user/source/community/ogc-api/features/img/features.png b/doc/en/user/source/community/ogc-api/features/img/features.png index 729791adb2051e357fa024f243be214c111c95f3..2f9fdc87b067b45c3404cbe870eb4d3df6bf7e42 100644 GIT binary patch literal 64667 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

>YL50I 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 literal 63842 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 W-Fd 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 + - `1.0.1 `__ - Passes compliance tests * - Part 2: Coordinate Systems by Reference - - `1.0.0 `__ + - `1.0.1 `__ - Passes compliance tests * - Part 3: Filtering - - `Draft `__ - - Draft implemented (mind, the draft does not include a filtering language) + - `1.0.0 `__ + - Implemented an earlier draft, being updated to final (no CITE tests yet) + * - Common Query Language (CQL2) + - `1.0.0 `__ + - Implemented an earlier draft, being updated to final (no CITE tests yet) * - Part 4: Create, Replace, Update and Delete - - `Draft `__ + - `1.0.0 `__ + - Not implemented (volunteers/sponsoring wanted) + * - Part 5: search + - `Proposal DRAFT `__ + - A search endpoint for complex queries is implemented at the single collection level (POST to immediately get a response, no support for stored queries). + * - Part 6 - Schemas + - `Proposal DRAFT `__ - Not implemented (volunteers/sponsoring wanted) - * - Common Query Language (CQL2) - - `Draft `__ - - Implements an earlier draft for for both text and JSON encodings. To be updated. * - Part n: Query by IDs - - `Proposal `__ + - `Proposal DRAFT `__ - Proposal implemented, but syntax and semantic is subject to change in a future release. Thus said, usage should be carefully considered. + * - Sorting + - `DRAFT in github `__ + - Partial implementation borrowed by OGC API Records, using the sortby parameter. Sortables are not exposed. Installing the GeoServer OGC API Features module ------------------------------------------------ @@ -48,7 +57,7 @@ Installing the GeoServer OGC API Features module Use of OGC API - Features service --------------------------------- -The OGC API Features Service is accessed via the :guilabel:`FEATURES` version :guilabel:`1.0` link on the home page. +The OGC API Features Service is accessed via the :guilabel:`Features` version :guilabel:`1.0.1` link on the home page. Capabilities '''''''''''' @@ -66,48 +75,27 @@ The service is self described using: .. code-block:: json { - "title": "GeoServer Web Feature Service", - "description": "This is the reference implementation of WFS 1.0.0 and WFS 1.1.0, supports all WFS operations including Transaction.", + "title": "GeoServer Features services", + "description": "This services delivers vector data in raw form, including both geometries and attributes.", "links": [ { - "href": "http://localhost:8080/geoserver/ogc/features/?f=application%2Fx-yaml", - "rel": "alternate", - "type": "application/x-yaml", - "title": "This document as application/x-yaml" - }, - { - "href": "http://localhost:8080/geoserver/ogc/features/?f=application%2Fjson", + "href": "https://gs-main.geosolutionsgroup.com/geoserver/ogc/features/v1/?f=application%2Fjson", "rel": "self", "type": "application/json", "title": "This document" }, { - "href": "http://localhost:8080/geoserver/ogc/features/?f=text%2Fhtml", + "href": "https://gs-main.geosolutionsgroup.com/geoserver/ogc/features/v1/?f=application%2Fx-yaml", + "rel": "alternate", + "type": "application/x-yaml", + "title": "This document as application/x-yaml" + }, + { + "href": "https://gs-main.geosolutionsgroup.com/geoserver/ogc/features/v1/?f=text%2Fhtml", "rel": "alternate", "type": "text/html", "title": "This document as text/html" - } - -* ``application/x-yaml``: A collection of :file:`yaml` documents, with references between each document for programmatic access. - - .. code-block:: yaml - - title: GeoServer Web Feature Service - description: This is the reference implementation of WFS 1.0.0 and WFS 1.1.0, supports - all WFS operations including Transaction. - links: - - href: http://localhost:8080/geoserver/ogc/features/?f=application%2Fx-yaml - rel: self - type: application/x-yaml - title: This document - - href: http://localhost:8080/geoserver/ogc/features/?f=application%2Fjson - rel: alternate - type: application/json - title: This document as application/json - - href: http://localhost:8080/geoserver/ogc/features/?f=text%2Fhtml - rel: alternate - type: text/html - title: This document as text/html + }, The service title and description are provided by the existing :ref:`wfs` settings. @@ -177,6 +165,7 @@ To override an OGC API Features template: #. Create a file in this location, using the GeoServer |release| examples below: + * :download:`ogc/features/v1/landingPage.ftl ` * :download:`ogc/features/v1/collection.ftl ` * :download:`ogc/features/v1/collection_include.ftl ` * :download:`ogc/features/v1/collections.ftl ` @@ -185,15 +174,14 @@ To override an OGC API Features template: The above built-in examples are for GeoServer |release|, please check for any changes when upgrading GeoServer. -The templates for listing feature content are shared between OGC API services. To override a template used to list features: +To override a template used to list features: -#. Use the directory in the location you wish to override: +#. Use the directory in the location you wish to override (can be general, specific to a workspace, datastore, or feature type): * :file:`GEOSERVER_DATA_DIR/templates` * :file:`GEOSERVER_DATA_DIR/workspace/{workspace}` * :file:`GEOSERVER_DATA_DIR/workspace/{workspace}/{datastore}` * :file:`GEOSERVER_DATA_DIR/workspace/{workspace}/{datastore}/{featuretype}` - * :download:`ogc/features/landingPage.ftl ` #. Create a file in this location, using the GeoServer |release| examples below: @@ -231,7 +219,7 @@ As an example customize how collections are listed: Presently each family of templates manages its own :file:`common-header.ftl` (as shown in the difference between :file:`ogc/features` service templates, and getfeature templates above). -#. A restart is required, as templates are cached. +#. A restart is not required, the system will notice when the template is updated and apply the changes automatically. .. figure:: img/template_override.png From f082b2045dd872b5fe4a3623f460f17e31d13a37 Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Thu, 7 Nov 2024 14:44:36 +0100 Subject: [PATCH 25/43] Clarify the requirements for Content dependent legends parameters The way it is stated now is misleading, because the LEGEND_OPTIONS params are required, but they are mentioned after the optional parameters. --- .../source/services/wms/get_legend_graphic/index.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/en/user/source/services/wms/get_legend_graphic/index.rst b/doc/en/user/source/services/wms/get_legend_graphic/index.rst index 34a2ff049b4..11cbaec560f 100644 --- a/doc/en/user/source/services/wms/get_legend_graphic/index.rst +++ b/doc/en/user/source/services/wms/get_legend_graphic/index.rst @@ -234,14 +234,14 @@ In order to support it the GetLegendGraphic call needs the following extra param * BBOX * SRS or CRS (depending on the WMS version, SRS for 1.1.1 and CRS for 1.3.0) * SRCWIDTH and SRCHEIGHT, the size of the reference map (width and height already have a different meaning in GetLegendGraphic) + +and the following LEGEND_OPTIONS parameters: + + * countMatched: adds the number of features matching the particular rule at the end of the rule label (requires visible labels to work). Applicable only to vector layers. + * hideEmptyRules: hides rules that are not matching any feature. Applicable only if countMatched is true. Other parameters can also be added to better match the GetMap request, for example, it is recommended to mirror filtering vendor parameters such as, for example, CQL_FILTER,FILTER,FEATUREID,TIME,ELEVATION. - -Content dependent evaluation is enabled via the following LEGEND_OPTIONS parameters: - - * countMatched: adds the number of features matching the particular rule at the end of the rule label (requires visible labels to work). Applicable only to vector layers. - * hideEmptyRules: hides rules that are not matching any feature. Applicable only if countMatched is true. For example, let's assume the following layout is added to GeoServer (``legend.xml`` to be placed in ``GEOSERVER_DATA_DIR/layouts``):: From 3a43a098e47b31a5fef017c9e78216b8463e0c20 Mon Sep 17 00:00:00 2001 From: Giovanni Allegri Date: Thu, 7 Nov 2024 14:53:37 +0100 Subject: [PATCH 26/43] hideEmptyRules doesn't require countMatched to be true --- doc/en/user/source/services/wms/get_legend_graphic/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/user/source/services/wms/get_legend_graphic/index.rst b/doc/en/user/source/services/wms/get_legend_graphic/index.rst index 11cbaec560f..ee73d5cacf2 100644 --- a/doc/en/user/source/services/wms/get_legend_graphic/index.rst +++ b/doc/en/user/source/services/wms/get_legend_graphic/index.rst @@ -238,7 +238,7 @@ In order to support it the GetLegendGraphic call needs the following extra param and the following LEGEND_OPTIONS parameters: * countMatched: adds the number of features matching the particular rule at the end of the rule label (requires visible labels to work). Applicable only to vector layers. - * hideEmptyRules: hides rules that are not matching any feature. Applicable only if countMatched is true. + * hideEmptyRules: hides rules that are not matching any feature. Other parameters can also be added to better match the GetMap request, for example, it is recommended to mirror filtering vendor parameters such as, for example, CQL_FILTER,FILTER,FEATUREID,TIME,ELEVATION. From 5974933b4c1aba17ac74461a521ee9a105a5c4fd Mon Sep 17 00:00:00 2001 From: Andrea Aime Date: Thu, 7 Nov 2024 19:07:44 +0100 Subject: [PATCH 27/43] Re-align the Features - Part 3, Filtering implementation with the finalized standard --- .../geoserver/ogcapi/ConformanceClass.java | 9 +- .../geoserver/ogcapi/FunctionsDocument.java | 13 +- .../java/org/geoserver/ogcapi/Queryables.java | 4 +- .../geoserver/ogcapi/QueryablesBuilder.java | 75 +++++----- .../org/geoserver/ogcapi/queryables.ftl | 2 +- .../ogcapi/QueryablesBuilderTest.java | 46 +++--- src/community/ogcapi/ogcapi-features/pom.xml | 6 + .../ogcapi/v1/features/FeatureService.java | 4 +- .../v1/features/FeaturesAPIBuilder.java | 1 + .../ogcapi/v1/features/functions.ftl | 6 +- .../ogcapi/v1/features/landingPage.ftl | 10 ++ .../geoserver/ogcapi/v1/features/openapi.yaml | 8 +- .../geoserver/ogcapi/v1/features/ApiTest.java | 12 ++ .../ogcapi/v1/features/CollectionTest.java | 45 ------ .../ogcapi/v1/features/CollectionsTest.java | 10 -- .../ogcapi/v1/features/ConformanceTest.java | 28 ++-- .../ogcapi/v1/features/FeatureTest.java | 132 ++++++++++-------- .../ogcapi/v1/features/FunctionsTest.java | 53 ++++++- .../ogcapi/v1/features/LandingPageTest.java | 20 +-- .../ogcapi/v1/features/QueryablesTest.java | 93 ++++++++++++ .../ogcapi/v1/features/functions-schema.yml | 52 +++++++ .../ogcapi/v1/tiles/QueryablesTest.java | 6 +- .../ogcapi/v1/stac/STACQueryablesBuilder.java | 5 +- .../ogcapi/v1/stac/queryables-common.ftl | 2 +- .../ogcapi/v1/stac/sortables-common.ftl | 2 +- .../org/geoserver/ogcapi/v1/stac/ApiTest.java | 2 +- .../ogcapi/v1/stac/QueryablesTest.java | 20 +-- .../ogcapi/v1/stac/SortablesTest.java | 2 +- src/pom.xml | 6 + 29 files changed, 436 insertions(+), 238 deletions(-) create mode 100644 src/community/ogcapi/ogcapi-features/src/test/java/org/geoserver/ogcapi/v1/features/QueryablesTest.java create mode 100644 src/community/ogcapi/ogcapi-features/src/test/resources/org/geoserver/ogcapi/v1/features/functions-schema.yml diff --git a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/ConformanceClass.java b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/ConformanceClass.java index cccb8e85961..138cb1976d9 100644 --- a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/ConformanceClass.java +++ b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/ConformanceClass.java @@ -20,11 +20,14 @@ public class ConformanceClass { * CQL filtering conformance classes, shared here to allow STAC using them, without depending on * ogc-api-features directly */ - public static final String FEATURES_FILTER = - "http://www.opengis.net/spec/ogcapi-features-3/1.0/req/features-filter"; + public static final String QUERYABLES = + "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/queryables"; public static final String FILTER = - "http://www.opengis.net/spec/ogcapi-features-3/1.0/req/filter"; + "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/filter"; + + public static final String FEATURES_FILTER = + "http://www.opengis.net/spec/ogcapi-features-3/1.0/conf/features-filter"; /** Sorting conformance class from OGC API - Records. */ public static final String SORTBY = diff --git a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/FunctionsDocument.java b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/FunctionsDocument.java index ff7371eff88..9dbc23477fb 100644 --- a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/FunctionsDocument.java +++ b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/FunctionsDocument.java @@ -5,6 +5,7 @@ package org.geoserver.ogcapi; import com.fasterxml.jackson.annotation.JsonInclude; +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Optional; @@ -16,7 +17,7 @@ /** A document enumerating the available functions */ @JsonInclude(JsonInclude.Include.NON_NULL) -public class FunctionsDocument { +public class FunctionsDocument extends AbstractDocument { public static final String REL = "http://www.opengis.net/def/rel/ogc/1.0/functions"; @@ -49,12 +50,14 @@ public AttributeType[] getType() { public static class Function { String name; String description; - Argument returns; + List returns; List arguments; Function(FunctionName fn) { this.name = fn.getName(); - this.returns = toParameter(fn.getReturn()); + this.returns = + Arrays.stream(toParameter(fn.getReturn()).getType()) + .collect(Collectors.toList()); this.description = null; // no support for descriptions in GeoTools functions this.arguments = fn.getArguments().stream() @@ -66,7 +69,7 @@ public String getName() { return name; } - public Argument getReturns() { + public List getReturns() { return returns; } @@ -92,6 +95,8 @@ public FunctionsDocument() { .map(Function::new) .distinct() .collect(Collectors.toList()); + + addSelfLinks("ogc/features/v1/functions"); } private static boolean isSimpleFunction(FunctionName functionName) { diff --git a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/Queryables.java b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/Queryables.java index dd68590d18a..930f7ca5614 100644 --- a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/Queryables.java +++ b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/Queryables.java @@ -19,8 +19,10 @@ public class Queryables extends Schema { public static final String REL = "http://www.opengis.net/def/rel/ogc/1.0/queryables"; + public static final String JSON_SCHEMA_DRAFT_2020_12 = + "https://json-schema.org/draft/2020-12/schema"; - private final String schema = "https://json-schema.org/draft/2019-09/schema"; + private final String schema = JSON_SCHEMA_DRAFT_2020_12; private String id; diff --git a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/QueryablesBuilder.java b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/QueryablesBuilder.java index c5a0322f1f8..318c65efc49 100644 --- a/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/QueryablesBuilder.java +++ b/src/community/ogcapi/ogcapi-core/src/main/java/org/geoserver/ogcapi/QueryablesBuilder.java @@ -8,13 +8,16 @@ import io.swagger.v3.oas.models.media.Schema; import java.io.IOException; +import java.net.URL; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; import org.geoserver.catalog.FeatureTypeInfo; import org.geotools.api.feature.type.FeatureType; -import org.geotools.api.feature.type.PropertyType; +import org.geotools.api.feature.type.PropertyDescriptor; +import org.geotools.feature.FeatureTypes; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.LineString; import org.locationtech.jts.geom.MultiLineString; @@ -25,23 +28,13 @@ public class QueryablesBuilder { - public static final String POINT_SCHEMA_REF = "https://geojson.org/schema/Point.json"; - public static final String MULTIPOINT_SCHEMA_REF = "https://geojson.org/schema/MultiPoint.json"; - public static final String LINESTRING_SCHEMA_REF = "https://geojson.org/schema/LineString.json"; - public static final String MULTILINESTRING_SCHEMA_REF = - "https://geojson.org/schema/MultiLineString.json"; - public static final String POLYGON_SCHEMA_REF = "https://geojson.org/schema/Polygon.json"; - public static final String MULTIPOLYGON_SCHEMA_REF = - "https://geojson.org/schema/MultiPolygon.json"; - public static final String GEOMETRY_SCHEMA_REF = "https://geojson.org/schema/Geometry.json"; - public static final String POINT = "Point"; public static final String MULTIPOINT = "MultiPoint"; public static final String LINESTRING = "LineString"; public static final String MULTILINESTRING = "MultiLineString"; public static final String POLYGON = "Polygon"; public static final String MULTIPOLYGON = "MultiPolygon"; - public static final String GENERIC_GEOMETRY = "Generic geometry"; + public static final String GENERIC_GEOMETRY = "Any geometry"; public static final String DATE = "Date"; public static final String DATE_TIME = "DateTime"; public static final String TIME = "Time"; @@ -68,7 +61,7 @@ public QueryablesBuilder forType(FeatureType ft) { .collect( Collectors.toMap( ad -> ad.getName().getLocalPart(), - ad -> getSchema(ad.getType()), + ad -> getSchema(ad), (u, v) -> { throw new IllegalStateException( String.format("Duplicate key %s", u)); @@ -78,9 +71,14 @@ public QueryablesBuilder forType(FeatureType ft) { return this; } - private Schema getSchema(PropertyType type) { - Class binding = type.getBinding(); - return getSchema(binding); + private Schema getSchema(PropertyDescriptor descriptor) { + Class binding = descriptor.getType().getBinding(); + Schema schema = getSchema(binding); + int fieldLength = FeatureTypes.getFieldLength(descriptor); + if (fieldLength != FeatureTypes.ANY_LENGTH) { + schema.setMaxLength(fieldLength); + } + return schema; } /** Returns the schema for a given data type */ @@ -91,33 +89,30 @@ public static Schema getSchema(Class binding) { private static Schema getGeometrySchema(Class binding) { Schema schema = new Schema<>(); - String ref; - String description; + String title; if (Point.class.isAssignableFrom(binding)) { - ref = POINT_SCHEMA_REF; - description = POINT; + title = POINT; } else if (MultiPoint.class.isAssignableFrom(binding)) { - ref = MULTIPOINT_SCHEMA_REF; - description = MULTIPOINT; + title = MULTIPOINT; } else if (LineString.class.isAssignableFrom(binding)) { - ref = LINESTRING_SCHEMA_REF; - description = LINESTRING; + title = LINESTRING; } else if (MultiLineString.class.isAssignableFrom(binding)) { - ref = MULTILINESTRING_SCHEMA_REF; - description = MULTILINESTRING; + title = MULTILINESTRING; } else if (Polygon.class.isAssignableFrom(binding)) { - ref = POLYGON_SCHEMA_REF; - description = POLYGON; + title = POLYGON; } else if (MultiPolygon.class.isAssignableFrom(binding)) { - ref = MULTIPOLYGON_SCHEMA_REF; - description = MULTIPOLYGON; + title = MULTIPOLYGON; + } else { + title = GENERIC_GEOMETRY; + } + + schema.setTitle(title); + if (title.equals(GENERIC_GEOMETRY)) { + schema.setFormat("geometry-any"); } else { - ref = GEOMETRY_SCHEMA_REF; - description = GENERIC_GEOMETRY; + schema.setFormat("geometry-" + title.toLowerCase()); } - schema.set$ref(ref); - schema.setDescription(description); return schema; } @@ -131,16 +126,20 @@ public static Schema getAlphanumericSchema(Class binding) { Schema schema = new Schema<>(); schema.setType(org.geoserver.ogcapi.AttributeType.fromClass(binding).getType()); - schema.setDescription(schema.getType()); + schema.setTitle(schema.getType()); if (java.sql.Date.class.isAssignableFrom(binding)) { schema.setFormat("date"); - schema.setDescription(DATE); + schema.setTitle(DATE); } else if (java.sql.Time.class.isAssignableFrom(binding)) { schema.setFormat("time"); - schema.setDescription(TIME); + schema.setTitle(TIME); } else if (java.util.Date.class.isAssignableFrom(binding)) { schema.setFormat("date-time"); - schema.setDescription(DATE_TIME); + schema.setTitle(DATE_TIME); + } else if (UUID.class.isAssignableFrom(binding)) { + schema.setFormat("uuid"); + } else if (URL.class.isAssignableFrom(binding)) { + schema.setTitle("uri"); } return schema; } diff --git a/src/community/ogcapi/ogcapi-core/src/main/resources/org/geoserver/ogcapi/queryables.ftl b/src/community/ogcapi/ogcapi-core/src/main/resources/org/geoserver/ogcapi/queryables.ftl index 280c2b4ff6a..305e9372e46 100644 --- a/src/community/ogcapi/ogcapi-core/src/main/resources/org/geoserver/ogcapi/queryables.ftl +++ b/src/community/ogcapi/ogcapi-core/src/main/resources/org/geoserver/ogcapi/queryables.ftl @@ -13,7 +13,7 @@ <#if model.getProperties()??>
    <#list model.getProperties() as name, definition> -
  • ${name}: ${definition.getDescription()}
  • +
  • ${name}: ${definition.getTitle()}
<#else> diff --git a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/QueryablesBuilderTest.java b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/QueryablesBuilderTest.java index 2eba737d079..95beb377d6c 100644 --- a/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/QueryablesBuilderTest.java +++ b/src/community/ogcapi/ogcapi-core/src/test/java/org/geoserver/ogcapi/QueryablesBuilderTest.java @@ -22,69 +22,69 @@ public class QueryablesBuilderTest { public void testGetSchema() throws Exception { Schema stringSchema = QueryablesBuilder.getSchema(String.class); assertEquals("string", stringSchema.getType()); - assertEquals("string", stringSchema.getDescription()); + assertEquals("string", stringSchema.getTitle()); Schema integerSchema = QueryablesBuilder.getSchema(Integer.class); assertEquals("integer", integerSchema.getType()); - assertEquals("integer", integerSchema.getDescription()); + assertEquals("integer", integerSchema.getTitle()); Schema longSchema = QueryablesBuilder.getSchema(Long.class); assertEquals("integer", longSchema.getType()); - assertEquals("integer", longSchema.getDescription()); + assertEquals("integer", longSchema.getTitle()); Schema doubleSchema = QueryablesBuilder.getSchema(Double.class); assertEquals("number", doubleSchema.getType()); - assertEquals("number", doubleSchema.getDescription()); + assertEquals("number", doubleSchema.getTitle()); Schema floatSchema = QueryablesBuilder.getSchema(Float.class); assertEquals("number", floatSchema.getType()); - assertEquals("number", floatSchema.getDescription()); + assertEquals("number", floatSchema.getTitle()); Schema booleanSchema = QueryablesBuilder.getSchema(Boolean.class); assertEquals("boolean", booleanSchema.getType()); - assertEquals("boolean", booleanSchema.getDescription()); + assertEquals("boolean", booleanSchema.getTitle()); Schema timeSchema = QueryablesBuilder.getSchema(java.sql.Time.class); assertEquals("string", timeSchema.getType()); assertEquals("time", timeSchema.getFormat()); - assertEquals("Time", timeSchema.getDescription()); + assertEquals("Time", timeSchema.getTitle()); Schema dateSChema = QueryablesBuilder.getSchema(java.sql.Date.class); assertEquals("string", dateSChema.getType()); assertEquals("date", dateSChema.getFormat()); - assertEquals("Date", dateSChema.getDescription()); + assertEquals("Date", dateSChema.getTitle()); Schema dateTimeSchema = QueryablesBuilder.getSchema(java.util.Date.class); assertEquals("string", dateTimeSchema.getType()); assertEquals("date-time", dateTimeSchema.getFormat()); - assertEquals("DateTime", dateTimeSchema.getDescription()); + assertEquals("DateTime", dateTimeSchema.getTitle()); Schema pointSchema = QueryablesBuilder.getSchema(Point.class); - assertEquals(QueryablesBuilder.POINT_SCHEMA_REF, pointSchema.get$ref()); - assertEquals("Point", pointSchema.getDescription()); + assertEquals("geometry-point", pointSchema.getFormat()); + assertEquals("Point", pointSchema.getTitle()); Schema multiPointSchema = QueryablesBuilder.getSchema(MultiPoint.class); - assertEquals(QueryablesBuilder.MULTIPOINT_SCHEMA_REF, multiPointSchema.get$ref()); - assertEquals("MultiPoint", multiPointSchema.getDescription()); + assertEquals("geometry-multipoint", multiPointSchema.getFormat()); + assertEquals("MultiPoint", multiPointSchema.getTitle()); Schema lineStringSchema = QueryablesBuilder.getSchema(LineString.class); - assertEquals(QueryablesBuilder.LINESTRING_SCHEMA_REF, lineStringSchema.get$ref()); - assertEquals("LineString", lineStringSchema.getDescription()); + assertEquals("geometry-linestring", lineStringSchema.getFormat()); + assertEquals("LineString", lineStringSchema.getTitle()); Schema multiLineStringSchema = QueryablesBuilder.getSchema(MultiLineString.class); - assertEquals(QueryablesBuilder.MULTILINESTRING_SCHEMA_REF, multiLineStringSchema.get$ref()); - assertEquals("MultiLineString", multiLineStringSchema.getDescription()); + assertEquals("geometry-multilinestring", multiLineStringSchema.getFormat()); + assertEquals("MultiLineString", multiLineStringSchema.getTitle()); Schema polygonSchema = QueryablesBuilder.getSchema(Polygon.class); - assertEquals(QueryablesBuilder.POLYGON_SCHEMA_REF, polygonSchema.get$ref()); - assertEquals("Polygon", polygonSchema.getDescription()); + assertEquals("geometry-polygon", polygonSchema.getFormat()); + assertEquals("Polygon", polygonSchema.getTitle()); Schema multiPolygonSchema = QueryablesBuilder.getSchema(MultiPolygon.class); - assertEquals(QueryablesBuilder.MULTIPOLYGON_SCHEMA_REF, multiPolygonSchema.get$ref()); - assertEquals("MultiPolygon", multiPolygonSchema.getDescription()); + assertEquals("geometry-multipolygon", multiPolygonSchema.getFormat()); + assertEquals("MultiPolygon", multiPolygonSchema.getTitle()); Schema geometrySchema = QueryablesBuilder.getSchema(Geometry.class); - assertEquals(QueryablesBuilder.GEOMETRY_SCHEMA_REF, geometrySchema.get$ref()); - assertEquals("Generic geometry", geometrySchema.getDescription()); + assertEquals("geometry-any", geometrySchema.getFormat()); + assertEquals("Any geometry", geometrySchema.getTitle()); } } diff --git a/src/community/ogcapi/ogcapi-features/pom.xml b/src/community/ogcapi/ogcapi-features/pom.xml index 0cf0e21fe32..4c3326b4a98 100644 --- a/src/community/ogcapi/ogcapi-features/pom.xml +++ b/src/community/ogcapi/ogcapi-features/pom.xml @@ -89,5 +89,11 @@ tests test + + com.github.erosb + json-sKema + test + + diff --git a/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/FeatureService.java b/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/FeatureService.java index 8214764f6df..bf9ea45cac3 100644 --- a/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/FeatureService.java +++ b/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/FeatureService.java @@ -17,6 +17,7 @@ import static org.geoserver.ogcapi.ConformanceClass.FEATURES_FILTER; import static org.geoserver.ogcapi.ConformanceClass.FILTER; import static org.geoserver.ogcapi.ConformanceClass.IDS; +import static org.geoserver.ogcapi.ConformanceClass.QUERYABLES; import static org.geoserver.ogcapi.ConformanceClass.SEARCH; import static org.geoserver.ogcapi.ConformanceClass.SORTBY; import static org.geoserver.ogcapi.MappingJackson2YAMLMessageConverter.APPLICATION_YAML_VALUE; @@ -318,8 +319,9 @@ public ConformanceDocument conformance() { GEOJSON, /* GMLSF0, GS does not use the gmlsf namespace */ CRS_BY_REFERENCE, - FEATURES_FILTER, FILTER, + QUERYABLES, + FEATURES_FILTER, SEARCH, ECQL, ECQL_TEXT, diff --git a/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/FeaturesAPIBuilder.java b/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/FeaturesAPIBuilder.java index 218b1d8f88b..4da220bb649 100644 --- a/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/FeaturesAPIBuilder.java +++ b/src/community/ogcapi/ogcapi-features/src/main/java/org/geoserver/ogcapi/v1/features/FeaturesAPIBuilder.java @@ -61,6 +61,7 @@ public OpenAPI build(WFSInfo wfs) throws IOException { Catalog catalog = wfs.getGeoServer().getCatalog(); List validCollectionIds = catalog.getFeatureTypes().stream() + .filter(ft -> ft.isEnabled() && ft.isAdvertised()) .map(ft -> ft.prefixedName()) .collect(Collectors.toList()); collectionId.getSchema().setEnum(validCollectionIds); diff --git a/src/community/ogcapi/ogcapi-features/src/main/resources/org/geoserver/ogcapi/v1/features/functions.ftl b/src/community/ogcapi/ogcapi-features/src/main/resources/org/geoserver/ogcapi/v1/features/functions.ftl index b405d98b7c4..b02fbeb247d 100644 --- a/src/community/ogcapi/ogcapi-features/src/main/resources/org/geoserver/ogcapi/v1/features/functions.ftl +++ b/src/community/ogcapi/ogcapi-features/src/main/resources/org/geoserver/ogcapi/v1/features/functions.ftl @@ -7,12 +7,12 @@ <#list model.functions as f>

${f.name}