use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use camel_api::CamelError;
use camel_api::datasource::DatasourceCatalog;
use camel_config::config::CamelConfig;
use camel_core::CamelContext;
use camel_core::datasource::RuntimeDatasourceCatalog;
pub mod security_boot;
#[cfg(feature = "security")]
pub use security_boot::build_security_compile_context_from_config;
struct BridgeCleanup {
xslt: Arc<camel_xslt::XsltBridgeRuntime>,
xj: Arc<camel_xj::XjBridgeRuntime>,
validator: Option<Arc<camel_component_validator::xsd_bridge::XsdBridgeBackend>>,
}
#[async_trait::async_trait]
impl camel_api::lifecycle::Lifecycle for BridgeCleanup {
fn name(&self) -> &str {
"bridge-cleanup"
}
async fn start(&mut self) -> Result<(), camel_api::CamelError> {
Ok(())
}
async fn stop(&mut self) -> Result<(), camel_api::CamelError> {
self.xslt.shutdown().await;
self.xj.shutdown().await;
if let Some(validator) = &self.validator {
validator.shutdown().await;
}
Ok(())
}
}
const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30);
pub struct BootHandle {
jms_pool: Arc<camel_component_jms::JmsBridgePool>,
cxf_pool: Arc<camel_component_cxf::CxfBridgePool>,
datasource_catalog: Arc<dyn DatasourceCatalog>,
}
impl BootHandle {
pub fn datasource_catalog(&self) -> Arc<dyn DatasourceCatalog> {
Arc::clone(&self.datasource_catalog)
}
pub async fn shutdown(&self, ctx: &mut CamelContext) -> Result<(), CamelError> {
self.shutdown_with_deadline(ctx, DEFAULT_SHUTDOWN_TIMEOUT)
.await
}
pub async fn shutdown_with_deadline(
&self,
ctx: &mut CamelContext,
deadline: Duration,
) -> Result<(), CamelError> {
self.jms_pool.begin_shutdown();
self.cxf_pool.begin_shutdown();
let mut failure = ctx.stop().await.err();
if let Some(e) = &failure {
tracing::error!("Error during shutdown: {}", e);
}
match tokio::time::timeout(deadline, self.jms_pool.shutdown()).await {
Ok(Ok(())) => {}
Ok(Err(e)) => {
tracing::error!("JMS pool shutdown failed: {}", e);
if failure.is_none() {
failure = Some(e);
}
}
Err(_) => tracing::warn!("JMS pool shutdown timed out after {}s", deadline.as_secs()),
}
match tokio::time::timeout(deadline, self.cxf_pool.shutdown()).await {
Ok(Ok(())) => {}
Ok(Err(e)) => {
tracing::error!("CXF pool shutdown failed: {}", e);
if failure.is_none() {
failure = Some(e);
}
}
Err(_) => tracing::warn!("CXF pool shutdown timed out after {}s", deadline.as_secs()),
}
match tokio::time::timeout(deadline, self.datasource_catalog.close_all()).await {
Ok(Ok(())) => {}
Ok(Err(e)) => {
tracing::error!("datasource pool close failed: {}", e);
if failure.is_none() {
failure = Some(e);
}
}
Err(_) => tracing::warn!(
"datasource pool close timed out after {}s",
deadline.as_secs()
),
}
match failure {
Some(e) => Err(e),
None => Ok(()),
}
}
}
pub fn bundle_from_config<B: camel_component_api::ComponentBundle>(
config: &CamelConfig,
) -> Result<B, CamelError> {
let raw = config
.components
.raw
.get(<B as camel_component_api::ComponentBundle>::config_key())
.cloned()
.unwrap_or_else(|| toml::Value::Table(toml::map::Map::new()));
<B as camel_component_api::ComponentBundle>::from_toml(raw)
}
pub fn register_bundle<B: camel_component_api::ComponentBundle>(
ctx: &mut CamelContext,
config: &CamelConfig,
) -> Result<(), CamelError> {
register_bundle_with::<B, ()>(ctx, config, |_: &B| ())
}
pub fn register_bundle_with<B, T>(
ctx: &mut CamelContext,
config: &CamelConfig,
extract: impl FnOnce(&B) -> T,
) -> Result<T, CamelError>
where
B: camel_component_api::ComponentBundle,
{
match bundle_from_config::<B>(config) {
Ok(bundle) => {
let extracted = extract(&bundle);
<B as camel_component_api::ComponentBundle>::register_all(bundle, ctx);
Ok(extracted)
}
Err(e) => Err(camel_api::CamelError::Config(format!(
"Failed to load {} config: {}",
<B as camel_component_api::ComponentBundle>::config_key(),
e
))),
}
}
pub async fn boot(
ctx: &mut CamelContext,
config: &CamelConfig,
project_root: &Path,
) -> Result<BootHandle, CamelError> {
let _ = project_root;
let datasource_catalog: Arc<dyn DatasourceCatalog> = {
let catalog = RuntimeDatasourceCatalog::new(config.datasources.clone())
.with_health_registry(ctx.health_registry());
Arc::new(catalog)
};
ctx.register_component(camel_component_timer::TimerComponent::new());
ctx.register_component(camel_component_cron::CronComponent::new());
ctx.register_component(camel_component_log::LogComponent::new());
ctx.register_component(camel_component_direct::DirectComponent::new());
ctx.register_component(camel_component_seda::SedaComponent::new());
ctx.register_component(camel_component_mock::MockComponent::new());
ctx.register_component(camel_component_controlbus::ControlBusComponent::new());
let validator_component = camel_component_validator::ValidatorComponent::new();
let validator_backend = validator_component.xsd_bridge_backend();
ctx.register_component(validator_component);
let xslt_component = camel_xslt::XsltComponent::default();
let xslt_runtime = xslt_component.bridge_runtime();
ctx.register_component(xslt_component);
let xj_component = camel_xj::XjComponent::default();
let xj_runtime = xj_component.bridge_runtime();
ctx.register_component(xj_component);
ctx.add_lifecycle(BridgeCleanup {
xslt: xslt_runtime,
xj: xj_runtime,
validator: validator_backend,
});
register_bundle::<camel_component_http::HttpBundle>(ctx, config)?;
#[cfg(feature = "http-static")]
register_bundle::<camel_component_http::HttpStaticBundle>(ctx, config)?;
register_bundle::<camel_component_ws::WsBundle>(ctx, config)?;
register_bundle::<camel_component_file::FileBundle>(ctx, config)?;
register_bundle::<camel_component_container::ContainerBundle>(ctx, config)?;
register_bundle::<camel_template::TemplateBundle>(ctx, config)?;
let jms_pool =
register_bundle_with(ctx, config, |b: &camel_component_jms::JmsBundle| b.pool())?;
let cxf_pool =
register_bundle_with(ctx, config, |b: &camel_component_cxf::CxfBundle| b.pool())?;
#[cfg(feature = "kafka")]
register_bundle::<camel_component_kafka::KafkaBundle>(ctx, config)?;
#[cfg(feature = "mqtt")]
register_bundle::<camel_component_mqtt::MqttBundle>(ctx, config)?;
register_bundle::<camel_master::MasterBundle>(ctx, config)?;
register_bundle::<camel_component_opensearch::OpenSearchBundle>(ctx, config)?;
register_bundle::<camel_component_redis::RedisBundle>(ctx, config)?;
{
match bundle_from_config::<camel_component_sql::SqlBundle>(config) {
Ok(bundle) => {
let bundle = bundle.with_catalog(Arc::clone(&datasource_catalog));
<camel_component_sql::SqlBundle as camel_component_api::ComponentBundle>::register_all(bundle, ctx);
}
Err(e) => {
tracing::error!("failed to initialize SQL bundle: {}", e);
}
}
}
#[cfg(feature = "surrealdb")]
{
match bundle_from_config::<camel_component_surrealdb::SurrealDbBundle>(config) {
Ok(bundle) => {
let bundle = bundle.with_catalog(Arc::clone(&datasource_catalog));
<camel_component_surrealdb::SurrealDbBundle as camel_component_api::ComponentBundle>::register_all(
bundle, ctx,
);
}
Err(e) => {
tracing::error!("failed to initialize SurrealDB bundle: {}", e);
}
}
}
#[cfg(feature = "grpc")]
register_bundle::<camel_component_grpc::GrpcBundle>(ctx, config)?;
#[cfg(feature = "llm")]
register_bundle::<camel_component_llm::LlmBundle>(ctx, config)?;
#[cfg(feature = "mcp")]
register_bundle::<camel_component_mcp::McpBundle>(ctx, config)?;
#[cfg(feature = "wasm")]
{
let wasm_bundle = camel_component_wasm::WasmBundle::new(
Arc::new(camel_core::RegistryComponentContext::new(
ctx.registry_arc(),
Some(ctx.metrics()),
camel_component_api::ComponentContext::component_metrics_enabled(&*ctx),
)),
project_root.to_path_buf(),
);
<camel_component_wasm::WasmBundle as camel_component_api::ComponentBundle>::register_all(
wasm_bundle,
ctx,
);
}
Ok(BootHandle {
jms_pool,
cxf_pool,
datasource_catalog: Arc::clone(&datasource_catalog),
})
}
#[cfg(test)]
mod tests {
use super::*;
use camel_core::{BuilderStep, RouteDefinition};
fn fixture(rel: &str) -> String {
format!("{}/tests/fixtures/{rel}", env!("CARGO_MANIFEST_DIR"))
}
async fn booted_context(fixture_rel: &str) -> (CamelContext, BootHandle, CamelConfig) {
let path = fixture(fixture_rel);
let config =
CamelConfig::from_file(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"));
let mut ctx = CamelConfig::configure_context_with_beans(&config, None)
.await
.expect("configure_context_with_beans must succeed");
let handle = boot(&mut ctx, &config, Path::new(env!("CARGO_MANIFEST_DIR")))
.await
.expect("boot must register the cascade");
(ctx, handle, config)
}
#[tokio::test]
async fn boot_registers_all_bundles_from_fixture_config() {
let (ctx, _handle, _config) = booted_context("bundles-present/Camel.toml").await;
for scheme in [
"http",
"https",
"ws",
"file",
"container",
"template",
"jms",
] {
assert!(
ctx.registry().get(scheme).is_some(),
"scheme '{scheme}' must resolve after boot"
);
}
}
#[cfg(not(feature = "kafka"))]
#[tokio::test]
async fn boot_feature_gating_matches_flags() {
let (ctx, _handle, _config) = booted_context("bundles-present/Camel.toml").await;
assert!(ctx.registry().get("kafka").is_none());
let def = RouteDefinition::new(
"direct:kafka-gate-probe",
vec![BuilderStep::To("kafka:orders".to_string())],
)
.with_route_id("kafka-gate-probe");
let err = ctx
.add_route_definition(def)
.await
.expect_err("kafka send step must not resolve without the kafka feature");
match err {
CamelError::ComponentNotFound(name) => assert_eq!(name, "kafka"),
CamelError::RouteError(msg) => assert!(
msg.contains("Component not found: kafka"),
"error must name the kafka component: {msg}"
),
other => panic!("expected ComponentNotFound, got {other:?}"),
}
}
#[cfg(feature = "kafka")]
#[tokio::test]
async fn boot_feature_gating_matches_flags_kafka_enabled() {
let (ctx, _handle, _config) = booted_context("bundles-present/Camel.toml").await;
assert!(
ctx.registry().get("kafka").is_some(),
"kafka must resolve with the kafka feature enabled"
);
}
#[tokio::test]
async fn boot_handle_exposes_datasource_catalog() {
use camel_api::datasource::DatasourceConfig;
use std::collections::HashMap;
let mut datasources = HashMap::new();
datasources.insert(
"appdb".to_string(),
DatasourceConfig {
db_url: "sqlite:file:memdb_bundles_catalog_probe?mode=memory&cache=shared"
.to_string(),
provider: Some("sqlx".to_string()),
max_connections: None,
min_connections: None,
idle_timeout_secs: None,
max_lifetime_secs: None,
ssl_mode: None,
ssl_root_cert: None,
ssl_cert: None,
ssl_key: None,
extra: HashMap::new(),
},
);
let config = CamelConfig {
datasources,
..CamelConfig::default()
};
let mut ctx = CamelConfig::configure_context_with_beans(&config, None)
.await
.expect("configure_context_with_beans must succeed");
let handle = boot(&mut ctx, &config, Path::new(env!("CARGO_MANIFEST_DIR")))
.await
.expect("boot must register the cascade");
let appdb = handle
.datasource_catalog()
.get_config("appdb")
.expect("booted handle must expose the appdb datasource");
assert_eq!(
appdb.db_url,
"sqlite:file:memdb_bundles_catalog_probe?mode=memory&cache=shared"
);
}
#[tokio::test]
async fn boot_missing_config_key_falls_back_to_bundle_defaults() {
let (ctx, _handle, config) = booted_context("no-http/Camel.toml").await;
assert!(
!config.components.raw.contains_key("http"),
"fixture must not carry an [components.http] table"
);
assert!(
ctx.registry().get("http").is_some(),
"http must resolve without an [components.http] table"
);
assert!(ctx.registry().get("https").is_some());
}
#[tokio::test]
async fn register_bundle_registers_one_bundle_without_boot() {
let path = fixture("no-http/Camel.toml");
let config =
CamelConfig::from_file(&path).unwrap_or_else(|e| panic!("fixture {path}: {e}"));
let mut ctx = CamelConfig::configure_context_with_beans(&config, None)
.await
.expect("configure_context_with_beans must succeed");
register_bundle::<camel_component_http::HttpBundle>(&mut ctx, &config)
.expect("single-bundle registration must succeed");
assert!(ctx.registry().get("http").is_some());
assert!(ctx.registry().get("timer").is_none());
}
}