Skip to main content

Components Overview

A custom plugin contributes one or more components to the platform. Each component is a Java class that implements one of the five SDK interfaces and carries an @ManifestResource annotation describing its configuration form.

This page is the index. Each component type has its own page with the contract, a complete working example, common gotchas, and when to reach for it.

ComponentInterfaceWhat it doesPage
ConnectorConnectorProtocol I/O. Opens and maintains sessions with external systems; hands inbound messages into the pipeline; accepts outbound messages from the pipeline.Connector
ProcessorProcessorPer-message business logic. Enriches, validates, routes, or drops a message.Processor
TransformerTransformerFormat conversion. Produces a new message whose payload or structure differs from the input.Transformer
ConditionConditionRoute selection (predicate or filter). Decides whether a route should run for a given message.Condition
ErrorProcessorErrorProcessorFailure handling. Decides whether a failed message is recovered (Ack) or rejected (Nack).ErrorProcessor

All five interfaces live in package com.connamara.sdk.v1.adapter.components.


Pipeline order

A route runs each step in order, every step optional except the source.

Source ──▶ Conditions ──▶ Transformers ──▶ Processors ──▶ Target

└──(on failure)──▶ ErrorProcessors
StepComponent type used here
SourceA Connector that produces messages from an external system.
ConditionsZero or more Condition components evaluated as a single AND expression. If any returns false, the route is skipped for that message.
TransformersZero or more Transformer components applied in declared order. Each receives the previous step's output.
ProcessorsZero or more Processor components applied in declared order. Side effects (validation, enrichment, send-to-target) live here.
TargetA Connector that accepts messages bound for an external system.
ErrorProcessorsZero or more ErrorProcessor components, evaluated in order on any unhandled exception from the pipeline.

A single plugin can contribute components for any combination of these slots. The official Essentials plugin, for example, ships Connectors, Processors, Transformers, and Conditions all in one JAR.


What every component shares

Every component, regardless of type, follows three rules.

1. @ManifestResource is required

Every component class carries the com.connamara.sdk.v1.common.component.ManifestResource annotation. Its value is either an inline JSON Schema string or a path to a JSON file on the classpath. The Portal reads the schema and renders the configuration form (fields, validation, defaults, conditional visibility) from it. A component without @ManifestResource is invisible to the Portal. See Component Manifest Schema for the full specification.

2. The constructor receives the operator's configuration

Every component declares a public constructor that accepts the operator's configuration. The plugin's entry point (see rule 3) constructs the component once per configured instance and passes the Map<String, Object> populated from the Portal form. The platform does not construct your class itself; it calls the entry point's createComponent, which does. Read fields from the map; do not query external configuration sources.

public MyComponent(Map<String, Object> configuration) {
this.threshold = (Integer) configuration.getOrDefault("threshold", 100);
}

A component may take additional constructor arguments (a host resource, a shared client) as long as the entry point's createComponent supplies them. The single Map<String, Object> constructor is the common case, not a hard requirement.

3. The class must be registered with the plugin entry point

Writing the class is not enough. The platform never scans your JAR for component classes. It discovers one class per plugin annotated with @org.pf4j.Extension and implementing com.connamara.sdk.v1.adapter.AdapterPlugin, and asks that entry point for its components. Registration means adding the component in two matching places on that entry point:

@Extension
public class MyPlugin implements AdapterPlugin {

// 1. Map the component's type id (the "id" from its @ManifestResource) to its class.
private static final SimpleComponentRegistry registry = new SimpleComponentRegistry(Map.ofEntries(
Map.entry("my-component", MyComponent.class)
));

@Override public String getId() { return "my-plugin"; }
@Override public String getDisplayName() { return "My Plugin"; }

@Override
public Optional<ComponentMetadata> getComponentMetadata(String componentId) {
return registry.getMetadata(componentId);
}

// 2. Instantiate it in the createComponent switch.
@Override
public AdapterComponent createComponent(String type, Map<String, Object> configuration) {
AdapterComponent component = switch (type) {
case "my-component" -> new MyComponent(configuration);
default -> null;
};
if (component != null) {
registry.trackInstance((String) configuration.get("instanceId"), component);
}
return component;
}

@Override
public List<AdapterComponent> getComponents() {
return List.copyOf(registry.getTrackedInstances().values());
}
}

The registry entry and the createComponent case are the load-bearing step, and the type id in both must equal the component's @ManifestResource id. A second class extending org.pf4j.Plugin, plus the Plugin-Class, Plugin-Base-Package, and System-Id manifest attributes in build.gradle.kts, let PF4J discover this entry point. Miss any of these and the JAR loads but contributes no components, so routes fail with No plugin found that handles component type: <type>. The full walkthrough (including the lifecycle class, the PF4J annotationProcessor, and the manifest block) is in Getting Started.

src/main/resources/plugin-components.json is not used by the runtime. Registration flows entirely through the @Extension AdapterPlugin entry point above.


Messages are immutable

Components see Message<?> instances and never mutate them. To change a payload or add headers, derive a new message:

return DerivedMessage.derive(message, newPayload, Map.of("x-added-header", value));

DerivedMessage.derive(Message<?> source, Object payload, Map<String, Object> additionalHeaders) returns a new wrapper that carries the source's original headers plus the supplied additions, with the supplied payload. The wrapper references the source rather than copying it, which preserves the platform's zero-copy semantics.

Return value (Processor / Transformer)Pipeline behavior
The same message reference passed inMessage continues unchanged to the next step.
A DerivedMessage.derive(...) resultNew message continues with the derived headers and payload.
null (Processor only)Message is dropped from the pipeline.

Configuration form is rendered from the schema

A component's manifest contains a JSON Schema in its configuration field. The Portal renders this schema into a form, validates operator input against it, and hands the validated Map<String, Object> to the plugin's createComponent, which constructs the component from it. The component never builds its own UI. See Component Manifest Schema for the supported fields and the x- extension keys that control layout, widgets, and conditional visibility, and Manifest Examples for two end-to-end manifests.


  1. Getting Started for the build and packaging workflow plus a Transformer walkthrough.
  2. The component-type page closest to what you are building.
  3. Component Manifest Schema once your component works and you want to refine the Portal form.
  4. Custom Bundles & Extensibility for deploying the JAR into a running platform.