Skip to main content

Documentation Index

Fetch the complete documentation index at: https://platform.docs.zenoo.com/llms.txt

Use this file to discover all available pages before exploring further.

Plugin System in Zenoo Hub

The Zenoo Hub features a robust plugin system that empowers developers to extend the platform with custom connectors and components, enabling seamless integration with third-party services and rapid adaptation to evolving business needs.

Overview

The plugin system allows you to package, deploy, and manage custom logic independently of the Hub core. Plugins can introduce new connectors, orchestration steps, or business logic, all while maintaining a clean separation from the main codebase.

Benefits

  • Extensibility: Add new features and integrations without modifying the Hub core.
  • Modularity: Develop, test, and deploy plugins independently.
  • Maintainability: Isolate customer-specific or experimental logic from the main platform.
  • Rapid Innovation: Prototype and roll out new capabilities quickly.

Architecture

Plugins are loaded at runtime by the Hub, using a dedicated classloader to ensure isolation and prevent conflicts. Each plugin can declare its own dependencies, resources, and configuration.

Types of Plugins

  • Custom Connectors: Integrate with external APIs, databases, or services.
  • Transport Customizers: Enhance HTTP transports with custom authentication, filters, circuit breakers, or advanced features.
  • Hub Components: Add new orchestration steps, validators, or business logic.

Development Workflow

  1. Create a Plugin Project: Use the provided SDK or follow the Plugin Connector Development guide.
  2. Implement Plugin Logic: Write your custom connector or component, specifying dependencies as needed.
  3. Package the Plugin: Build a JAR (or other supported format) containing your code and resources.
  4. Test Locally: Use the Hub’s testing tools or a local instance to validate your plugin.

Example: Adding a New Hub Component as a Plugin

Let’s walk through creating a custom Hub component as a plugin using com.zenoo.hub.plugin.sdk.ComponentFactory.

1. Implement the Plugin Component

Create a Groovy class that implements ComponentFactory and uses the ComponentBuilder DSL to define your component logic:
package com.zenoo.plugin

import com.zenoo.hub.plugin.sdk.ComponentBuilder
import com.zenoo.hub.plugin.sdk.ComponentFactory
import org.osgi.service.component.annotations.Component

@Component
class PluginComponent implements ComponentFactory {
    @Override
    ComponentBuilder define() {
        return ComponentBuilder.component {
            dependencies {
                connector 'plugin'
            }
            exposed('test') { payload ->
                exchange('plugin') {
                    connector 'plugin'
                    config payload
                }
            }
        }
    }
}
  • The @Component annotation registers this class as an OSGi component.
  • The define() method returns a ComponentBuilder using a Groovy DSL to declare dependencies and exposed operations.
  • The exposed('test') block defines an entry point named test for this component.

2. Package the Plugin

  • Ensure your build includes the necessary dependencies and OSGi metadata.
  • Build your project as a JAR file (a fat/uber JAR if needed).

3. Deploy the Plugin

  • Place the JAR in the Hub’s plugin directory or deploy it using the Hub’s deployment mechanism.
  • The Hub will detect and load your new component, making it available in workflow definitions as test (or the name you expose).
You can now use your custom component in Hub workflows just like any built-in component.

Example: Creating a Transport Customizer Plugin

Transport customizers allow you to enhance HTTP transports defined in DSL with advanced features while preserving the base configuration (baseUrl, authentication). This follows the decorator pattern where customizers add functionality without replacing the underlying transport.

Use Cases for Transport Customizers

  • Advanced Authentication: OAuth2 flows, mTLS, API key rotation
  • Resilience Patterns: Circuit breakers, retry logic, fallback mechanisms
  • Custom Headers: Dynamic headers, correlation IDs, API versioning
  • Monitoring: Request/response logging, timing metrics, tracing
  • Content Transformation: Custom codecs, request/response interceptors

1. Create a Typed Configuration Class

Define a configuration class implementing PluginConfig with JSR-303 validation:
package com.zenoo.plugin;

import com.zenoo.hub.plugin.sdk.PluginConfig;
import jakarta.validation.constraints.NotNull;
import lombok.Data;

@Data
public class HttpTransportConfig implements PluginConfig {

    @NotNull(message = "Custom header name is required")
    private String customHeaderName = "X-Custom-Header";

    @NotNull(message = "Custom header value is required")
    private String customHeaderValue = "default-value";

    private Boolean enableDetailedLogging = false;
}
Key Points:
  • Use @Data from Lombok for automatic getters/setters
  • Add JSR-303 validation annotations (@NotNull, @Min, @Max, etc.)
  • Provide sensible defaults for optional fields
  • No need for Jackson annotations (Hub handles mapping automatically)

2. Implement the Transport Customizer

Create a class implementing TransportCustomizer and register it as an OSGi component:
package com.zenoo.plugin;

import com.zenoo.hub.plugin.sdk.TransportCustomizer;
import com.zenoo.hub.plugin.sdk.PluginConfig;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.util.UUID;

@Component
public class CustomHttpTransportCustomizer implements TransportCustomizer {

    private static final Logger log = LoggerFactory.getLogger(CustomHttpTransportCustomizer.class);

    @Override
    public String name() {
        return "custom-http";
    }

    @Override
    public Class<? extends PluginConfig> configType() {
        return HttpTransportConfig.class;
    }

    @Override
    public WebClient customize(WebClient baseClient, PluginConfig config) {
        if (config == null) {
            log.debug("No configuration provided, returning base client unchanged");
            return baseClient;
        }

        HttpTransportConfig httpConfig = (HttpTransportConfig) config;
        log.info("Customizing HTTP transport with config: {}", httpConfig);

        // Use mutate() to ENHANCE base client (preserves DSL baseUrl & auth)
        WebClient.Builder builder = baseClient.mutate();

        // Add custom headers filter
        ExchangeFilterFunction customHeadersFilter = ExchangeFilterFunction.ofRequestProcessor(request -> {
            ClientRequest modifiedRequest = ClientRequest.from(request)
                    .header(httpConfig.getCustomHeaderName(), httpConfig.getCustomHeaderValue())
                    .header("X-API-Version", "1.0")
                    .header("X-Correlation-ID", UUID.randomUUID().toString())
                    .build();
            return Mono.just(modifiedRequest);
        });
        builder.filter(customHeadersFilter);

        // Add timing filter
        ExchangeFilterFunction timingFilter = (request, next) -> {
            long startTime = System.currentTimeMillis();
            return next.exchange(request).doOnSuccess(response -> {
                long duration = System.currentTimeMillis() - startTime;
                log.info("Request to {} took {}ms", request.url(), duration);
            });
        };
        builder.filter(timingFilter);

        // Conditional logging based on config
        if (Boolean.TRUE.equals(httpConfig.getEnableDetailedLogging())) {
            builder.filter(ExchangeFilterFunction.ofRequestProcessor(request -> {
                log.debug("Outgoing request: {} {}", request.method(), request.url());
                return Mono.just(request);
            }));
        }

        return builder.build();
    }
}
Key Points:
  • name(): Returns the unique identifier for this customizer within the plugin
  • configType(): Returns the configuration class type for automatic mapping and validation
  • customize(): Receives the pre-configured baseClient (with DSL baseUrl & auth) and your typed config
  • Always use baseClient.mutate() to preserve DSL settings while adding enhancements
  • Return the unmodified baseClient if no customization is needed

3. Using Transport Customizers in DSL

Define the transport in your DSL with base configuration, then reference the customizer:
// Define DSL transport with baseUrl and authentication
transport('custom-http') {
    baseUrl 'https://api.example.com'
    bearer {
        clientId 'my-client'
        clientSecret 'secret'
        tokenUri 'https://auth.example.com/token'
    }
}

// Component configuration for the customizer
config {
    customHeaderName 'X-API-Key'
    customHeaderValue config('api.key')
    enableDetailedLogging true
}

// Use in workflow - gets DSL baseUrl + auth + custom enhancements
workflow('my-workflow') {
    exchange('api-call') {
        http {
            transport 'custom-http'  // References customizer by name
            url '/api/endpoint'
            method 'POST'
            body([data: 'value'])
        }
    }
}
How It Works:
  1. Hub creates base WebClient from DSL transport (baseUrl + bearer auth)
  2. Hub looks up customizer by name 'custom-http'
  3. Hub maps component config to HttpTransportConfig and validates it
  4. Hub calls customizer.customize(baseClient, typedConfig)
  5. Your customizer adds filters using mutate()
  6. Enhanced WebClient is used for HTTP requests

4. Build Configuration for OSGi

Configure your plugin’s build.gradle to exclude framework packages from OSGi imports:
plugins {
    id 'java-library'
    id 'biz.aQute.bnd.builder' version '7.1.0'
}

dependencies {
    implementation project(':component-sdk')
    annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
}

jar {
    manifest {
        attributes(
            'Bundle-SymbolicName': 'my-plugin',
            'Bundle-Version': '1.0.0',
            'Service-Component': '*',
            'Import-Package': '!groovy.*,!org.springframework.*,!reactor.*,*'
        )
    }
}
Key Points:
  • Exclude Spring and Reactor from Import-Package (they’re loaded via boot delegation)
  • 'Service-Component': '*' enables OSGi Declarative Services
  • The bnd.builder plugin generates proper OSGi metadata

5. Karaf Configuration

Ensure your Karaf config.properties includes boot delegation for framework classes:
org.osgi.framework.bootdelegation=org.codehaus.groovy.*,groovy.*,org.springframework.*,reactor.*
This allows plugins to use Spring and Reactor classes from the Hub’s classpath without explicit OSGi wiring.

Deployment Process

  • Deploy to Hub: Place the plugin artifact in the designated plugins directory or use the Hub’s deployment API.
  • Activation: The Hub detects new plugins and loads them at runtime, making new components available for use in workflows.
  • Hot Reload (if supported): Some environments allow plugins to be updated without restarting the Hub.

Versioning and Isolation

  • Each plugin is versioned independently, allowing safe upgrades and rollbacks.
  • Plugins run in isolated classloaders to avoid dependency conflicts.

Best Practices

General Plugin Development

  • Keep plugins focused and single-purpose.
  • Document plugin APIs and configuration options.
  • Use semantic versioning for plugin releases.
  • Test plugins thoroughly before deployment.
  • Monitor plugin performance and errors using Hub’s observability tools.

Transport Customizer Best Practices

  • Always use baseClient.mutate() to preserve DSL configuration (baseUrl, auth)
  • Return unmodified baseClient when config is null or no customization needed
  • Use typed configuration with PluginConfig and JSR-303 validation for type safety
  • Avoid overriding DSL settings unless explicitly required by your use case
  • Be cautious with logging - avoid exposing sensitive data (credentials, tokens, PII)
  • Implement proper error handling with meaningful exceptions
  • Consider thread safety - the returned WebClient is cached and used concurrently
  • Add filters for cross-cutting concerns - logging, timing, custom headers
  • Test with various DSL transport configurations to ensure decorator pattern works correctly
  • Document which DSL settings your customizer depends on or modifies