jhattab commented on code in PR #651: URL: https://github.com/apache/camel-karaf/pull/651#discussion_r2641149824
########## 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><cm:property-placeholder persistent-id="PID" update-strategy="reload"></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><cm:property-placeholder></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() { 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]
