diff --git a/src/apps/geoserver/gwc/pom.xml b/src/apps/geoserver/gwc/pom.xml index 53eca065a..50b25f864 100644 --- a/src/apps/geoserver/gwc/pom.xml +++ b/src/apps/geoserver/gwc/pom.xml @@ -49,6 +49,19 @@ spring-boot-starter-test test + + org.geoserver.cloud.catalog + gs-cloud-catalog-plugin + ${project.version} + test-jar + test + + + * + * + + + diff --git a/src/apps/geoserver/gwc/src/test/java/org/geoserver/cloud/gwc/app/GeoWebCacheApplicationTest.java b/src/apps/geoserver/gwc/src/test/java/org/geoserver/cloud/gwc/app/GeoWebCacheApplicationTest.java index 0039127cd..4ea9b6ca7 100644 --- a/src/apps/geoserver/gwc/src/test/java/org/geoserver/cloud/gwc/app/GeoWebCacheApplicationTest.java +++ b/src/apps/geoserver/gwc/src/test/java/org/geoserver/cloud/gwc/app/GeoWebCacheApplicationTest.java @@ -5,22 +5,33 @@ package org.geoserver.cloud.gwc.app; import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.http.MediaType.APPLICATION_XML; +import static org.springframework.http.MediaType.*; import com.google.gson.JsonElement; import com.google.gson.JsonParser; +import org.geoserver.catalog.GeoServerCatalogTestData; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") @@ -28,6 +39,23 @@ class GeoWebCacheApplicationTest { @Autowired private TestRestTemplate restTemplate; + @Autowired private ApplicationContext context; + + @TempDir private static Path tmpPath; + + @BeforeAll + static void initializeDatadir() throws URISyntaxException, IOException { + assertThat(tmpPath).isNotNull(); + GeoServerCatalogTestData.unzipGeoserverCatalogTestData(tmpPath); + } + + @DynamicPropertySource + static void dynamicProperties(DynamicPropertyRegistry registry) + throws URISyntaxException, IOException { + File tmpFilePath = tmpPath.toFile(); + registry.add("geoserver.backend.data-directory.location", tmpFilePath::toString); + } + @BeforeEach void before() { restTemplate = restTemplate.withBasicAuth("admin", "geoserver"); @@ -53,10 +81,73 @@ void testRESTPathExtensionContentNegotiation() { testGetRequestContentType("/gwc/rest/layers.xml", APPLICATION_XML); } + @Test + void testGwcUrlHandlerMappingArePresentInTheClasspathAndLocalWorkspaceUrlsAreHandled() { + assertThat(context.isTypeMatch("gwcDemoUrlHandlerMapping", WebMvcRegistrations.class)) + .as("expected a bean gwcDemoUrlHandlerMapping of type GwcUrlHandlerMapping") + .isTrue(); + assertThat(context.isTypeMatch("gwcRestWebUrlHandlerMapping", WebMvcRegistrations.class)) + .as("expected a bean gwcRestWebUrlHandlerMapping of type GwcUrlHandlerMapping") + .isTrue(); + + ResponseEntity response = + restTemplate.getForEntity("/gwc/demo/ne:world", String.class); + + assertThat(response.getStatusCode()) + .as( + String.format( + "expected a HTTP return code 200, got %s", + response.getStatusCode())) + .isEqualTo(HttpStatus.OK); + + assertThat(response.getHeaders().getContentType()).isNotEqualTo(APPLICATION_XML); + } + + @Test + void testGeneralGwcUrlsAreWorking() { + ResponseEntity response = testGetRequestContentType("/gwc/home", TEXT_HTML); + assertThat(response.getHeaders().getContentType()).isNotEqualTo(APPLICATION_XML); + assertThat(response.getBody()).doesNotContain("ows:Exception"); + + response = testGetRequestContentType("/gwc/demo", TEXT_HTML); + assertThat(response.getHeaders().getContentType()).isNotEqualTo(APPLICATION_XML); + assertThat(response.getBody()).doesNotContain("ows:Exception"); + + response = testGetRequestContentType("/gwc/demo/ne:boundary_lines", TEXT_HTML); + assertThat(response.getHeaders().getContentType()).isNotEqualTo(APPLICATION_XML); + assertThat(response.getBody()).doesNotContain("ows:Exception"); + + response = testGetRequestContentType("/gwc/service/wmts?REQUEST=GetCapabilities", TEXT_XML); + assertThat(response.getBody()).doesNotContain("ows:Exception"); + } + + @Test + void testGwcNamespacePrefixedUrls() { + ResponseEntity response = testGetRequestContentType("/ne/gwc/home", TEXT_HTML); + assertThat(response.getHeaders().getContentType()).isNotEqualTo(APPLICATION_XML); + assertThat(response.getBody()).doesNotContain("ows:Exception"); + + response = testGetRequestContentType("/ne/gwc/demo", TEXT_HTML); + assertThat(response.getHeaders().getContentType()).isNotEqualTo(APPLICATION_XML); + assertThat(response.getBody()).doesNotContain("ows:Exception"); + + response = testGetRequestContentType("/ne/gwc/demo/boundary_lines", TEXT_HTML); + assertThat(response.getHeaders().getContentType()).isNotEqualTo(APPLICATION_XML); + assertThat(response.getBody()).doesNotContain("ows:Exception"); + + response = testGetRequestContentType("/ne/gwc/demo/ne:boundary_lines", TEXT_HTML); + assertThat(response.getHeaders().getContentType()).isNotEqualTo(APPLICATION_XML); + assertThat(response.getBody()).doesNotContain("ows:Exception"); + + response = + testGetRequestContentType("/ne/gwc/service/wmts?REQUEST=GetCapabilities", TEXT_XML); + assertThat(response.getBody()).doesNotContain("ows:Exception"); + } + protected ResponseEntity testGetRequestContentType(String uri, MediaType expected) { ResponseEntity response = restTemplate.getForEntity(uri, String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getHeaders().getContentType()).isEqualTo(expected); + assertThat(response.getHeaders().getContentType().isCompatibleWith(expected)).isTrue(); return response; } } diff --git a/src/catalog/plugin/pom.xml b/src/catalog/plugin/pom.xml index 483154e44..1d0bf8735 100644 --- a/src/catalog/plugin/pom.xml +++ b/src/catalog/plugin/pom.xml @@ -116,6 +116,11 @@ org.geotools gt-process-feature + + org.geotools + gt-sample-data + ${gt.version} + com.github.f4b6a3 ulid-creator diff --git a/src/catalog/plugin/src/test/java/org/geoserver/catalog/GeoServerCatalogTestData.java b/src/catalog/plugin/src/test/java/org/geoserver/catalog/GeoServerCatalogTestData.java new file mode 100644 index 000000000..27d511fef --- /dev/null +++ b/src/catalog/plugin/src/test/java/org/geoserver/catalog/GeoServerCatalogTestData.java @@ -0,0 +1,51 @@ +/* + * (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.catalog; + +import lombok.experimental.UtilityClass; + +import org.apache.commons.io.FileUtils; +import org.geotools.test.TestData; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.file.Path; + +/** + * The purpose of this class is to provide a geoserver datadir which is not fake (it is actually a + * copy of the one from GeoServer upstream), which could be used for testing. + * + *

It copies a zip file into a directory - generally a temporary one - an unzip it, so that it is + * ready for use. + */ +@UtilityClass +public class GeoServerCatalogTestData { + + /** + * This method copies the zipped datadir into the Path object given as argument and unzip it at + * the same place. + * + *

It is the caller's responsability to clean up when the datadir is not needed anymore. + * + *

Note: we have to copy the resource into the directory first, because the method from + * GeoTools which is being used does not support zip URIs nested into jar files. + * + * @param tmpPath the temporary path where the datadir has to be unzipped to. + * @throws URISyntaxException + * @throws IOException + */ + public static void unzipGeoserverCatalogTestData(Path tmpPath) + throws URISyntaxException, IOException { + InputStream zippedDatadir = + GeoServerCatalogTestData.class.getResourceAsStream("/test-data-directory.zip"); + File tmpDir = tmpPath.toFile(); + File destFile = new File(tmpDir, "test-data-directory.zip"); + FileUtils.copyToFile(zippedDatadir, destFile); + TestData.unzip(destFile, tmpDir); + destFile.delete(); + } +} diff --git a/src/catalog/plugin/src/test/resources/test-data-directory.zip b/src/catalog/plugin/src/test/resources/test-data-directory.zip new file mode 100644 index 000000000..faf9dafed Binary files /dev/null and b/src/catalog/plugin/src/test/resources/test-data-directory.zip differ diff --git a/src/gwc/autoconfigure/src/main/java/org/geoserver/cloud/autoconfigure/web/gwc/GeoWebCacheUIAutoConfiguration.java b/src/gwc/autoconfigure/src/main/java/org/geoserver/cloud/autoconfigure/web/gwc/GeoWebCacheUIAutoConfiguration.java index 39f5aee81..e090844d6 100644 --- a/src/gwc/autoconfigure/src/main/java/org/geoserver/cloud/autoconfigure/web/gwc/GeoWebCacheUIAutoConfiguration.java +++ b/src/gwc/autoconfigure/src/main/java/org/geoserver/cloud/autoconfigure/web/gwc/GeoWebCacheUIAutoConfiguration.java @@ -6,15 +6,21 @@ import lombok.extern.slf4j.Slf4j; -import org.geoserver.cloud.autoconfigure.gwc.ConditionalOnGeoWebCacheRestConfigEnabled; +import org.geoserver.catalog.Catalog; import org.geoserver.cloud.autoconfigure.gwc.ConditionalOnWebUIEnabled; import org.geoserver.cloud.gwc.config.core.GeoWebCacheConfigurationProperties; +import org.geoserver.cloud.virtualservice.VirtualServiceVerifier; +import org.geoserver.gwc.controller.GwcUrlHandlerMapping; +import org.geoserver.ows.Dispatcher; import org.geowebcache.GeoWebCacheDispatcher; import org.geowebcache.rest.controller.ByteStreamController; import org.gwc.web.rest.GeoWebCacheController; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import javax.annotation.PostConstruct; @@ -28,17 +34,63 @@ public class GeoWebCacheUIAutoConfiguration { } @Bean - GeoWebCacheController gwcController(GeoWebCacheDispatcher gwcDispatcher) { - return new GeoWebCacheController(gwcDispatcher); + GeoWebCacheController gwcController( + Dispatcher geoserverDispatcher, + GeoWebCacheDispatcher geoWebCacheDispatcher, + VirtualServiceVerifier verifier) { + return new GeoWebCacheController(geoserverDispatcher, geoWebCacheDispatcher, verifier); } - /** - * Provide a handler for static web resources if missing, for example, because {@link - * ConditionalOnGeoWebCacheRestConfigEnabled} is disabled - */ + /** ConditionalOnGeoWebCacheRestConfigEnabled} is disabled */ @Bean @ConditionalOnMissingBean(ByteStreamController.class) ByteStreamController byteStreamController() { return new ByteStreamController(); } + + @Bean + VirtualServiceVerifier virtualServiceVerifier(@Qualifier("rawCatalog") Catalog catalog) { + return new VirtualServiceVerifier(catalog); + } + + /** + * GS's src/web/gwc/src/main/java/applicationContext.xml + * + * + * + * + * + * + * + */ + @Bean + WebMvcRegistrations gwcDemoUrlHandlerMapping(@Qualifier("rawCatalog") Catalog catalog) { + GwcUrlHandlerMapping handler = new GwcUrlHandlerMapping(catalog, "/gwc/demo"); + handler.setAlwaysUseFullPath(true); + handler.setOrder(10); + + return new WebMvcRegistrations() { + @Override + public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { + return handler; + } + }; + } + + @Bean + WebMvcRegistrations gwcRestWebUrlHandlerMapping(@Qualifier("rawCatalog") Catalog catalog) { + GwcUrlHandlerMapping handler = new GwcUrlHandlerMapping(catalog, "/gwc/rest/web"); + handler.setAlwaysUseFullPath(true); + handler.setOrder(10); + + return new WebMvcRegistrations() { + @Override + public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { + return handler; + } + }; + } } diff --git a/src/gwc/autoconfigure/src/test/java/org/geoserver/cloud/autoconfigure/gwc/web/gwc/GeoWebCacheUIAutoConfigurationTest.java b/src/gwc/autoconfigure/src/test/java/org/geoserver/cloud/autoconfigure/gwc/web/gwc/GeoWebCacheUIAutoConfigurationTest.java new file mode 100644 index 000000000..95e48578e --- /dev/null +++ b/src/gwc/autoconfigure/src/test/java/org/geoserver/cloud/autoconfigure/gwc/web/gwc/GeoWebCacheUIAutoConfigurationTest.java @@ -0,0 +1,42 @@ +/* + * (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.cloud.autoconfigure.gwc.web.gwc; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.geoserver.cloud.autoconfigure.gwc.GeoWebCacheContextRunner; +import org.geoserver.cloud.autoconfigure.web.gwc.GeoWebCacheUIAutoConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; + +import java.io.File; + +public class GeoWebCacheUIAutoConfigurationTest { + + WebApplicationContextRunner runner; + + @TempDir File tmpDir; + + @BeforeEach + void setUp() throws Exception { + runner = + GeoWebCacheContextRunner.newMinimalGeoWebCacheContextRunner(tmpDir) + .withPropertyValues("gwc.web-ui=true") + .withConfiguration( + AutoConfigurations.of(GeoWebCacheUIAutoConfiguration.class)); + } + + @Test + void beansForLocalWorkspacePathsHandlingArePresent() { + runner.run( + context -> { + assertNotNull(context.getBean("gwcDemoUrlHandlerMapping")); + assertNotNull(context.getBean("gwcRestWebUrlHandlerMapping")); + }); + } +} diff --git a/src/gwc/services/pom.xml b/src/gwc/services/pom.xml index 408746cfb..27dff27bc 100644 --- a/src/gwc/services/pom.xml +++ b/src/gwc/services/pom.xml @@ -21,5 +21,9 @@ org.geoserver gs-gwc-rest + + org.geoserver.cloud + gs-cloud-starter-webmvc + diff --git a/src/gwc/services/src/main/java/org/gwc/web/rest/GeoWebCacheController.java b/src/gwc/services/src/main/java/org/gwc/web/rest/GeoWebCacheController.java index 021920b80..10926a264 100644 --- a/src/gwc/services/src/main/java/org/gwc/web/rest/GeoWebCacheController.java +++ b/src/gwc/services/src/main/java/org/gwc/web/rest/GeoWebCacheController.java @@ -7,12 +7,14 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; +import org.geoserver.cloud.virtualservice.VirtualServiceVerifier; import org.geoserver.gwc.dispatch.GeoServerGWCDispatcherController; +import org.geoserver.ows.Dispatcher; import org.geowebcache.GeoWebCacheDispatcher; import org.geowebcache.controller.GeoWebCacheDispatcherController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.PathVariable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -25,21 +27,34 @@ *

Copied from {@link GeoServerGWCDispatcherController} */ @Controller -@RequestMapping("/gwc") @RequiredArgsConstructor public class GeoWebCacheController { - private final @NonNull GeoWebCacheDispatcher gwcDispatcher; + private final @NonNull Dispatcher geoserverDispatcher; + + private final @NonNull GeoWebCacheDispatcher geoWebCacheDispatcher; + + private final @NonNull VirtualServiceVerifier virtualServiceVerifier; + + @GetMapping(path = {"/gwc", "/gwc/home", "/gwc/demo/**", "/gwc/proxy/**"}) + public void handleGet(HttpServletRequest request, HttpServletResponse response) + throws Exception { + geoWebCacheDispatcher.handleRequest(request, response); + } @GetMapping( path = { - "", - "/home", - "/demo/**", - "/proxy/**", + "/{namespace}/gwc", + "/{namespace}/gwc/home", + "/{namespace}/gwc/demo/**", + "/{namespace}/gwc/proxy/**" }) - public void handleGet(HttpServletRequest request, HttpServletResponse response) + public void handlePrefixedNamespaceGet( + @PathVariable String namespace, + HttpServletRequest request, + HttpServletResponse response) throws Exception { - gwcDispatcher.handleRequest(request, response); + virtualServiceVerifier.checkVirtualService(namespace); + geoserverDispatcher.handleRequest(request, response); } }