Dockerized GeoServer micro-services
This guide outlines the process for adding new extensions to the GeoServer Cloud ecosystem. Extensions allow for modularity and flexibility in the codebase, enabling features to be optionally included based on application requirements.
GeoServer Cloud organizes extensions in a modular structure:
src/extensions/<category>/<extension-name>
src/starters/<category>
First, create the appropriate directory structure for your extension:
src/extensions/
└── <category>/
└── <extension-name>/
├── pom.xml
└── src/
├── main/
│ ├── java/
│ │ └── org/geoserver/cloud/autoconfigure/extensions/...
│ └── resources/
│ └── META-INF/
│ └── spring.factories (or spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)
└── test/
├── java/
│ └── org/geoserver/cloud/autoconfigure/extensions/...
└── resources/
Where:
<category>
is the functional category (e.g., security
, input-formats
, output-formats
, etc.)<extension-name>
is the specific extension nameCreate a pom.xml
file for your extension with the appropriate dependencies:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.geoserver.cloud.extensions</groupId>
<artifactId>gs-cloud-extensions-<category></artifactId>
<version>${revision}</version>
</parent>
<artifactId>gs-cloud-extension-<category>-<extension-name></artifactId>
<packaging>jar</packaging>
<description>GeoServer Cloud <extension-name> extension</description>
<dependencies>
<!-- Core extension dependency -->
<dependency>
<groupId>org.geoserver.cloud.extensions</groupId>
<artifactId>gs-cloud-extensions-core</artifactId>
</dependency>
<!-- Extension-specific dependencies -->
<dependency>
<groupId>org.geoserver</groupId>
<artifactId>gs-<extension-related-module></artifactId>
<optional>true</optional>
</dependency>
<!-- Test dependencies are inherited from parent -->
</dependencies>
</project>
Add your extension module to its category’s parent pom.xml
. For example, if adding a security extension, update:
<!-- In src/extensions/security/pom.xml -->
<modules>
<!-- existing modules -->
<module><extension-name></module>
</modules>
Add your extension to the dependency management section in the root pom.xml
:
<dependencyManagement>
<dependencies>
<!-- existing dependencies -->
<dependency>
<groupId>org.geoserver.cloud.extensions</groupId>
<artifactId>gs-cloud-extension-<category>-<extension-name></artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
Create a configuration properties class to enable/disable and configure your extension:
package org.geoserver.cloud.autoconfigure.extensions.<category>.<extensionname>;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = <Extension>ConfigProperties.PREFIX)
public class <Extension>ConfigProperties {
public static final String PREFIX = "geoserver.extension.<category>.<extension-name>";
/** Whether the extension is enabled (default: false) */
public static final boolean DEFAULT = false;
/** Enable/disable the extension */
private boolean enabled = DEFAULT;
/** Additional configuration properties */
private String someProperty;
}
Create a custom conditional annotation to control when your extension’s beans are registered:
package org.geoserver.cloud.autoconfigure.extensions.<category>.<extensionname>;
import java.lang.annotation.*;
import org.geoserver.cloud.autoconfigure.extensions.ConditionalOnGeoServer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Documented
@Inherited
@ConditionalOnGeoServer
@ConditionalOnProperty(
prefix = <Extension>ConfigProperties.PREFIX,
name = "enabled",
havingValue = "true",
matchIfMissing = <Extension>ConfigProperties.DEFAULT)
public @interface ConditionalOn<Extension> {}
Create an auto-configuration class for your extension:
package org.geoserver.cloud.autoconfigure.extensions.<category>.<extensionname>;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@EnableConfigurationProperties(<Extension>ConfigProperties.class)
@ConditionalOn<Extension>
@Slf4j(topic = "org.geoserver.cloud.autoconfigure.extensions.<category>.<extensionname>")
public class <Extension>AutoConfiguration {
private final <Extension>ConfigProperties properties;
public <Extension>AutoConfiguration(<Extension>ConfigProperties properties) {
this.properties = properties;
}
@PostConstruct
void log() {
log.info("GeoServer <Extension> extension enabled");
}
@Bean
public SomeExtensionBean someExtensionBean() {
return new SomeExtensionBean(properties.getSomeProperty());
}
}
Depending on your Spring Boot version:
For Spring Boot 2.x, add to META-INF/spring.factories
:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.geoserver.cloud.autoconfigure.extensions.<category>.<extensionname>.<Extension>AutoConfiguration
For Spring Boot 3.x, add to META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
:
org.geoserver.cloud.autoconfigure.extensions.<category>.<extensionname>.<Extension>AutoConfiguration
Create tests to verify your extension:
package org.geoserver.cloud.autoconfigure.extensions.<category>.<extensionname>;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import org.geoserver.config.GeoServer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
class <Extension>AutoConfigurationTest {
// Create a common test method to verify conditional activation
private void verifyConditionalActivation(
ApplicationContextRunner runner,
String propertyName,
Class<?> componentClass) {
// Test without the property set - condition should not activate
runner.run(context -> {
assertThat(context).doesNotHaveBean(componentClass);
});
// Test with the property set to false - condition should not activate
runner
.withPropertyValues(propertyName + "=false")
.run(context -> {
assertThat(context).doesNotHaveBean(componentClass);
});
// Test with the property set to true - condition should activate
runner
.withPropertyValues(propertyName + "=true")
.run(context -> {
assertThat(context).hasSingleBean(componentClass);
});
}
@Test
void testDisabledByDefault() {
var contextRunner = new ApplicationContextRunner()
.withBean("geoServer", GeoServer.class, () -> mock(GeoServer.class))
.withConfiguration(AutoConfigurations.of(<Extension>AutoConfiguration.class));
contextRunner.run(context -> {
assertThat(context).hasNotFailed();
assertThat(context).doesNotHaveBean(<Extension>AutoConfiguration.class);
assertThat(context).getBean(<Extension>ConfigProperties.class)
.hasFieldOrPropertyWithValue("enabled", false);
});
}
@Test
void testConditionalActivation() {
var contextRunner = new ApplicationContextRunner()
.withBean("geoServer", GeoServer.class, () -> mock(GeoServer.class))
.withConfiguration(AutoConfigurations.of(<Extension>AutoConfiguration.class));
verifyConditionalActivation(
contextRunner,
"geoserver.extension.<category>.<extension-name>.enabled",
<Extension>AutoConfiguration.class);
}
@Test
void testClassCondition() {
// Test with required class filtered out
new ApplicationContextRunner()
.withClassLoader(new FilteredClassLoader(RequiredClass.class))
.withBean("geoServer", GeoServer.class, () -> mock(GeoServer.class))
.withPropertyValues("geoserver.extension.<category>.<extension-name>.enabled=true")
.withConfiguration(AutoConfigurations.of(<Extension>AutoConfiguration.class))
.run(context -> {
// Even with the property set to true, bean should not be created
// when the required class is not available
assertThat(context).doesNotHaveBean(<Extension>AutoConfiguration.class);
});
}
}
Add your extension to the appropriate starter:
<!-- In src/starters/<category>/pom.xml -->
<dependencies>
<!-- existing dependencies -->
<dependency>
<groupId>org.geoserver.cloud.extensions</groupId>
<artifactId>gs-cloud-extension-<category>-<extension-name></artifactId>
</dependency>
</dependencies>
geoserver.yml
Add your extension’s configuration to config/geoserver.yml
:
geoserver:
extension:
<category>:
<extension-name>:
enabled: ${geoserver.<extension-name>.enabled:false}
# Other properties
Create a README.md file with documentation for your extension:
# GeoServer <Extension> Extension
This module integrates <extension functionality> with GeoServer Cloud.
## Overview
<Overview of what the extension does>
## Configuration
The extension is **disabled by default**. To enable it, add the following configuration:
```yaml
geoserver:
extension:
<category>:
<extension-name>:
enabled: true
Here’s a simplified example from adding the GeoServer ACL security extension:
src/extensions/security/geoserver-acl/
@Data
@ConfigurationProperties(prefix = AclConfigProperties.PREFIX)
public class AclConfigProperties {
public static final String PREFIX = "geoserver.extension.security.acl";
public static final String LEGACY_PREFIX = "geoserver.acl";
public static final boolean DEFAULT = false;
private boolean enabled = DEFAULT;
}
@ConditionalOnGeoServer
@ConditionalOnProperty(
prefix = AclConfigProperties.PREFIX,
name = "enabled",
havingValue = "true",
matchIfMissing = AclConfigProperties.DEFAULT)
public @interface ConditionalOnAcl {}
@AutoConfiguration
@EnableConfigurationProperties(AclConfigProperties.class)
@ConditionalOnAcl
public class AclAutoConfiguration {
@PostConstruct
void log() {
log.info("GeoServer ACL extension enabled");
}
}
Clear Dependencies: Make dependencies explicit and use <optional>true</optional>
for those that might not be available.
Consistent Naming: Follow naming conventions used by existing extensions.
Default to Disabled: Extensions should be disabled by default (DEFAULT = false
).
Test Coverage: Write comprehensive tests using ApplicationContextRunner
.
Mock Dependencies: Use Mockito to mock any beans required by the conditional annotations.
Documentation: Provide clear documentation for your extension.
Configuration Properties: Use standard prefixes and provide both new and legacy property support when needed.
Handling Service-Specific Extensions: For extensions that require specific GeoServer services (like WMS), use additional conditional annotations like @ConditionalOnGeoServerWMS
.
By following these guidelines, you can create well-structured, testable, and maintainable extensions for GeoServer Cloud.
For extensions that should only be active when a specific GeoServer service is available (like WMS, WFS, etc.), utilize the service-specific conditional annotations:
import org.geoserver.cloud.autoconfigure.extensions.ConditionalOnGeoServerWMS;
@Configuration
@ConditionalOnGeoServerWMS
public class WmsSpecificExtensionConfiguration {
// Beans that only make sense in a WMS service
}
These conditionals check for:
geoserver.service.wms.enabled=true
)The service properties are automatically set to true
in each service’s bootstrap configuration file, making service detection reliable without requiring explicit bean activation checks.
Available service conditionals:
@ConditionalOnGeoServerWMS
- For WMS service extensions@ConditionalOnGeoServerWFS
- For WFS service extensions@ConditionalOnGeoServerWCS
- For WCS service extensions@ConditionalOnGeoServerWPS
- For WPS service extensions@ConditionalOnGeoServerREST
- For REST Configuration service extensions@ConditionalOnGeoServerWebUI
- For Web UI extensionsFor extensions that integrate with the GeoServer web UI:
src/main/resources/org/geoserver/...
LoginFormInfo
) with appropriate prioritiesorg.geoserver.web
package structure@ConditionalOnGeoServerWebUI
to ensure it only activates in the web UI serviceWhen multiple extensions provide similar functionality, use the ExtensionPriority
interface:
public class PrioritizableComponent implements ExtensionPriority {
private int priority = ExtensionPriority.LOWEST;
@Override
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
}
For maintaining backward compatibility with existing configurations:
geoserver.yml
:
geoserver:
extension:
category:
name:
enabled: ${legacy.property.name:false}
Creating well-structured extensions for GeoServer Cloud promotes maintainability, testability, and modularity. By following this guide, you can create extensions that seamlessly integrate with the GeoServer Cloud ecosystem while maintaining high code quality standards.