Repository: kroxylicious/kroxylicious
Pull request #159 (2023-02-27 23:01:49)

Extract kroxylicious-api and kroxylicious-filter-api modules
## Why

1. We want to create a clear separation of the interfaces and classes required to build custom filters and the internals of the proxy.
2. We want to re-use the Contributor interface for configuring and instantiating more services in future.

Filter authors should be able to create their own module that depends on `kroxylicious-filter-api`, implement a Filter using the interfaces provided, implement their own Contributor (or extend the BaseContributor) and include the service definition file in META-INF. Then their custom filters should be loaded into kroxylicious.

robobario: I've tested it's possible to extract the `MultiTenantTransformationFilter` into a module that only depends on kroxylicious-filter-api and implement a `Contributor` (plus add the service to META-INF) then see the integration tests pass once a dependency is added on that new module.

Patch:
diff --git a/api/kroxylicious-api/pom.xml b/api/kroxylicious-api/pom.xml
new file mode 100644
index 00000000..41324858
--- /dev/null
+++ b/api/kroxylicious-api/pom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright Kroxylicious Authors.
+
+    Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+
+-->
+<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>io.kroxylicious</groupId>
+        <artifactId>kroxylicious-parent</artifactId>
+        <version>1.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>kroxylicious-api</artifactId>
+    <version>0.0.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <name>Kroxylicious API</name>
+    <description>generic APIs used throughout Kroxylicious</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/BaseConfig.java b/api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/config/BaseConfig.java
similarity index 100%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/config/BaseConfig.java
rename to api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/config/BaseConfig.java
diff --git a/api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/config/ProxyConfig.java b/api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/config/ProxyConfig.java
new file mode 100644
index 00000000..3035aead
--- /dev/null
+++ b/api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/config/ProxyConfig.java
@@ -0,0 +1,10 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package io.kroxylicious.proxy.config;
+
+public interface ProxyConfig {
+    String address();
+}
diff --git a/api/kroxylicious-filter-api/pom.xml b/api/kroxylicious-filter-api/pom.xml
new file mode 100644
index 00000000..3863adc4
--- /dev/null
+++ b/api/kroxylicious-filter-api/pom.xml
@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright Kroxylicious Authors.
+
+    Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+
+-->
+<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>io.kroxylicious</groupId>
+        <artifactId>kroxylicious-parent</artifactId>
+        <version>1.0-SNAPSHOT</version>
+        <relativePath>../../pom.xml</relativePath>
+    </parent>
+    
+    <artifactId>kroxylicious-filter-api</artifactId>
+    <version>0.0.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <properties>
+        <libs.dir>libs</libs.dir>
+    </properties>
+
+    <name>Kroxylicious Filter API</name>
+    <description>The public interfaces, classes, etc required to author custom filters and configure Kroxylicious to use them</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-api</artifactId>
+            <version>0.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>io.micrometer</groupId>
+            <artifactId>micrometer-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.kafka</groupId>
+            <artifactId>kafka-clients</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>unpack-message-specs</id>
+                        <goals>
+                            <goal>unpack</goal>
+                        </goals>
+                        <phase>generate-sources</phase>
+                        <configuration>
+                            <artifact>org.apache.kafka:kafka-clients:${kafka.version}</artifact>
+                            <includes>common/message/*.json</includes>
+                            <outputDirectory>${project.build.directory}/message-specs</outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <!-- This actually shouldn't be needed, as the plug-in itself adds that directory as a source directory;
+                 Unfortunately, Eclipse/M2E doesn't reliably pick it up without this helper plug-in -->
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>add-source</id>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>add-source</goal>
+                        </goals>
+                        <configuration>
+                            <sources>
+                                <source>${project.build.directory}/generated-sources/krpc</source>
+                            </sources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>io.kroxylicious</groupId>
+                <artifactId>kroxylicious-krpc-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>generate-filters</id>
+                        <goals>
+                            <goal>generate-single</goal>
+                        </goals>
+                        <phase>generate-sources</phase>
+                        <configuration>
+                            <messageSpecDirectory>${project.build.directory}/message-specs/common/message</messageSpecDirectory>
+                            <messageSpecFilter>*{Request,Response}.json</messageSpecFilter>
+                            <templateDirectory>${project.basedir}/src/main/templates</templateDirectory>
+                            <templateNames>Kproxy/Filter.ftl</templateNames>
+                            <outputFilePattern>${messageSpecName}Filter.java</outputFilePattern>
+                            <outputPackage>io.kroxylicious.proxy.filter</outputPackage>
+                            <outputDirectory>${project.build.directory}/generated-sources/krpc</outputDirectory>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>generate-request-filters</id>
+                        <goals>
+                            <goal>generate-multi</goal>
+                        </goals>
+                        <phase>generate-sources</phase>
+                        <configuration>
+                            <messageSpecDirectory>${project.build.directory}/message-specs/common/message</messageSpecDirectory>
+                            <messageSpecFilter>*{Request,Response}.json</messageSpecFilter>
+                            <templateDirectory>${project.basedir}/src/main/templates</templateDirectory>
+                            <templateNames>Kproxy/KrpcFilter.ftl</templateNames>
+                            <outputFilePattern>${templateName}.java</outputFilePattern>
+                            <outputPackage>io.kroxylicious.proxy.filter</outputPackage>
+                            <outputDirectory>${project.build.directory}/generated-sources/krpc</outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/filter/FilterContributor.java b/api/kroxylicious-filter-api/src/main/java/io/kroxylicious/proxy/filter/FilterContributor.java
similarity index 100%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/filter/FilterContributor.java
rename to api/kroxylicious-filter-api/src/main/java/io/kroxylicious/proxy/filter/FilterContributor.java
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java b/api/kroxylicious-filter-api/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java
similarity index 100%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java
rename to api/kroxylicious-filter-api/src/main/java/io/kroxylicious/proxy/filter/KrpcFilterContext.java
diff --git a/kroxylicious/src/main/templates/Kproxy/Filter.ftl b/api/kroxylicious-filter-api/src/main/templates/Kproxy/Filter.ftl
similarity index 100%
rename from kroxylicious/src/main/templates/Kproxy/Filter.ftl
rename to api/kroxylicious-filter-api/src/main/templates/Kproxy/Filter.ftl
diff --git a/kroxylicious/src/main/templates/Kproxy/KrpcFilter.ftl b/api/kroxylicious-filter-api/src/main/templates/Kproxy/KrpcFilter.ftl
similarity index 100%
rename from kroxylicious/src/main/templates/Kproxy/KrpcFilter.ftl
rename to api/kroxylicious-filter-api/src/main/templates/Kproxy/KrpcFilter.ftl
diff --git a/kroxylicious/pom.xml b/kroxylicious/pom.xml
index ef9d370d..655184b1 100644
--- a/kroxylicious/pom.xml
+++ b/kroxylicious/pom.xml
@@ -28,6 +28,16 @@
     <name>Kroxylicious Proxy</name>
 
     <dependencies>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-api</artifactId>
+            <version>0.0.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>io.kroxylicious</groupId>
+            <artifactId>kroxylicious-filter-api</artifactId>
+            <version>0.0.0-SNAPSHOT</version>
+        </dependency>
         <!-- We depend on the specific Netty dependencies not netty-all to reduce the size of fatjars -->
         <dependency>
             <groupId>io.netty</groupId>
@@ -195,44 +205,6 @@
                     </execution>
                 </executions>
             </plugin>
-            <plugin>
-                <groupId>io.kroxylicious</groupId>
-                <artifactId>kroxylicious-krpc-plugin</artifactId>
-                <executions>
-                    <execution>
-                        <id>generate-filters</id>
-                        <goals>
-                            <goal>generate-single</goal>
-                        </goals>
-                        <phase>generate-sources</phase>
-                        <configuration>
-                            <messageSpecDirectory>${project.build.directory}/message-specs/common/message</messageSpecDirectory>
-                            <messageSpecFilter>*{Request,Response}.json</messageSpecFilter>
-                            <templateDirectory>${project.basedir}/src/main/templates</templateDirectory>
-                            <templateNames>Kproxy/Filter.ftl</templateNames>
-                            <outputFilePattern>${messageSpecName}Filter.java</outputFilePattern>
-                            <outputPackage>io.kroxylicious.proxy.filter</outputPackage>
-                            <outputDirectory>${project.build.directory}/generated-sources/krpc</outputDirectory>
-                        </configuration>
-                    </execution>
-                    <execution>
-                        <id>generate-request-filters</id>
-                        <goals>
-                            <goal>generate-multi</goal>
-                        </goals>
-                        <phase>generate-sources</phase>
-                        <configuration>
-                            <messageSpecDirectory>${project.build.directory}/message-specs/common/message</messageSpecDirectory>
-                            <messageSpecFilter>*{Request,Response}.json</messageSpecFilter>
-                            <templateDirectory>${project.basedir}/src/main/templates</templateDirectory>
-                            <templateNames>Kproxy/KrpcFilter.ftl</templateNames>
-                            <outputFilePattern>${templateName}.java</outputFilePattern>
-                            <outputPackage>io.kroxylicious.proxy.filter</outputPackage>
-                            <outputDirectory>${project.build.directory}/generated-sources/krpc</outputDirectory>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
         </plugins>
     </build>
 
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/Configuration.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/Configuration.java
index 37bad001..1d75984e 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/Configuration.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/Configuration.java
@@ -13,14 +13,14 @@ import io.kroxylicious.proxy.config.micrometer.MicrometerConfiguration;
 
 public class Configuration {
 
-    private final ProxyConfig proxy;
+    private final DefaultProxyConfig proxy;
 
     private final AdminHttpConfiguration adminHttp;
     private final MicrometerConfiguration micrometer;
     private final Map<String, Cluster> clusters;
     private final List<FilterDefinition> filters;
 
-    public Configuration(ProxyConfig proxy,
+    public Configuration(DefaultProxyConfig proxy,
                          AdminHttpConfiguration adminHttp,
                          MicrometerConfiguration micrometer,
                          Map<String, Cluster> clusters,
@@ -32,7 +32,7 @@ public class Configuration {
         this.filters = filters;
     }
 
-    public ProxyConfig proxy() {
+    public DefaultProxyConfig proxy() {
         return proxy;
     }
 
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ProxyConfig.java b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/DefaultProxyConfig.java
similarity index 82%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/config/ProxyConfig.java
rename to kroxylicious/src/main/java/io/kroxylicious/proxy/config/DefaultProxyConfig.java
index f07e01af..6cfdd4fa 100644
--- a/kroxylicious/src/main/java/io/kroxylicious/proxy/config/ProxyConfig.java
+++ b/kroxylicious/src/main/java/io/kroxylicious/proxy/config/DefaultProxyConfig.java
@@ -7,7 +7,7 @@ package io.kroxylicious.proxy.config;
 
 import java.util.Optional;
 
-public class ProxyConfig {
+public class DefaultProxyConfig implements ProxyConfig {
 
     private final String address;
     private final boolean logNetwork;
@@ -16,7 +16,7 @@ public class ProxyConfig {
     private final Optional<String> keyStoreFile;
     private final Optional<String> keyPassword;
 
-    public ProxyConfig(String address, boolean logNetwork, boolean logFrames, boolean useIoUring, Optional<String> keyStoreFile, Optional<String> keyPassword) {
+    public DefaultProxyConfig(String address, boolean logNetwork, boolean logFrames, boolean useIoUring, Optional<String> keyStoreFile, Optional<String> keyPassword) {
         this.address = address;
         this.logNetwork = logNetwork;
         this.logFrames = logFrames;
@@ -25,6 +25,7 @@ public class ProxyConfig {
         this.keyPassword = keyPassword;
     }
 
+    @Override
     public String address() {
         return address;
     }
diff --git a/pom.xml b/pom.xml
index da4a9d24..aee8725e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -205,6 +205,8 @@
     </dependencyManagement>
 
     <modules>
+        <module>api/kroxylicious-api</module>
+        <module>api/kroxylicious-filter-api</module>
         <module>krpc-code-gen</module>
         <module>kroxylicious</module>
         <module>integrationtests</module>


Test patch:
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/service/BaseContributor.java b/api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/BaseContributor.java
similarity index 100%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/service/BaseContributor.java
rename to api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/BaseContributor.java
diff --git a/kroxylicious/src/main/java/io/kroxylicious/proxy/service/Contributor.java b/api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/Contributor.java
similarity index 100%
rename from kroxylicious/src/main/java/io/kroxylicious/proxy/service/Contributor.java
rename to api/kroxylicious-api/src/main/java/io/kroxylicious/proxy/service/Contributor.java
diff --git a/api/kroxylicious-api/src/test/java/io/kroxylicious/proxy/service/BaseContributorTest.java b/api/kroxylicious-api/src/test/java/io/kroxylicious/proxy/service/BaseContributorTest.java
new file mode 100644
index 00000000..48719711
--- /dev/null
+++ b/api/kroxylicious-api/src/test/java/io/kroxylicious/proxy/service/BaseContributorTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright Kroxylicious Authors.
+ *
+ * Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+ */
+package io.kroxylicious.proxy.service;
+
+import org.junit.jupiter.api.Test;
+
+import io.kroxylicious.proxy.config.BaseConfig;
+import io.kroxylicious.proxy.config.ProxyConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class BaseContributorTest {
+    static class LongConfig extends BaseConfig {
+        private final Long value = 2L;
+
+    }
+
+    static class AnotherConfig extends BaseConfig {
+    }
+
+    static class LongProxyConfig implements ProxyConfig {
+        @Override
+        public String address() {
+            return "3";
+        }
+    }
+
+    @Test
+    public void testDefaultConfigClass() {
+        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        builder.add("one", () -> 1L);
+        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        };
+        Class<? extends BaseConfig> one = baseContributor.getConfigType("one");
+        assertThat(one).isEqualTo(BaseConfig.class);
+    }
+
+    @Test
+    public void testSupplier() {
+        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        builder.add("one", () -> 1L);
+        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        };
+        Long instance = baseContributor.getInstance("one", new LongProxyConfig(), new BaseConfig());
+        assertThat(instance).isEqualTo(1L);
+    }
+
+    @Test
+    public void testSpecifyingConfigType() {
+        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        builder.add("fromBaseConfig", LongConfig.class, baseConfig -> baseConfig.value);
+        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        };
+        Class<? extends BaseConfig> configType = baseContributor.getConfigType("fromBaseConfig");
+        assertThat(configType).isEqualTo(LongConfig.class);
+    }
+
+    @Test
+    public void testSpecifyingConfigTypeInstance() {
+        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        builder.add("fromBaseConfig", LongConfig.class, baseConfig -> baseConfig.value);
+        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        };
+        Long instance = baseContributor.getInstance("fromBaseConfig", new LongProxyConfig(), new LongConfig());
+        assertThat(instance).isEqualTo(2L);
+    }
+
+    @Test
+    public void testFailsIfConfigNotAssignableToSpecifiedType() {
+        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        builder.add("fromBaseConfig", LongConfig.class, baseConfig -> baseConfig.value);
+        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        };
+        AnotherConfig incompatibleConfig = new AnotherConfig();
+        assertThatThrownBy(() -> {
+            baseContributor.getInstance("fromBaseConfig", new LongProxyConfig(), incompatibleConfig);
+        }).isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    public void testUsingProxyConfig() {
+        BaseContributor.BaseContributorBuilder<Long> builder = BaseContributor.builder();
+        builder.add("fromProxyConfig", LongConfig.class, (proxyConfig, longConfig) -> Long.valueOf(proxyConfig.address()));
+        BaseContributor<Long> baseContributor = new BaseContributor<>(builder) {
+        };
+        Long instance = baseContributor.getInstance("fromProxyConfig", new LongProxyConfig(), new LongConfig());
+        assertThat(instance).isEqualTo(3L);
+    }
+
+}
\ No newline at end of file