This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karaf.git
The following commit(s) were added to refs/heads/main by this push:
new d54f9a806 fix(#625): Invalidate OsgiTypeConverter delegate when new
TypeConverterLoader arrives (#684)
d54f9a806 is described below
commit d54f9a806cee9c2323e4b341680f7608915959f7
Author: JB Onofré <[email protected]>
AuthorDate: Thu Mar 12 19:53:30 2026 +0100
fix(#625): Invalidate OsgiTypeConverter delegate when new
TypeConverterLoader arrives (#684)
* fix(#625): Invalidate OsgiTypeConverter delegate when new
TypeConverterLoader arrives
The delegate was not rebuilt when a new TypeConverterLoader service was
registered, causing HTTP TypeConverters to be missing when endpoints were
eagerly created via to(). This worked with toD() because endpoints are
lazily created after all bundles and their TypeConverters are loaded.
Now the delegate is invalidated (set to null) on addingService, matching
the existing pattern in removedService, so it is rebuilt with all
available loaders on next access.
* fix(#625): change camel-core-model dependency scope from test to provided
KarafCamelContextProvider uses ModelCamelContext and RouteDefinition in
main source code, so the dependency must be available at compile time.
* fix(#625): open ServiceTracker early so TypeConverterLoaders are
available during doInit
The OsgiTypeConverter's ServiceTracker was only opened in doStart(),
but endpoints created eagerly via to() during doInit() need type
converters (e.g. String to Timeout for camel-http). The tracker was
closed at that point, so createRegistry() found no OSGi-loaded
TypeConverterLoaders.
Open the tracker lazily via ensureTrackerOpen() in getDelegate() so
that already-registered TypeConverterLoader services are discoverable
when the delegate is first created. Also revert addingService() to
load into the existing delegate instead of invalidating it, which
preserves programmatically-added converters (e.g. Blueprint beans
implementing TypeConverters).
Add responseTimeout to the camel-http integration test to cover
the exact scenario from #625.
---
core/camel-core-osgi/pom.xml | 18 ++++
.../apache/camel/karaf/core/OsgiTypeConverter.java | 20 +++-
.../camel/karaf/core/OsgiTypeConverterTest.java | 113 +++++++++++++++++++++
.../karaf/camel/test/CamelHttpRouteSupplier.java | 2 +-
4 files changed, 150 insertions(+), 3 deletions(-)
diff --git a/core/camel-core-osgi/pom.xml b/core/camel-core-osgi/pom.xml
index 04218721f..4c066eb1f 100644
--- a/core/camel-core-osgi/pom.xml
+++ b/core/camel-core-osgi/pom.xml
@@ -67,12 +67,24 @@
<version>${junit-jupiter-version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <version>${junit-jupiter-version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito-version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-junit-jupiter</artifactId>
+ <version>${mockito-version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-core</artifactId>
@@ -91,6 +103,12 @@
<version>${camel-version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-core-model</artifactId>
+ <version>${camel-version}</version>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
<build>
diff --git
a/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiTypeConverter.java
b/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiTypeConverter.java
index 258645467..2aa75037c 100644
---
a/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiTypeConverter.java
+++
b/core/camel-core-osgi/src/main/java/org/apache/camel/karaf/core/OsgiTypeConverter.java
@@ -58,6 +58,7 @@ public class OsgiTypeConverter extends ServiceSupport
implements TypeConverter,
private final Injector injector;
private final ServiceTracker<TypeConverterLoader, Object> tracker;
private volatile DefaultTypeConverter delegate;
+ private volatile boolean trackerOpened;
public OsgiTypeConverter(BundleContext bundleContext, CamelContext
camelContext, Injector injector) {
this.bundleContext = bundleContext;
@@ -66,6 +67,13 @@ public class OsgiTypeConverter extends ServiceSupport
implements TypeConverter,
this.tracker = new ServiceTracker<>(bundleContext,
TypeConverterLoader.class.getName(), this);
}
+ private void ensureTrackerOpen() {
+ if (!trackerOpened) {
+ tracker.open();
+ trackerOpened = true;
+ }
+ }
+
@Override
public Object addingService(ServiceReference<TypeConverterLoader>
serviceReference) {
LOG.trace("AddingService: {}, Bundle: {}", serviceReference,
serviceReference.getBundle());
@@ -74,7 +82,9 @@ public class OsgiTypeConverter extends ServiceSupport
implements TypeConverter,
try {
LOG.debug("loading type converter from bundle: {}",
serviceReference.getBundle().getSymbolicName());
if (delegate != null) {
- ServiceHelper.startService(this.delegate);
+ // load the converter directly into the existing delegate
to preserve
+ // any converters that were added programmatically (e.g.
via Blueprint beans
+ // implementing TypeConverters)
loader.load(delegate);
}
} catch (Throwable t) {
@@ -104,12 +114,13 @@ public class OsgiTypeConverter extends ServiceSupport
implements TypeConverter,
@Override
protected void doStart() throws Exception {
- this.tracker.open();
+ ensureTrackerOpen();
}
@Override
protected void doStop() throws Exception {
this.tracker.close();
+ this.trackerOpened = false;
ServiceHelper.stopService(this.delegate);
this.delegate = null;
}
@@ -232,6 +243,11 @@ public class OsgiTypeConverter extends ServiceSupport
implements TypeConverter,
public DefaultTypeConverter getDelegate() {
if (delegate == null) {
+ // ensure the tracker is open so we can discover
TypeConverterLoader services
+ // before creating the registry - this is important because
getDelegate() may be
+ // called during doInit() (e.g. when to() eagerly creates
endpoints) which happens
+ // before doStart() where the tracker is normally opened
+ ensureTrackerOpen();
delegate = createRegistry();
}
return delegate;
diff --git
a/core/camel-core-osgi/src/test/java/org/apache/camel/karaf/core/OsgiTypeConverterTest.java
b/core/camel-core-osgi/src/test/java/org/apache/camel/karaf/core/OsgiTypeConverterTest.java
new file mode 100644
index 000000000..838193400
--- /dev/null
+++
b/core/camel-core-osgi/src/test/java/org/apache/camel/karaf/core/OsgiTypeConverterTest.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.karaf.core;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.Injector;
+import org.apache.camel.spi.TypeConverterLoader;
+import org.apache.camel.spi.TypeConverterRegistry;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class OsgiTypeConverterTest {
+
+ @Mock
+ private BundleContext bundleContext;
+ @Mock
+ private CamelContext camelContext;
+ @Mock
+ private Injector injector;
+ @Mock
+ private ServiceReference<TypeConverterLoader> serviceReference;
+ @Mock
+ private TypeConverterLoader loader;
+ @Mock
+ private Bundle bundle;
+
+ private OsgiTypeConverter osgiTypeConverter;
+
+ @BeforeEach
+ void setUp() {
+
lenient().when(bundleContext.getService(serviceReference)).thenReturn(loader);
+ lenient().when(serviceReference.getBundle()).thenReturn(bundle);
+ lenient().when(bundle.getSymbolicName()).thenReturn("test-bundle");
+ osgiTypeConverter = new OsgiTypeConverter(bundleContext, camelContext,
injector);
+ }
+
+ @Test
+ void addingServiceShouldLoadIntoExistingDelegate() throws Exception {
+ // trigger delegate creation
+ var delegate = osgiTypeConverter.getDelegate();
+ assertNotNull(delegate, "delegate should be created");
+
+ // simulate a new TypeConverterLoader service arriving
+ osgiTypeConverter.addingService(serviceReference);
+
+ // the loader should have been loaded into the existing delegate
+ verify(loader).load(delegate);
+
+ // the delegate should be the same instance (not invalidated)
+ assertSame(delegate, osgiTypeConverter.getDelegate(),
+ "delegate should be preserved when a new loader arrives");
+ }
+
+ @Test
+ void addingServiceShouldNotFailWhenDelegateIsNull() throws Exception {
+ // delegate is null initially, adding a service should not fail
+ // and should not attempt to load (no delegate to load into)
+ osgiTypeConverter.addingService(serviceReference);
+
+ verify(loader, never()).load(any());
+
+ // delegate should still be lazily created on next access
+ assertNotNull(osgiTypeConverter.getDelegate());
+ }
+
+ @Test
+ void newDelegateIncludesLateArrivingLoader() throws Exception {
+ // simulate a loader arriving before delegate is created
+ osgiTypeConverter.addingService(serviceReference);
+
+ // when delegate is created, it should pick up the loader
+ // via tracker.getServiceReferences() in createRegistry()
+ var delegate = osgiTypeConverter.getDelegate();
+ assertNotNull(delegate);
+ }
+
+ @Test
+ void removedServiceShouldInvalidateDelegate() throws Exception {
+ // trigger delegate creation
+ osgiTypeConverter.getDelegate();
+
+ // simulate service removal
+ osgiTypeConverter.removedService(serviceReference, loader);
+
+ // delegate should be rebuilt on next access
+ var delegateAfter = osgiTypeConverter.getDelegate();
+ assertNotNull(delegateAfter);
+ }
+}
diff --git
a/tests/features/camel-http/src/main/java/org/apache/karaf/camel/test/CamelHttpRouteSupplier.java
b/tests/features/camel-http/src/main/java/org/apache/karaf/camel/test/CamelHttpRouteSupplier.java
index 28422e972..535323ab6 100644
---
a/tests/features/camel-http/src/main/java/org/apache/karaf/camel/test/CamelHttpRouteSupplier.java
+++
b/tests/features/camel-http/src/main/java/org/apache/karaf/camel/test/CamelHttpRouteSupplier.java
@@ -40,7 +40,7 @@ public class CamelHttpRouteSupplier extends
AbstractCamelSingleFeatureResultMock
configureConsumer(
producerRoute.log("calling local http server")
.setHeader("CamelHttpMethod", constant("GET"))
- .toF("http://localhost:%s",
System.getProperty("http.port"))
+ .toF("http://localhost:%s?responseTimeout=55000",
System.getProperty("http.port"))
.log("got ${body}"));
}
}