Getting Started with the Plugin SDK
This page walks you through building a custom Processor and Transformer. By the end, you will have working components that you can upload to the platform and use in any adapter pipeline.
Prerequisites
- Java 25 SDK
- Gradle 8.0+
- The Conncentric Plugin Project Template (included in the distribution archive)
Setting Up Your Project
The Conncentric distribution archive includes a ready-to-use Gradle project template at sdk/plugin-template/. Copy this directory and rename it for your plugin:
cp -r conncentric-dist/sdk/plugin-template my-plugin
cd my-plugin
The template includes the SDK dependency, build configuration, and all required packaging scaffolding. Open gradle.properties and set your plugin's identity:
pluginId=my-plugin
pluginVersion=1.0.0
pluginProvider=Your Organization
Verify the project compiles:
./gradlew build
You are now ready to add your custom components.
Implementing a Processor
A Processor receives a message from the pipeline, performs business logic, and returns the message to continue the chain. Returning null drops the message.
Create src/main/java/com/example/myplugin/TagCountProcessor.java:
package com.example.myplugin;
import com.connamara.sdk.v1.adapter.RouteContext;
import com.connamara.sdk.v1.adapter.components.Processor;
import com.connamara.sdk.v1.common.component.ManifestResource;
import com.connamara.sdk.v1.common.message.DerivedMessage;
import com.connamara.sdk.v1.common.message.Message;
import java.util.Map;
@ManifestResource("""
{
"id": "tag-count-processor",
"pluginId": "my-plugin",
"functionalType": "tag-count-processor",
"displayName": "Tag Count Processor",
"category": "PROCESSOR",
"configuration": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Tag Count Settings",
"x-order": ["headerName"],
"properties": {
"headerName": {
"type": "string",
"title": "Output Header Name",
"description": "The header key where the tag count will be stored.",
"default": "x-tag-count"
}
}
}
}
""")
public class TagCountProcessor implements Processor {
private final String headerName;
public TagCountProcessor(Map<String, Object> configuration) {
this.headerName = (String) configuration.getOrDefault("headerName", "x-tag-count");
}
@Override
public Message<?> process(Message<?> message, RouteContext routeContext) {
int count = message.getHeaders().toMap().size();
return DerivedMessage.derive(message, message.getPayload(), Map.of(headerName, count));
}
}
Key concepts:
@ManifestResourcedefines how the Portal renders the configuration form for this component. ThepluginIdmust match thepluginIdingradle.properties. Theconfigurationblock is standard JSON Schema (Draft 07); the Portal renders it automatically.- Constructor receives the configuration map populated from the Portal form.
DerivedMessage.derive()creates a new message with additional headers while preserving the original headers and payload without copying them.
Implementing a Transformer
A Transformer changes the message format (payload type, encoding, structure). It receives a message and returns a new message with the transformed payload. Headers must be preserved using DerivedMessage.derive().
Create src/main/java/com/example/myplugin/UpperCaseTransformer.java:
package com.example.myplugin;
import com.connamara.sdk.v1.adapter.components.Transformer;
import com.connamara.sdk.v1.common.component.ManifestResource;
import com.connamara.sdk.v1.common.message.DerivedMessage;
import com.connamara.sdk.v1.common.message.Message;
import java.util.Map;
@ManifestResource("""
{
"id": "uppercase-transformer",
"pluginId": "my-plugin",
"functionalType": "uppercase-transformer",
"displayName": "Uppercase Transformer",
"category": "TRANSFORMER",
"configuration": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Uppercase Settings",
"properties": {}
}
}
""")
public class UpperCaseTransformer implements Transformer {
public UpperCaseTransformer(Map<String, Object> config) {
// No configuration needed for this transformer.
}
@Override
public Message<?> transform(Message<?> message) {
Object payload = message.getPayload();
if (payload instanceof String text) {
return DerivedMessage.derive(message, text.toUpperCase(), Map.of());
}
return message;
}
}
Registering Components
After writing your components, register them in src/main/resources/plugin-components.json so the platform discovers them at runtime:
{
"components": [
"com.example.myplugin.TagCountProcessor",
"com.example.myplugin.UpperCaseTransformer"
]
}
Each entry is the fully qualified class name of a component annotated with @ManifestResource. The platform reads this file, loads each class, and makes it available in the Portal's pipeline editor.
Writing Tests
Tests follow standard JUnit 5. Mock the SDK interfaces (Message, RouteContext, MessageHeaders) to verify your component's behavior in isolation.
TagCountProcessorTest.java
package com.example.myplugin;
import com.connamara.sdk.v1.adapter.RouteContext;
import com.connamara.sdk.v1.common.message.Message;
import com.connamara.sdk.v1.common.message.MessageHeaders;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class TagCountProcessorTest {
@Test
@DisplayName("GIVEN a message with 3 headers WHEN processed THEN output header contains count 3")
void process_countsHeaders() {
// Given
TagCountProcessor processor = new TagCountProcessor(Map.of("headerName", "x-tag-count"));
Map<String, Object> headerMap = new HashMap<>();
headerMap.put("h1", "v1");
headerMap.put("h2", "v2");
headerMap.put("h3", "v3");
Message<String> message = mock(Message.class);
when(message.getPayload()).thenReturn("test");
when(message.getHeaders()).thenReturn(new MessageHeaders(headerMap));
RouteContext routeContext = mock(RouteContext.class);
// When
Message<?> result = processor.process(message, routeContext);
// Then
assertEquals(3, result.getHeaders().get("x-tag-count"));
}
}
UpperCaseTransformerTest.java
package com.example.myplugin;
import com.connamara.sdk.v1.common.message.Message;
import com.connamara.sdk.v1.common.message.MessageHeaders;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class UpperCaseTransformerTest {
@Test
@DisplayName("GIVEN a lowercase string payload WHEN transformed THEN payload is uppercase")
void transform_uppercasesString() {
// Given
UpperCaseTransformer transformer = new UpperCaseTransformer(Map.of());
Message<String> message = mock(Message.class);
when(message.getPayload()).thenReturn("hello world");
when(message.getHeaders()).thenReturn(new MessageHeaders(Collections.emptyMap()));
// When
Message<?> result = transformer.transform(message);
// Then
assertEquals("HELLO WORLD", result.getPayload());
}
@Test
@DisplayName("GIVEN a non-string payload WHEN transformed THEN payload is unchanged")
void transform_nonString_passesThrough() {
// Given
UpperCaseTransformer transformer = new UpperCaseTransformer(Map.of());
Message<Integer> message = mock(Message.class);
when(message.getPayload()).thenReturn(42);
when(message.getHeaders()).thenReturn(new MessageHeaders(Collections.emptyMap()));
// When
Message<?> result = transformer.transform(message);
// Then
assertEquals(42, result.getPayload());
}
}
Building and Deploying
Build the plugin JAR:
./gradlew build
The output JAR is in build/libs/. Deploy it using one of two methods:
- Upload via the Portal: Go to Settings > Plugins and upload the JAR. Your new components appear in the pipeline editor immediately.
- Include in a custom bundle for automated deployment. Copy the JAR into the
plugins/directory of your bundle. See Custom Bundles & Extensibility for the full workflow.
What You Can Build
| Component Type | Interface | Purpose |
|---|---|---|
| Processor | Processor | Business logic: enrich, validate, filter, route, or drop messages |
| Transformer | Transformer | Format conversion: change the message payload type or structure |
| Connector | Connector | Protocol I/O: connect to external systems (FIX venues, message brokers, APIs) |
Processors and Transformers are the most common extension points. Connectors are for teams that need to integrate with protocols not covered by the official plugins.
Defining the Configuration Form
The @ManifestResource annotation on each component class defines the JSON Schema that drives the Portal configuration form. The Portal renders the form automatically from your schema, including validation, tooltips, and conditional fields.
See Manifest Schema for the full specification of supported fields and extension keys (x-order, x-section-title, x-advanced, x-visible-if, x-widget).