Development Guide¶
Table of Contents¶
- Getting Started
- Project Structure
- Building
- Testing
- Code Conventions
- Extending the Extension
- Debugging
- Contributing
Getting Started¶
Prerequisites¶
- Java: JDK 21 or higher
- Maven: 3.9.0 or higher
- IDE: IntelliJ IDEA, Eclipse, or VS Code with Java extensions
- Docker: For integration testing (optional)
- Git: For version control
Clone the Repository¶
IDE Setup¶
IntelliJ IDEA¶
- Open the project:
File β Open β Select pom.xml - Wait for Maven import to Complete
- Set JDK:
File β Project Structure β Project SDK β 21 - Enable annotation processing:
Settings β Build β Compiler β Annotation Processors β Enable
VS Code¶
- Install extensions:
- Extension Pack for Java
- Maven for Java
- Open folder
- Select Java 21 in status bar
- Maven will auto-import dependencies
Eclipse¶
- Import project:
File β Import β Maven β Existing Maven Projects - Select project directory
- Set JDK:
Properties β Java Build Path β Libraries β Add Library β JRE System Library
Project Structure¶
kete/
βββ src/
β βββ main/
β β βββ java/io/github/fortunen/kete/
β β β βββ CertificateLoader.java # Abstract certificate loader
β β β βββ Component.java # DI annotation
β β β βββ Configuration.java # Config parser
β β β βββ Constants.java # Constants
β β β βββ Destination.java # Abstract destination
β β β βββ DestinationConfig.java # Destination config base
β β β βββ DestinationPooledObjectFactory.java # Destination pooling
β β β βββ EventMessage.java # Event data record
β β β βββ Matcher.java # Abstract matcher
β β β βββ MatchMode.java # Match mode enum
β β β βββ OAuthMaterial.java # OAuth configuration
β β β βββ Provider.java # Event handler
β β β βββ ProviderFactory.java # Lifecycle mgmt
β β β βββ Route.java # Route configuration
β β β βββ Serializer.java # Abstract serializer
β β β βββ SerializerRoutes.java # Serializer-routes mapping
β β β βββ TlsMaterial.java # TLS configuration
β β β βββ certificateloaders/ # Certificate loader implementations
β β β β βββ DerFileBase64CertificateLoader.java
β β β β βββ DerFilePathCertificateLoader.java
β β β β βββ JksFileBase64CertificateLoader.java
β β β β βββ JksFilePathCertificateLoader.java
β β β β βββ PemFileBase64CertificateLoader.java
β β β β βββ PemFilePathCertificateLoader.java
β β β β βββ PemFileTextCertificateLoader.java
β β β β βββ Pkcs12FileBase64CertificateLoader.java
β β β β βββ Pkcs12FilePathCertificateLoader.java
β β β β βββ Pkcs7FileBase64CertificateLoader.java
β β β β βββ Pkcs7FilePathCertificateLoader.java
β β β βββ destinations/ # Destination implementations
β β β β βββ Amqp091Destination.java
β β β β βββ Amqp091DestinationConfig.java
β β β β βββ Amqp1Destination.java
β β β β βββ Amqp1DestinationConfig.java
β β β β βββ HttpDestination.java
β β β β βββ HttpDestinationConfig.java
β β β β βββ KafkaDestination.java
β β β β βββ KafkaDestinationConfig.java
β β β β βββ Mqtt3Destination.java
β β β β βββ Mqtt3DestinationConfig.java
β β β β βββ Mqtt5Destination.java
β β β β βββ Mqtt5DestinationConfig.java
β β β βββ matchers/ # Matcher implementations
β β β β βββ GlobMatcher.java
β β β β βββ ListMatcher.java
β β β β βββ RegexMatcher.java
β β β β βββ SqlMatcher.java
β β β βββ serializers/ # Serializer implementations
β β β β βββ CborSerializer.java
β β β β βββ CsvSerializer.java
β β β β βββ JsonSerializer.java
β β β β βββ PropertiesSerializer.java
β β β β βββ SmileSerializer.java
β β β β βββ TomlSerializer.java
β β β β βββ XmlSerializer.java
β β β β βββ YamlSerializer.java
β β β βββ utils/ # Utility classes
β β β βββ CertificateUtils.java
β β β βββ ConfigurationUtils.java
β β β βββ DestinationUtils.java
β β β βββ FileUtils.java
β β β βββ IocUtils.java
β β β βββ MatcherUtils.java
β β β βββ RetryUtils.java
β β β βββ RouteUtils.java
β β β βββ SerializerUtils.java
β β β βββ TemplateUtils.java
β β β βββ ValidationUtils.java
β β βββ resources/
β β βββ META-INF/services/
β β βββ org.keycloak.events.EventListenerProviderFactory
β βββ tests/
β βββ java/io/github/fortunen/kete/
β β βββ certificateloaders/ # Certificate loader tests
β β βββ destinationpooledobjectfactory/ # Pool factory tests
β β βββ destinations/ # Destination tests
β β βββ e2e/ # End-to-end tests
β β βββ eventmessage/ # EventMessage tests
β β βββ matchers/ # Matcher tests
β β βββ oauthmaterial/ # OAuth tests
β β βββ provider/ # Provider tests
β β βββ providerfactory/ # Factory tests
β β βββ route/ # Route tests
β β βββ serializers/ # Serializer tests
β β βββ tlsmaterial/ # TLS tests
β β βββ utils/ # Utility tests
β βββ resources/
β βββ testcontainers.properties # Test config
βββ docs/ # Documentation
βββ Dockerfile # Container image
βββ pom.xml # Maven config
βββ README.md # Main documentation
Key Files¶
| File | Purpose |
|---|---|
pom.xml |
Maven dependencies and build configuration |
META-INF/services/... |
SPI registration for Keycloak |
Component.java |
Custom DI annotation for component discovery |
ProviderFactory.java |
Extension entry point and lifecycle management |
Provider.java |
Event processing logic |
Route.java |
Route configuration with matchers, serializer, destination |
DestinationConfig.java |
Base class for destination configurations |
TlsMaterial.java |
TLS/SSL configuration builder |
Building¶
Dependency Shading (Critical)¶
All runtime dependencies are shade-relocated to prevent classpath conflicts with Keycloak's internal libraries.
Why this matters:
- Keycloak bundles Guava, Jackson, Netty, Apache Commons, etc.
- Different Keycloak versions use different library versions
- Without shading: NoSuchMethodError, ClassNotFoundException at runtime
- With shading: Extension works across Keycloak 25.x β 26.x+ without recompilation
What is shaded:
Every dependency without <scope>provided</scope> or <scope>test</scope> is relocated under io.github.fortunen.kete.shaded.*
See: pom.xml β maven-shade-plugin β <relocations> section (17 libraries relocated)
NEVER: Add runtime dependencies without corresponding <relocation> entries.
Full Build¶
Output: target/kete.jar (shaded JAR with all dependencies isolated)
Skip Tests¶
Clean Build¶
Build with Coverage¶
Output: target/site/jacoco/index.html
Verify Build¶
Runs all tests and creates the final JAR.
Testing¶
Unit Tests¶
Run all unit tests:
Run specific test:
Run tests in a package:
Test Naming Convention¶
Tests are organized following the one-method-per-file pattern:
Directory structure: {classname}/{methodName}Tests.java
Examples:
- provider/constructorTests.java - Tests Provider constructor
- provider/onEventTests.java - Tests Provider.onEvent() method
- providerfactory/closeTests.java - Tests ProviderFactory.close() method
Method naming: should{ExpectedBehavior}[When{Condition}]
Examples:
- shouldReturnTrueWhenEventMatches()
- shouldThrowWhenConfigurationIsNull()
- shouldSerializeEventToJson()
Test Structure (AAA Pattern)¶
All tests use the Arrange-Act-Assert pattern with required comments:
@Test
public void shouldDoSomethingWhenConditionMet() {
// arrange
var instance = new ClassUnderTest();
var input = "test-input";
// act
var result = instance.methodUnderTest(input);
// assert
assertThat(result).isNotNull();
assertThat(result).isEqualTo("expected");
}
Mocking with Mockito¶
Use mock() method (not @Mock annotations):
@Test
public void shouldSendMessageToDestination() {
// arrange
var destination = mock(Destination.class);
when(destination.accept("LOGIN")).thenReturn(true);
// act
route.send(message);
// assert
verify(destination).sendMessage(any(EventMessage.class));
}
Exception Testing¶
Use catchThrowable() with chained assertions:
@Test
public void shouldThrowWhenConfigurationIsNull() {
// arrange
var instance = new ClassUnderTest();
// act
var thrown = catchThrowable(() -> instance.initialize(null));
// assert
assertThat(thrown)
.isInstanceOf(IllegalStateException.class)
.hasMessage("configuration is required");
}
Integration Tests¶
Integration tests use Testcontainers for Kafka, RabbitMQ, etc.:
Tests automatically start containers, run tests, and clean up.
Coverage Reports¶
mvn clean test jacoco:report
open target/site/jacoco/index.html # macOS/Linux
start target/site/jacoco/index.html # Windows
Current Coverage: ~90% (check latest report)
Code Conventions¶
Java Style¶
- Indentation: Tabs, no spaces
- Line Length: None
- Braces: K&R style (opening brace on same line)
- Naming:
- Classes:
PascalCase - Methods:
camelCase - Constants:
UPPER_SNAKE_CASE - Packages:
lowercase
Code Example¶
public class MyComponent {
private static final String CONSTANT_VALUE = "value";
private final Logger logger;
private final String configuration;
public MyComponent(Logger logger, String configuration) {
this.logger = Objects.requireNonNull(logger, "logger is required");
this.configuration = Objects.requireNonNull(configuration, "configuration is required");
}
public void processEvent(Event event) {
try {
// Process event
} catch (Exception exception) {
logger.log(Level.WARNING, "Failed to process event", exception);
}
}
}
Documentation¶
- No JavaDoc: Code should be self-documenting through clear naming
- Inline Comments: Minimal - only for truly complex logic
- README: Keep README.md up to date
- Developer Docs: Update docs/ when adding features
Extending the Extension¶
Adding a New Destination¶
See Extending KETE for comprehensive details.
Quick Steps:
- Create config class extending
DestinationConfig - Create destination class extending
Destination<TConfig> - Add
@Component(name = "xxx")annotation - Implement
doInitialize()anddoSend(EventMessage) - Add tests following the test patterns
Adding a New Serializer¶
See Extending KETE for comprehensive details.
Quick Steps:
- Create class extending
Serializer - Add
@Component(name = "xxx", scope = Component.SINGLETON) - Set
contentTypein constructor - Implement
serialize(Event)andserialize(AdminEvent) - Add tests
Adding a New Matcher¶
See Extending KETE for comprehensive details.
Quick Steps:
- Create class extending
Matcher - Add
@Component(name = "xxx")annotation - Implement
initialize()andmatches(String) - Add tests
Debugging¶
Local Debugging with IDE¶
-
Build the extension:
-
Copy to local Keycloak:
-
Start Keycloak in debug mode:
-
Attach debugger:
- IntelliJ:
Run β Attach to Process β Select Keycloak - VS Code: Add configuration:
{
"type": "java",
"name": "Attach to Keycloak",
"request": "attach",
"hostName": "localhost",
"port": 5005
}
-
Set breakpoints in your code
-
Trigger events in Keycloak (login, logout, etc.)
Docker Debugging¶
- Build Docker image with debug enabled:
FROM quay.io/keycloak/keycloak:26.5.0
COPY target/kete.jar /opt/keycloak/providers/
ENV JAVA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
RUN /opt/keycloak/bin/kc.sh build
-
Run with port exposed:
-
Attach debugger to
localhost:5005
Logging¶
Enable detailed logging:
View logs:
Common Debug Scenarios¶
Extension not loading¶
- Check
providers/directory has JAR - Check logs for SPI registration
- Verify
META-INF/servicesfile is in JAR:
Events not streaming¶
- Check
enabledis not set tofalse(defaults totrue) - Check realm has listener registered:
- Admin Console β Realm β Events β Event Listeners
- Check destination configuration is valid
- Check destination connection logs
Serialization errors¶
- Add breakpoint in
Serializer.serialize() - Check event structure
- Verify Jackson configuration
Contributing¶
Development Workflow¶
-
Fork the repository on GitHub
-
Clone your fork:
-
Create a feature branch:
-
Make changes following code conventions
-
Write tests for new code
-
Run tests locally:
-
Run coverage check:
-
Commit with descriptive message:
-
Push to your fork:
-
Create Pull Request on GitHub
Pull Request Guidelines¶
- One feature per PR: Keep PRs focused
- Tests required: All new code must have tests
- Documentation: Update docs for user-facing changes
- Code style: Follow existing conventions
- Clean history: Squash commits if needed
- Descriptive title: "Add SQS destination" not "Update"
Commit Message Format¶
Types:
- feat: New feature
- fix: Bug fix
- docs: Documentation changes
- test: Test additions/changes
- refactor: Code refactoring
- perf: Performance improvement
- chore: Build/tooling changes
Code Review Process¶
- Automated checks run (tests, coverage)
- Maintainer reviews code
- Address feedback
- Approval β Merge
Getting Help¶
- Issues: Open GitHub issue for bugs/features
- Discussions: Use GitHub discussions for questions
- Documentation: Check existing docs first
Useful Maven Commands¶
# Build without tests
mvn clean package -DskipTests
# Run single test class
mvn test -Dtest=ProviderTest
# Run tests matching pattern
mvn test -Dtest=*Provider*
# Show dependency tree
mvn dependency:tree
# Update dependencies
mvn versions:display-dependency-updates
# Format code (if formatter configured)
mvn formatter:format
# Check for outdated plugins
mvn versions:display-plugin-updates
# Install to local Maven repo
mvn clean install
Troubleshooting¶
Build Fails¶
Problem: JAVA_HOME not set
Solution:
Problem: Maven version too old
Solution: Upgrade to Maven 3.9+
Tests Fail¶
Problem: Docker not running for Testcontainers
Solution: Start Docker daemon
Problem: Port conflicts in tests
Solution: Stop services using ports 9092 (Kafka), 5672 (RabbitMQ)
IDE Issues¶
Problem: "Cannot resolve symbol" errors
Solution: Reimport Maven project
- IntelliJ: Maven β Reload Project
- Eclipse: Right-click project β Maven β Update Project
- VS Code: Cmd+Shift+P β Java: Clean Language Server Workspace