This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Forage is a plugin extension for Apache Camel that provides opinionated bean factories for simplified component configuration. The library eliminates manual Java bean instantiation by providing factory classes configurable through properties files, environment variables, or system properties.
Technology Stack:
- Java 17+
- Apache Camel 4.x
- LangChain4j 1.x
- Maven, Spotless (Palantir Java Format), JUnit 5, AssertJ, Testcontainers, Citrus Test Framework
Forage uses major.minor.micro versioning (1.4.0, 1.4.1, 1.5.0, etc.).
| Branch | Tracks | Version | Description |
|---|---|---|---|
main |
Apache Camel LTS | 1.4.x |
Follows the latest Camel LTS release (currently 4.18.x). Micro bumps (1.4.0 → 1.4.1 → ...) for each release. |
camel-latest |
Latest Apache Camel | 1.5.x |
Follows the latest Camel release (currently 4.20.x) with corresponding Spring Boot and Quarkus versions. |
When making changes (features, bug fixes, etc.), always investigate whether the change needs backporting to the other branch:
- A fix on
mainmay also apply tocamel-latestand vice versa. - After completing work on one branch, check if the same change is relevant to the other branch and create a backport PR if needed.
- Use
/oss-backport-prto automate backporting when applicable.
# Full build with tests
mvn clean install
# Compile only (includes automatic code formatting)
mvn clean compile
# Apply code formatting manually
mvn spotless:apply
# Check code formatting
mvn spotless:check
# Run all tests
mvn verify
# Run a single test class
mvn test -Dtest=ClassName
# Run a single test method
mvn test -Dtest=ClassName#methodName
# Run integration tests for a specific module
mvn verify -f integration-tests/jdbc
# Run integration tests with specific runtime (plain, quarkus, spring-boot)
export INTEGRATION_TEST_RUNTIME=quarkus
mvn clean verify -f integration-tests/jdbc -Dit.test=JdbcTest
# Skip tests
mvn install -DskipTestsforage/
├── core/ # Core interfaces and utilities
│ ├── forage-core-ai/ # AI interfaces (ModelProvider, ChatMemoryFactory)
│ ├── forage-core-common/ # Config system (ConfigStore, ConfigModule, ConfigEntry, ConfigEntries, AbstractConfig)
│ ├── forage-core-vectordb/ # EmbeddingStoreProvider interface
│ ├── forage-core-jdbc/ # DataSourceProvider interface
│ ├── forage-core-jms/ # JMS interfaces
│ ├── forage-core-jta/ # JTA transaction interfaces
│ ├── forage-core-cloud/ # Cloud provider interfaces
│ └── forage-core-vertx/ # Vert.x interfaces
├── library/ # Implementation modules
│ ├── ai/ # AI implementations
│ │ ├── agents/ # forage-agent, forage-agent-factories
│ │ ├── chat-memory/ # Memory providers (message-window, infinispan, redis)
│ │ ├── models/chat/ # Model providers (openai, ollama, gemini, anthropic, etc.)
│ │ └── vector-dbs/ # Vector DB providers (qdrant, milvus, pgvector, etc.)
│ ├── jdbc/ # JDBC data source providers
│ ├── jms/ # JMS connection factories
│ ├── cloud/ # Cloud provider implementations
│ └── vertx/ # Vert.x implementations
├── integration-tests/ # Citrus-based integration tests
├── tests/plans/ # End-to-end test plans (Markdown)
│ ├── common/ # Shared procedures (container setup, forage-run)
│ ├── jdbc-datasource.md # JDBC DataSource provisioning
│ ├── jms-messaging.md # JMS ConnectionFactory provisioning
│ ├── cxf-soap-endpoints.md # CXF/SOAP endpoint provisioning
│ ├── rabbitmq-connection.md # Spring RabbitMQ provisioning
│ ├── property-validation.md # Property typo detection and --strict mode
│ ├── config-commands.md # camel forage config read/write
│ └── route-policies.md # Flip and schedule route policies
├── tooling/ # Build tooling
│ ├── camel-jbang-plugin-forage/ # Camel JBang plugin
│ └── forage-maven-catalog-plugin/ # Catalog generation plugin
├── forage-catalog/ # Generated catalog
└── docs/ # Documentation
Components are discovered via Java ServiceLoader:
io.kaoto.forage.core.ai.ModelProvider- Chat modelsio.kaoto.forage.core.ai.ChatMemoryBeanProvider- Memory providersio.kaoto.forage.core.ai.EmbeddingStoreProvider- Vector databases
All providers extend BeanProvider<T>:
public interface BeanProvider<T> {
default T create() { return create(null); }
T create(String id); // id enables named/prefixed configurations
}@ForageBean - Required on all provider classes:
@ForageBean(value = "provider-name", components = {"camel-langchain4j-agent"}, description = "Description")
public class MyProvider implements ModelProvider { ... }@ForageFactory - Required on all factory classes:
@ForageFactory(value = "factory-name", components = {"camel-langchain4j-agent"},
description = "Description", type = FactoryType.AGENT)
public class MyFactory implements AgentFactory { ... }Each module requires two configuration classes:
ConfigEntries class - Defines configuration modules using the central registry in the ConfigEntries base class. Subclasses contain only field declarations and a static { initModules(...) } block — no methods:
public final class ExampleConfigEntries extends ConfigEntries {
public static final ConfigModule API_KEY = ConfigModule.of(ExampleConfig.class, "forage.example.api.key");
public static final ConfigModule MODEL_NAME = ConfigModule.of(ExampleConfig.class, "forage.example.model.name",
"Model name", "Model Name", "default-model", "string", true, ConfigTag.COMMON);
static {
initModules(ExampleConfigEntries.class, API_KEY, MODEL_NAME);
}
}Callers use ConfigEntries base class methods directly: ConfigEntries.entriesOf(ExampleConfigEntries.class), ConfigEntries.registerPrefix(ExampleConfigEntries.class, prefix), ConfigEntries.find(ConfigEntries.getModules(ExampleConfigEntries.class), prefix, name), ConfigEntries.loadOverridesFor(ExampleConfigEntries.class, prefix).
Config class - Extends AbstractConfig, which handles constructor boilerplate (prefix registration, properties loading, overrides) and provides get(ConfigModule) / getRequired(ConfigModule, String) helpers:
public class ExampleConfig extends AbstractConfig {
public ExampleConfig() { this(null); }
public ExampleConfig(String prefix) {
super(prefix, ExampleConfigEntries.class);
}
@Override public String name() { return "forage-module-example"; }
public String apiKey() {
return getRequired(API_KEY, "Missing API key");
}
public String modelName() {
return get(MODEL_NAME).orElse(MODEL_NAME.defaultValue());
}
}Key base class infrastructure:
ConfigEntries.initModules(Class, ConfigModule...)— registers modules in a central registry (replaces per-classCONFIG_MODULESmap andinit()method)ConfigEntries.entriesOf/getModules/registerPrefix/loadOverridesFor/find— public base class helpers called directly by callers (no subclass delegation)AbstractConfig— handles the 3-step constructor pattern (register prefix → load properties → load overrides), providesget(),getRequired(), and auto-implementsregister(String, String)AbstractConfig.ensureInitialized()— forces ConfigEntries subclass static initialization when only aClassliteral is passed
Configuration precedence (highest to lowest):
- Environment variables (
FORAGE_EXAMPLE_API_KEY) - System properties (
-Dforage.example.api.key=value) - Properties files (
<module-name>.properties)
Create META-INF/services/<interface-name> files listing implementation classes.
- Artifacts:
forage-<category>-<technology>(e.g.,forage-model-open-ai) - Packages:
io.kaoto.forage.<category>.<technology> - Config env vars:
FORAGE_<TECHNOLOGY>_<PROPERTY>(e.g.,FORAGE_OPENAI_API_KEY) - Config properties:
forage.<technology>.<property>(e.g.,forage.openai.api.key)
Tests use Citrus Test Framework with custom Forage actions. Tests run against three runtimes: plain Camel, Quarkus, and Spring Boot.
@CitrusSupport
@ExtendWith(IntegrationTestSetupExtension.class)
public class MyTest implements ForageIntegrationTest {
@Test
void testRoute(ForageTestCaseRunner runner) {
runner.when(forageRun("process-name", "config.properties", "route.camel.yaml")
.dumpIntegrationOutput(true)); // Enable logs
}
}See docs/adding-modules.md for the complete guide on adding new Forage modules with support for plain Camel, Spring Boot, and Quarkus runtimes. The guide covers:
- Configuration two-class pattern (
ConfigEntries+AbstractConfig) - Provider implementation with
@ForageBean ForageModuleDescriptorfor runtime adapters- Spring Boot auto-configuration and bean registration
- Quarkus
ConfigSourceFactoryand deployment processors - Integration testing with Citrus
End-to-end test plans live in tests/plans/. Each plan creates a sample project (properties + YAML route), runs it via camel run or camel forage run, and verifies behavior. They test Forage as a user would use it — not by wrapping Maven unit tests.
# Install the Forage JBang plugin (required once)
mvn clean install -DskipTests
FORAGE_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
camel plugin add forage --gav io.kaoto.forage:camel-jbang-plugin-forage:${FORAGE_VERSION}
# Then follow the phases in any test planPlans that require containers (JDBC, JMS, RabbitMQ) use ${CONTAINER_RUNTIME} for Docker/Podman compatibility. Plans without containers (CXF, property validation, route policies, config commands) need only Java and Camel JBang.
See docs/contributing-test-plans.md for the guide on writing new test plans.
- Code formatting is applied automatically during compile phase via Spotless
- All provider classes must have
@ForageBeanannotation - All factory classes must have
@ForageFactoryannotation - Config classes must extend
AbstractConfig(not implementConfigdirectly) - ConfigEntries classes must use
initModules()in their static block — no delegator methods (callers useConfigEntriesbase class methods directly) - Use
getRequired(MODULE, "error message")for required config,get(MODULE)for optional config - Use
MissingConfigExceptionfor required missing configuration (orgetRequired()which wraps it) - Properties files named
<module-name>.propertiesin resources - Special cases:
FlipRoutePolicyConfigandScheduleRoutePolicyConfigextendAbstractConfigand follow the standard configuration pattern. Their ConfigEntries classes (FlipRoutePolicyConfigEntries,ScheduleRoutePolicyConfigEntries) use a dynamic ConfigModule pattern with factory methods (e.g.,pairedRoute(prefix)) instead of static fields +initModules(), because property names are route-ID-dependent.