jhattab commented on code in PR #651:
URL: https://github.com/apache/camel-karaf/pull/651#discussion_r2641139618


##########
components/camel-test/camel-test-blueprint-junit5/src/main/java/org/apache/camel/test/blueprint/CamelBlueprintTestSupport.java:
##########
@@ -0,0 +1,757 @@
+/*
+ * 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.test.blueprint;
+
+import org.apache.aries.blueprint.compendium.cm.CmNamespaceHandler;
+import org.apache.camel.CamelContext;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.blueprint.CamelBlueprintHelper;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.component.properties.PropertiesComponent;
+import org.apache.camel.model.ModelCamelContext;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.support.builder.xml.XMLConverterHelper;
+import org.apache.camel.test.junit5.*;
+import org.apache.camel.test.junit5.util.CamelContextTestHelper;
+import org.apache.camel.test.junit5.util.ExtensionHelper;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.KeyValueHolder;
+import org.apache.camel.util.StopWatch;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
+import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.blueprint.container.BlueprintEvent;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.nio.file.Files;
+import java.util.*;
+import java.util.jar.JarFile;
+
+/**
+ * Base class for OSGi Blueprint unit tests with Camel
+ */
+public abstract class CamelBlueprintTestSupport extends AbstractTestSupport
+        implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(CamelBlueprintTestSupport.class);
+
+    /** Name of a system property that sets camel context creation timeout. */
+    public static final String SPROP_CAMEL_CONTEXT_CREATION_TIMEOUT = 
"org.apache.camel.test.blueprint.camelContextCreationTimeout";
+
+    private static ThreadLocal<BundleContext> threadLocalBundleContext = new 
ThreadLocal<>();
+    private volatile BundleContext bundleContext;
+    private final Set<ServiceRegistration<?>> services = new LinkedHashSet<>();
+
+    private final StopWatch watch = new StopWatch();
+
+    @RegisterExtension
+    @Order(1)
+    public final ContextManagerExtension contextManagerExtension;
+    private CamelContextManager contextManager;
+
+    protected CamelBlueprintTestSupport() {
+        super(new TestExecutionConfiguration(), new 
CamelBlueprintContextConfiguration());
+
+        configureTest(testConfigurationBuilder);
+        configureContext(camelContextConfiguration);
+        contextManagerExtension = new 
ContextManagerExtension(testConfigurationBuilder, camelContextConfiguration);
+    }
+
+    /**
+     * Override this method if you don't want CamelBlueprintTestSupport create 
the test bundle
+     * @return includeTestBundle
+     * If the return value is true CamelBlueprintTestSupport creates the test 
bundle which includes blueprint configuration files
+     * If the return value is false CamelBlueprintTestSupport won't create the 
test bundle
+     */
+    protected boolean includeTestBundle() {
+        return true;
+    }
+
+    /**
+     * <p>Override this method if you want to start Blueprint containers 
asynchronously using the thread
+     * that starts the bundles itself.
+     * By default this method returns <code>true</code> which means Blueprint 
Extender will use thread pool
+     * (threads named "<code>Blueprint Extender: N</code>") to startup 
Blueprint containers.</p>
+     * <p>Karaf and Fuse OSGi containers use synchronous startup.</p>
+     * <p>Asynchronous startup is more in the <em>spirit</em> of OSGi and 
usually means that if everything works fine
+     * asynchronously, it'll work synchronously as well. This isn't always 
true otherwise.</p>
+     * @return <code>true</code> when blueprint containers are to be started 
asynchronously, otherwise <code>false</code>.
+     */
+    protected boolean useAsynchronousBlueprintStartup() {
+        return true;
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    protected BundleContext createBundleContext() throws Exception {
+        System.setProperty("org.apache.aries.blueprint.synchronous", 
Boolean.toString(!useAsynchronousBlueprintStartup()));
+
+        // load configuration file
+        String[] file = loadConfigAdminConfigurationFile();
+        String[][] configAdminPidFiles = new String[0][0];
+        if (file != null) {
+            if (file.length % 2 != 0) {  // This needs to return pairs of 
filename and pid
+                throw new IllegalArgumentException("The length of the String[] 
returned from loadConfigAdminConfigurationFile must divisible by 2, was " + 
file.length);
+            }
+            configAdminPidFiles = new String[file.length / 2][2];
+
+            int pair = 0;
+            for (int i = 0; i < file.length; i += 2) {
+                String fileName = file[i];
+                String pid = file[i + 1];
+                if (!new File(fileName).exists()) {
+                    throw new IllegalArgumentException("The provided file \"" 
+ fileName + "\" from loadConfigAdminConfigurationFile doesn't exist");
+                }
+                configAdminPidFiles[pair][0] = fileName;
+                configAdminPidFiles[pair][1] = pid;
+                pair++;
+            }
+        }
+
+        // fetch initial configadmin configuration if provided programmatically
+        Properties initialConfiguration = new Properties();
+        String pid = setConfigAdminInitialConfiguration(initialConfiguration);
+        if (pid != null) {
+            configAdminPidFiles = new 
String[][]{{prepareInitialConfigFile(initialConfiguration), pid}};
+        }
+
+        final String symbolicName = getClass().getSimpleName();
+        final BundleContext answer = 
CamelBlueprintHelper.createBundleContext(symbolicName, getBlueprintDescriptor(),
+            includeTestBundle(), getBundleFilter(), getBundleVersion(), 
getBundleDirectives(), configAdminPidFiles);
+
+        boolean expectReload = 
expectBlueprintContainerReloadOnConfigAdminUpdate();
+
+        // must register override properties early in OSGi containers
+        var extra = useOverridePropertiesWithPropertiesComponent();
+        if (extra != null) {
+            answer.registerService(PropertiesComponent.OVERRIDE_PROPERTIES, 
extra, null);
+        }
+
+        Map<String, KeyValueHolder<Object, Dictionary>> map = new 
LinkedHashMap<>();
+        addServicesOnStartup(map);
+
+        List<KeyValueHolder<String, KeyValueHolder<Object, Dictionary>>> 
servicesList = new LinkedList<>();
+        for (Map.Entry<String, KeyValueHolder<Object, Dictionary>> entry : 
map.entrySet()) {
+            servicesList.add(asKeyValueService(entry.getKey(), 
entry.getValue().getKey(), entry.getValue().getValue()));
+        }
+
+        addServicesOnStartup(servicesList);
+
+        for (KeyValueHolder<String, KeyValueHolder<Object, Dictionary>> item : 
servicesList) {
+            String clazz = item.getKey();
+            Object service = item.getValue().getKey();
+            Dictionary dict = item.getValue().getValue();
+            LOG.debug("Registering service {} -> {}", clazz, service);
+            ServiceRegistration<?> reg = answer.registerService(clazz, 
service, dict);
+            if (reg != null) {
+                services.add(reg);
+            }
+        }
+
+        // if blueprint XML uses <cm:property-placeholder> (any 
update-strategy and any default properties)
+        // - 
org.apache.aries.blueprint.compendium.cm.ManagedObjectManager.register() is 
called
+        // - ManagedServiceUpdate is scheduled in felix.cm
+        // - 
org.apache.felix.cm.impl.ConfigurationImpl.setDynamicBundleLocation() is called
+        // - CM_LOCATION_CHANGED event is fired
+        // - if BP was already created, it's <cm:property-placeholder> 
receives the event and
+        // - 
org.apache.aries.blueprint.compendium.cm.CmPropertyPlaceholder.updated() is 
called,
+        //   but no BP reload occurs
+        // we will however wait for BP container of the test bundle to become 
CREATED for the first time
+        // each configadmin update *may* lead to reload of BP container, if it 
uses <cm:property-placeholder>
+        // with update-strategy="reload"
+
+        // we will gather timestamps of BP events. We don't want to be fooled 
but repeated events related
+        // to the same state of BP container
+        Set<Long> bpEvents = new HashSet<>();
+
+        CamelBlueprintHelper.waitForBlueprintContainer(bpEvents, answer, 
symbolicName, BlueprintEvent.CREATED, null);
+
+        // must reuse props as we can do both load from .cfg file and override 
afterwards
+        final Dictionary props = new Properties();
+
+        // allow end user to override properties
+        pid = useOverridePropertiesWithConfigAdmin(props);
+        if (pid != null) {
+            // we will update the configuration again
+            ConfigurationAdmin configAdmin = 
CamelBlueprintHelper.getOsgiService(answer, ConfigurationAdmin.class);
+            // passing null as second argument ties the configuration to 
correct bundle.
+            // using single-arg method causes:
+            // *ERROR* Cannot use configuration xxx.properties for 
[org.osgi.service.cm.ManagedService, id=N, bundle=N/jar:file:xyz.jar!/]: No 
visibility to configuration bound to felix-connect
+            final Configuration config = configAdmin.getConfiguration(pid, 
null);
+            if (config == null) {
+                throw new IllegalArgumentException("Cannot find configuration 
with pid " + pid + " in OSGi ConfigurationAdmin service.");
+            }
+            // lets merge configurations
+            Dictionary<String, Object> currentProperties = 
config.getProperties();
+            final Dictionary newProps = new Properties();
+            if (currentProperties == null) {
+                currentProperties = newProps;
+            }
+            for (Enumeration<String> ek = currentProperties.keys(); 
ek.hasMoreElements();) {
+                String k = ek.nextElement();
+                newProps.put(k, currentProperties.get(k));
+            }
+            for (String p : ((Properties) props).stringPropertyNames()) {
+                newProps.put(p, ((Properties) props).getProperty(p));
+            }
+
+            LOG.info("Updating ConfigAdmin {} by overriding properties {}", 
config, newProps);
+            if (expectReload) {
+                CamelBlueprintHelper.waitForBlueprintContainer(bpEvents, 
answer, symbolicName, BlueprintEvent.CREATED, new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            config.update(newProps);
+                        } catch (IOException e) {
+                            throw new RuntimeException(e.getMessage(), e);
+                        }
+                    }
+                });
+            } else {
+                config.update(newProps);
+            }
+        }
+
+        return answer;
+    }
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        System.setProperty("skipStartingCamelContext", "true");
+        System.setProperty("registerBlueprintCamelContextEager", "true");
+
+        if (isCreateCamelContextPerClass()) {
+            // test is per class, so only setup once (the first time)
+            boolean first = threadLocalBundleContext.get() == null;
+            if (first) {
+                threadLocalBundleContext.set(createBundleContext());
+            }
+            bundleContext = threadLocalBundleContext.get();
+        } else {
+            bundleContext = createBundleContext();
+        }
+
+        ExtensionHelper.hasUnsupported(getClass());
+
+        setupResources();
+
+        contextManager = contextManagerExtension.getContextManager();
+        contextManager.createCamelContext(this);
+        context = contextManager.context();
+
+
+
+        // only start timing after all the setup
+        watch.restart();
+
+        // we don't have to wait for BP container's OSGi service - we've 
already waited
+        // for BlueprintEvent.CREATED
+
+        // start context when we are ready
+        LOG.debug("Starting CamelContext: {}", context.getName());
+        if (isUseAdviceWith()) {
+            LOG.info("Skipping starting CamelContext as isUseAdviceWith is set 
to true.");
+        } else {
+            context.start();
+        }
+    }
+
+    /**
+     * Override this method to add services to be registered on startup.
+     * <p/>
+     * You can use the builder methods {@link #asService(Object, Dictionary)}, 
{@link #asService(Object, String, String)}
+     * to make it easy to add the services to the map.
+     */
+    protected void addServicesOnStartup(Map<String, KeyValueHolder<Object, 
Dictionary>> services) {
+        // noop
+    }
+
+    /**
+     * This method may be overriden to instruct BP test support that BP 
container will reloaded when
+     * Config Admin configuration is updated. By default, this is expected, 
when blueprint XML definition
+     * contains <code>&lt;cm:property-placeholder persistent-id="PID" 
update-strategy="reload"&gt;</code>
+     */
+    protected boolean expectBlueprintContainerReloadOnConfigAdminUpdate() {
+        boolean expectedReload = false;
+        DocumentBuilderFactory dbf = new 
XMLConverterHelper().createDocumentBuilderFactory();
+        try {
+            // cm-1.0 doesn't define update-strategy attribute
+            Set<String> cmNamesaces = new HashSet<>(Arrays.asList(
+                    CmNamespaceHandler.BLUEPRINT_CM_NAMESPACE_1_1,
+                    CmNamespaceHandler.BLUEPRINT_CM_NAMESPACE_1_2,
+                    CmNamespaceHandler.BLUEPRINT_CM_NAMESPACE_1_3
+            ));
+            for (URL descriptor : 
CamelBlueprintHelper.getBlueprintDescriptors(getBlueprintDescriptor())) {
+                DocumentBuilder db = dbf.newDocumentBuilder();
+                try (InputStream is = descriptor.openStream()) {
+                    Document doc = db.parse(is);
+                    NodeList nl = doc.getDocumentElement().getChildNodes();
+                    for (int i = 0; i < nl.getLength(); i++) {
+                        Node node = nl.item(i);
+                        if (node instanceof Element) {
+                            Element pp = (Element) node;
+                            if (cmNamesaces.contains(pp.getNamespaceURI())) {
+                                String us = pp.getAttribute("update-strategy");
+                                if (us != null && us.equals("reload")) {
+                                    expectedReload = true;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
+        return expectedReload;
+    }
+
+    /**
+     * Override this method to add services to be registered on startup.
+     * <p/>
+     * You can use the builder methods {@link #asKeyValueService(String, 
Object, Dictionary)}
+     * to make it easy to add the services to the List.
+     */
+    protected void addServicesOnStartup(List<KeyValueHolder<String, 
KeyValueHolder<Object, Dictionary>>> services) {
+        // noop
+    }
+
+    /**
+     * Creates a holder for the given service, which make it easier to use 
{@link #addServicesOnStartup(Map)}
+     */
+    protected KeyValueHolder<Object, Dictionary> asService(Object service, 
Dictionary dict) {
+        return new KeyValueHolder<>(service, dict);
+    }
+
+    /**
+     * Creates a holder for the given service, which make it easier to use 
{@link #addServicesOnStartup(List)}
+     */
+    protected KeyValueHolder<String, KeyValueHolder<Object, Dictionary>> 
asKeyValueService(String name, Object service, Dictionary dict) {
+        return new KeyValueHolder<>(name, new KeyValueHolder<>(service, dict));
+    }
+
+    /**
+     * Creates a holder for the given service, which make it easier to use 
{@link #addServicesOnStartup(Map)}
+     */
+    protected KeyValueHolder<Object, Dictionary> asService(Object service, 
String key, String value) {
+        Properties prop = new Properties();
+        if (key != null && value != null) {
+            prop.put(key, value);
+        }
+        return new KeyValueHolder<>(service, prop);
+    }
+
+    /**
+     * <p>Override this method to override config admin properties. Overriden 
properties will be passed to
+     * {@link Configuration#update(Dictionary)} and may or may not lead to 
reload of Blueprint container - this
+     * depends on <code>update-strategy="reload|none"</code> in 
<code>&lt;cm:property-placeholder&gt;</code></p>
+     * <p>This method should be used to simulate configuration update 
<strong>after</strong> Blueprint container
+     * is already initialized and started. Don't use this method to 
initialized ConfigAdmin configuration.</p>
+     *
+     * @param props properties where you add the properties to override
+     * @return the PID of the OSGi {@link ConfigurationAdmin} which are 
defined in the Blueprint XML file.
+     */
+    protected String useOverridePropertiesWithConfigAdmin(Dictionary<String, 
String> props) throws Exception {
+        return null;
+    }
+
+    /**
+     * Override this method and provide the name of the .cfg configuration 
file to use for
+     * ConfigAdmin service. Provided file will be used to initialize 
ConfigAdmin configuration before Blueprint
+     * container is loaded.
+     *
+     * @return the name of the path for the .cfg file to load, and the 
persistence-id of the property placeholder.
+     */
+    protected String[] loadConfigAdminConfigurationFile() {
+        return null;
+    }
+
+    /**
+     * Override this method as an alternative to {@link 
#loadConfigAdminConfigurationFile()} if there's a need
+     * to set initial ConfigAdmin configuration without using files.
+     *
+     * @param props always non-null. Tests may initialize ConfigAdmin 
configuration by returning PID.
+     * @return persistence-id of the property placeholder. If non-null, 
<code>props</code> will be used as
+     * initial ConfigAdmin configuration
+     */
+    protected String setConfigAdminInitialConfiguration(Properties props) {
+        return null;
+    }
+
+    @AfterEach
+    public void afterEach() throws Exception {
+        System.clearProperty("skipStartingCamelContext");
+        System.clearProperty("registerBlueprintCamelContextEager");
+
+        tearDown(new TestInfo() {
+            @Override
+            public String getDisplayName() {
+                return "";
+            }
+
+            @Override
+            public Set<String> getTags() {
+                return Set.of();
+            }
+
+            @Override
+            public Optional<Class<?>> getTestClass() {
+                return Optional.empty();
+            }
+
+            @Override
+            public Optional<Method> getTestMethod() {
+                return Optional.empty();
+            }
+        });
+
+        // unregister services
+        if (bundleContext != null) {
+            for (ServiceRegistration<?> reg : services) {
+                bundleContext.ungetService(reg.getReference());
+            }
+        }
+
+        // close bundle context
+        if (bundleContext != null) {
+            // remember bundles before closing
+            Bundle[] bundles = bundleContext.getBundles();
+            // close bundle context
+            CamelBlueprintHelper.disposeBundleContext(bundleContext);
+            // now close jar files from the bundles
+            closeBundleJArFile(bundles);
+        }
+    }
+
+    public final void tearDown(TestInfo testInfo) throws Exception {
+        long time = watch.taken();
+        LOG.debug("tearDown()");
+
+        if (contextManager != null) {
+            contextManager.dumpRouteCoverage(getClass(), 
testInfo.getDisplayName(), time);
+            String dump = CamelContextTestHelper.getRouteDump(getDumpRoute());
+            contextManager.dumpRoute(getClass(), testInfo.getDisplayName(), 
dump);
+        } else {
+            LOG.warn(
+                    "A context manager is required to dump the route coverage 
for the Camel context but it's not available (it's null). "
+                            + "It's likely that the test is misconfigured!");
+        }
+
+//        doPostTearDown();

Review Comment:
   Done



##########
components/camel-test/camel-test-blueprint-junit5/src/main/java/org/apache/camel/test/blueprint/CamelBlueprintTestSupport.java:
##########
@@ -0,0 +1,757 @@
+/*
+ * 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.test.blueprint;
+
+import org.apache.aries.blueprint.compendium.cm.CmNamespaceHandler;
+import org.apache.camel.CamelContext;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.blueprint.CamelBlueprintHelper;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.component.properties.PropertiesComponent;
+import org.apache.camel.model.ModelCamelContext;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.support.builder.xml.XMLConverterHelper;
+import org.apache.camel.test.junit5.*;
+import org.apache.camel.test.junit5.util.CamelContextTestHelper;
+import org.apache.camel.test.junit5.util.ExtensionHelper;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.KeyValueHolder;
+import org.apache.camel.util.StopWatch;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
+import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.blueprint.container.BlueprintEvent;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.nio.file.Files;
+import java.util.*;
+import java.util.jar.JarFile;
+
+/**
+ * Base class for OSGi Blueprint unit tests with Camel
+ */
+public abstract class CamelBlueprintTestSupport extends AbstractTestSupport
+        implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(CamelBlueprintTestSupport.class);
+
+    /** Name of a system property that sets camel context creation timeout. */
+    public static final String SPROP_CAMEL_CONTEXT_CREATION_TIMEOUT = 
"org.apache.camel.test.blueprint.camelContextCreationTimeout";
+
+    private static ThreadLocal<BundleContext> threadLocalBundleContext = new 
ThreadLocal<>();
+    private volatile BundleContext bundleContext;
+    private final Set<ServiceRegistration<?>> services = new LinkedHashSet<>();
+
+    private final StopWatch watch = new StopWatch();
+
+    @RegisterExtension
+    @Order(1)
+    public final ContextManagerExtension contextManagerExtension;
+    private CamelContextManager contextManager;
+
+    protected CamelBlueprintTestSupport() {
+        super(new TestExecutionConfiguration(), new 
CamelBlueprintContextConfiguration());
+
+        configureTest(testConfigurationBuilder);
+        configureContext(camelContextConfiguration);
+        contextManagerExtension = new 
ContextManagerExtension(testConfigurationBuilder, camelContextConfiguration);
+    }
+
+    /**
+     * Override this method if you don't want CamelBlueprintTestSupport create 
the test bundle
+     * @return includeTestBundle
+     * If the return value is true CamelBlueprintTestSupport creates the test 
bundle which includes blueprint configuration files
+     * If the return value is false CamelBlueprintTestSupport won't create the 
test bundle
+     */
+    protected boolean includeTestBundle() {
+        return true;
+    }
+
+    /**
+     * <p>Override this method if you want to start Blueprint containers 
asynchronously using the thread
+     * that starts the bundles itself.
+     * By default this method returns <code>true</code> which means Blueprint 
Extender will use thread pool
+     * (threads named "<code>Blueprint Extender: N</code>") to startup 
Blueprint containers.</p>
+     * <p>Karaf and Fuse OSGi containers use synchronous startup.</p>
+     * <p>Asynchronous startup is more in the <em>spirit</em> of OSGi and 
usually means that if everything works fine
+     * asynchronously, it'll work synchronously as well. This isn't always 
true otherwise.</p>
+     * @return <code>true</code> when blueprint containers are to be started 
asynchronously, otherwise <code>false</code>.
+     */
+    protected boolean useAsynchronousBlueprintStartup() {
+        return true;
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    protected BundleContext createBundleContext() throws Exception {
+        System.setProperty("org.apache.aries.blueprint.synchronous", 
Boolean.toString(!useAsynchronousBlueprintStartup()));
+
+        // load configuration file
+        String[] file = loadConfigAdminConfigurationFile();
+        String[][] configAdminPidFiles = new String[0][0];
+        if (file != null) {
+            if (file.length % 2 != 0) {  // This needs to return pairs of 
filename and pid
+                throw new IllegalArgumentException("The length of the String[] 
returned from loadConfigAdminConfigurationFile must divisible by 2, was " + 
file.length);
+            }
+            configAdminPidFiles = new String[file.length / 2][2];
+
+            int pair = 0;
+            for (int i = 0; i < file.length; i += 2) {
+                String fileName = file[i];
+                String pid = file[i + 1];
+                if (!new File(fileName).exists()) {
+                    throw new IllegalArgumentException("The provided file \"" 
+ fileName + "\" from loadConfigAdminConfigurationFile doesn't exist");
+                }
+                configAdminPidFiles[pair][0] = fileName;
+                configAdminPidFiles[pair][1] = pid;
+                pair++;
+            }
+        }
+
+        // fetch initial configadmin configuration if provided programmatically
+        Properties initialConfiguration = new Properties();
+        String pid = setConfigAdminInitialConfiguration(initialConfiguration);
+        if (pid != null) {
+            configAdminPidFiles = new 
String[][]{{prepareInitialConfigFile(initialConfiguration), pid}};
+        }
+
+        final String symbolicName = getClass().getSimpleName();
+        final BundleContext answer = 
CamelBlueprintHelper.createBundleContext(symbolicName, getBlueprintDescriptor(),
+            includeTestBundle(), getBundleFilter(), getBundleVersion(), 
getBundleDirectives(), configAdminPidFiles);
+
+        boolean expectReload = 
expectBlueprintContainerReloadOnConfigAdminUpdate();
+
+        // must register override properties early in OSGi containers
+        var extra = useOverridePropertiesWithPropertiesComponent();
+        if (extra != null) {
+            answer.registerService(PropertiesComponent.OVERRIDE_PROPERTIES, 
extra, null);
+        }
+
+        Map<String, KeyValueHolder<Object, Dictionary>> map = new 
LinkedHashMap<>();
+        addServicesOnStartup(map);
+
+        List<KeyValueHolder<String, KeyValueHolder<Object, Dictionary>>> 
servicesList = new LinkedList<>();
+        for (Map.Entry<String, KeyValueHolder<Object, Dictionary>> entry : 
map.entrySet()) {
+            servicesList.add(asKeyValueService(entry.getKey(), 
entry.getValue().getKey(), entry.getValue().getValue()));
+        }
+
+        addServicesOnStartup(servicesList);
+
+        for (KeyValueHolder<String, KeyValueHolder<Object, Dictionary>> item : 
servicesList) {
+            String clazz = item.getKey();
+            Object service = item.getValue().getKey();
+            Dictionary dict = item.getValue().getValue();
+            LOG.debug("Registering service {} -> {}", clazz, service);
+            ServiceRegistration<?> reg = answer.registerService(clazz, 
service, dict);
+            if (reg != null) {
+                services.add(reg);
+            }
+        }
+
+        // if blueprint XML uses <cm:property-placeholder> (any 
update-strategy and any default properties)
+        // - 
org.apache.aries.blueprint.compendium.cm.ManagedObjectManager.register() is 
called
+        // - ManagedServiceUpdate is scheduled in felix.cm
+        // - 
org.apache.felix.cm.impl.ConfigurationImpl.setDynamicBundleLocation() is called
+        // - CM_LOCATION_CHANGED event is fired
+        // - if BP was already created, it's <cm:property-placeholder> 
receives the event and
+        // - 
org.apache.aries.blueprint.compendium.cm.CmPropertyPlaceholder.updated() is 
called,
+        //   but no BP reload occurs
+        // we will however wait for BP container of the test bundle to become 
CREATED for the first time
+        // each configadmin update *may* lead to reload of BP container, if it 
uses <cm:property-placeholder>
+        // with update-strategy="reload"
+
+        // we will gather timestamps of BP events. We don't want to be fooled 
but repeated events related
+        // to the same state of BP container
+        Set<Long> bpEvents = new HashSet<>();
+
+        CamelBlueprintHelper.waitForBlueprintContainer(bpEvents, answer, 
symbolicName, BlueprintEvent.CREATED, null);
+
+        // must reuse props as we can do both load from .cfg file and override 
afterwards
+        final Dictionary props = new Properties();
+
+        // allow end user to override properties
+        pid = useOverridePropertiesWithConfigAdmin(props);
+        if (pid != null) {
+            // we will update the configuration again
+            ConfigurationAdmin configAdmin = 
CamelBlueprintHelper.getOsgiService(answer, ConfigurationAdmin.class);
+            // passing null as second argument ties the configuration to 
correct bundle.
+            // using single-arg method causes:
+            // *ERROR* Cannot use configuration xxx.properties for 
[org.osgi.service.cm.ManagedService, id=N, bundle=N/jar:file:xyz.jar!/]: No 
visibility to configuration bound to felix-connect
+            final Configuration config = configAdmin.getConfiguration(pid, 
null);
+            if (config == null) {
+                throw new IllegalArgumentException("Cannot find configuration 
with pid " + pid + " in OSGi ConfigurationAdmin service.");
+            }
+            // lets merge configurations
+            Dictionary<String, Object> currentProperties = 
config.getProperties();
+            final Dictionary newProps = new Properties();
+            if (currentProperties == null) {
+                currentProperties = newProps;
+            }
+            for (Enumeration<String> ek = currentProperties.keys(); 
ek.hasMoreElements();) {
+                String k = ek.nextElement();
+                newProps.put(k, currentProperties.get(k));
+            }
+            for (String p : ((Properties) props).stringPropertyNames()) {
+                newProps.put(p, ((Properties) props).getProperty(p));
+            }
+
+            LOG.info("Updating ConfigAdmin {} by overriding properties {}", 
config, newProps);
+            if (expectReload) {
+                CamelBlueprintHelper.waitForBlueprintContainer(bpEvents, 
answer, symbolicName, BlueprintEvent.CREATED, new Runnable() {
+                    @Override
+                    public void run() {
+                        try {
+                            config.update(newProps);
+                        } catch (IOException e) {
+                            throw new RuntimeException(e.getMessage(), e);
+                        }
+                    }
+                });
+            } else {
+                config.update(newProps);
+            }
+        }
+
+        return answer;
+    }
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        System.setProperty("skipStartingCamelContext", "true");
+        System.setProperty("registerBlueprintCamelContextEager", "true");
+
+        if (isCreateCamelContextPerClass()) {
+            // test is per class, so only setup once (the first time)
+            boolean first = threadLocalBundleContext.get() == null;
+            if (first) {
+                threadLocalBundleContext.set(createBundleContext());
+            }
+            bundleContext = threadLocalBundleContext.get();
+        } else {
+            bundleContext = createBundleContext();
+        }
+
+        ExtensionHelper.hasUnsupported(getClass());
+
+        setupResources();
+
+        contextManager = contextManagerExtension.getContextManager();
+        contextManager.createCamelContext(this);
+        context = contextManager.context();
+
+
+
+        // only start timing after all the setup
+        watch.restart();
+
+        // we don't have to wait for BP container's OSGi service - we've 
already waited
+        // for BlueprintEvent.CREATED
+
+        // start context when we are ready
+        LOG.debug("Starting CamelContext: {}", context.getName());
+        if (isUseAdviceWith()) {
+            LOG.info("Skipping starting CamelContext as isUseAdviceWith is set 
to true.");
+        } else {
+            context.start();
+        }
+    }
+
+    /**
+     * Override this method to add services to be registered on startup.
+     * <p/>
+     * You can use the builder methods {@link #asService(Object, Dictionary)}, 
{@link #asService(Object, String, String)}
+     * to make it easy to add the services to the map.
+     */
+    protected void addServicesOnStartup(Map<String, KeyValueHolder<Object, 
Dictionary>> services) {
+        // noop
+    }
+
+    /**
+     * This method may be overriden to instruct BP test support that BP 
container will reloaded when
+     * Config Admin configuration is updated. By default, this is expected, 
when blueprint XML definition
+     * contains <code>&lt;cm:property-placeholder persistent-id="PID" 
update-strategy="reload"&gt;</code>
+     */
+    protected boolean expectBlueprintContainerReloadOnConfigAdminUpdate() {
+        boolean expectedReload = false;
+        DocumentBuilderFactory dbf = new 
XMLConverterHelper().createDocumentBuilderFactory();
+        try {
+            // cm-1.0 doesn't define update-strategy attribute
+            Set<String> cmNamesaces = new HashSet<>(Arrays.asList(
+                    CmNamespaceHandler.BLUEPRINT_CM_NAMESPACE_1_1,
+                    CmNamespaceHandler.BLUEPRINT_CM_NAMESPACE_1_2,
+                    CmNamespaceHandler.BLUEPRINT_CM_NAMESPACE_1_3
+            ));
+            for (URL descriptor : 
CamelBlueprintHelper.getBlueprintDescriptors(getBlueprintDescriptor())) {
+                DocumentBuilder db = dbf.newDocumentBuilder();
+                try (InputStream is = descriptor.openStream()) {
+                    Document doc = db.parse(is);
+                    NodeList nl = doc.getDocumentElement().getChildNodes();
+                    for (int i = 0; i < nl.getLength(); i++) {
+                        Node node = nl.item(i);
+                        if (node instanceof Element) {
+                            Element pp = (Element) node;
+                            if (cmNamesaces.contains(pp.getNamespaceURI())) {
+                                String us = pp.getAttribute("update-strategy");
+                                if (us != null && us.equals("reload")) {
+                                    expectedReload = true;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
+        return expectedReload;
+    }
+
+    /**
+     * Override this method to add services to be registered on startup.
+     * <p/>
+     * You can use the builder methods {@link #asKeyValueService(String, 
Object, Dictionary)}
+     * to make it easy to add the services to the List.
+     */
+    protected void addServicesOnStartup(List<KeyValueHolder<String, 
KeyValueHolder<Object, Dictionary>>> services) {
+        // noop
+    }
+
+    /**
+     * Creates a holder for the given service, which make it easier to use 
{@link #addServicesOnStartup(Map)}
+     */
+    protected KeyValueHolder<Object, Dictionary> asService(Object service, 
Dictionary dict) {
+        return new KeyValueHolder<>(service, dict);
+    }
+
+    /**
+     * Creates a holder for the given service, which make it easier to use 
{@link #addServicesOnStartup(List)}
+     */
+    protected KeyValueHolder<String, KeyValueHolder<Object, Dictionary>> 
asKeyValueService(String name, Object service, Dictionary dict) {
+        return new KeyValueHolder<>(name, new KeyValueHolder<>(service, dict));
+    }
+
+    /**
+     * Creates a holder for the given service, which make it easier to use 
{@link #addServicesOnStartup(Map)}
+     */
+    protected KeyValueHolder<Object, Dictionary> asService(Object service, 
String key, String value) {
+        Properties prop = new Properties();
+        if (key != null && value != null) {
+            prop.put(key, value);
+        }
+        return new KeyValueHolder<>(service, prop);
+    }
+
+    /**
+     * <p>Override this method to override config admin properties. Overriden 
properties will be passed to
+     * {@link Configuration#update(Dictionary)} and may or may not lead to 
reload of Blueprint container - this
+     * depends on <code>update-strategy="reload|none"</code> in 
<code>&lt;cm:property-placeholder&gt;</code></p>
+     * <p>This method should be used to simulate configuration update 
<strong>after</strong> Blueprint container
+     * is already initialized and started. Don't use this method to 
initialized ConfigAdmin configuration.</p>
+     *
+     * @param props properties where you add the properties to override
+     * @return the PID of the OSGi {@link ConfigurationAdmin} which are 
defined in the Blueprint XML file.
+     */
+    protected String useOverridePropertiesWithConfigAdmin(Dictionary<String, 
String> props) throws Exception {
+        return null;
+    }
+
+    /**
+     * Override this method and provide the name of the .cfg configuration 
file to use for
+     * ConfigAdmin service. Provided file will be used to initialize 
ConfigAdmin configuration before Blueprint
+     * container is loaded.
+     *
+     * @return the name of the path for the .cfg file to load, and the 
persistence-id of the property placeholder.
+     */
+    protected String[] loadConfigAdminConfigurationFile() {
+        return null;
+    }
+
+    /**
+     * Override this method as an alternative to {@link 
#loadConfigAdminConfigurationFile()} if there's a need
+     * to set initial ConfigAdmin configuration without using files.
+     *
+     * @param props always non-null. Tests may initialize ConfigAdmin 
configuration by returning PID.
+     * @return persistence-id of the property placeholder. If non-null, 
<code>props</code> will be used as
+     * initial ConfigAdmin configuration
+     */
+    protected String setConfigAdminInitialConfiguration(Properties props) {
+        return null;
+    }
+
+    @AfterEach
+    public void afterEach() throws Exception {
+        System.clearProperty("skipStartingCamelContext");
+        System.clearProperty("registerBlueprintCamelContextEager");
+
+        tearDown(new TestInfo() {
+            @Override
+            public String getDisplayName() {
+                return "";
+            }
+
+            @Override
+            public Set<String> getTags() {
+                return Set.of();
+            }
+
+            @Override
+            public Optional<Class<?>> getTestClass() {
+                return Optional.empty();
+            }
+
+            @Override
+            public Optional<Method> getTestMethod() {
+                return Optional.empty();
+            }
+        });
+
+        // unregister services
+        if (bundleContext != null) {
+            for (ServiceRegistration<?> reg : services) {
+                bundleContext.ungetService(reg.getReference());
+            }
+        }
+
+        // close bundle context
+        if (bundleContext != null) {
+            // remember bundles before closing
+            Bundle[] bundles = bundleContext.getBundles();
+            // close bundle context
+            CamelBlueprintHelper.disposeBundleContext(bundleContext);
+            // now close jar files from the bundles
+            closeBundleJArFile(bundles);
+        }
+    }
+
+    public final void tearDown(TestInfo testInfo) throws Exception {
+        long time = watch.taken();
+        LOG.debug("tearDown()");
+
+        if (contextManager != null) {
+            contextManager.dumpRouteCoverage(getClass(), 
testInfo.getDisplayName(), time);
+            String dump = CamelContextTestHelper.getRouteDump(getDumpRoute());
+            contextManager.dumpRoute(getClass(), testInfo.getDisplayName(), 
dump);
+        } else {
+            LOG.warn(
+                    "A context manager is required to dump the route coverage 
for the Camel context but it's not available (it's null). "
+                            + "It's likely that the test is misconfigured!");
+        }
+
+//        doPostTearDown();
+        cleanupResources();
+    }
+
+    @Override
+    public void cleanupResources() throws Exception {
+        if (threadLocalBundleContext.get() != null) {
+            
CamelBlueprintHelper.disposeBundleContext(threadLocalBundleContext.get());
+            threadLocalBundleContext.remove();
+        }
+        super.cleanupResources();
+    }
+
+    /**
+     * Felix Connect leaks "open files" as a JarFile on Bundle Revision is not 
closed when stopping the bundle
+     * which can cause the JVM to open up too many file handles.
+     */
+    private void closeBundleJArFile(Bundle[] bundles) {
+        for (Bundle bundle : bundles) {
+            try {
+                // not all bundles is from PojoSRBundle that has a revision
+                Field field = bundle.getClass().getDeclaredField("m_revision");
+                field.setAccessible(true);
+                Object val = field.get(bundle);
+                field = val.getClass().getDeclaredField("m_jar");
+                field.setAccessible(true);
+                Object mJar = field.get(val);
+                if (mJar instanceof JarFile) {
+                    JarFile jf = (JarFile) mJar;

Review Comment:
   Done



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to