org.apache.camel.test.blueprint.CamelBlueprintTestSupport Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of camel-test-blueprint-junit5 Show documentation
Show all versions of camel-test-blueprint-junit5 Show documentation
Camel unit testing with OSGi Blueprint and Junit5
The newest version!
/*
* 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 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.net.URL;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.jar.JarFile;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.osgi.framework.Bundle;
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 org.apache.aries.blueprint.compendium.cm.CmNamespaceHandler;
import org.apache.camel.CamelContext;
import org.apache.camel.blueprint.CamelBlueprintHelper;
import org.apache.camel.component.properties.PropertiesComponent;
import org.apache.camel.model.ModelCamelContext;
import org.apache.camel.support.builder.xml.XMLConverterHelper;
import org.apache.camel.test.junit5.CamelTestSupport;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.KeyValueHolder;
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;
/**
* Base class for OSGi Blueprint unit tests with Camel
*/
public abstract class CamelBlueprintTestSupport extends CamelTestSupport {
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 threadLocalBundleContext = new ThreadLocal<>();
private volatile BundleContext bundleContext;
private final Set> services = new LinkedHashSet<>();
/**
* 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;
}
/**
* Override this method if you want to start Blueprint containers asynchronously using the thread
* that starts the bundles itself.
* By default this method returns true
which means Blueprint Extender will use thread pool
* (threads named "Blueprint Extender: N
") to startup Blueprint containers.
* Karaf and Fuse OSGi containers use synchronous startup.
* Asynchronous startup is more in the spirit of OSGi and usually means that if everything works fine
* asynchronously, it'll work synchronously as well. This isn't always true otherwise.
* @return true
when blueprint containers are to be started asynchronously, otherwise false
.
*/
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
extra = useOverridePropertiesWithPropertiesComponent();
if (extra != null) {
answer.registerService(PropertiesComponent.OVERRIDE_PROPERTIES, extra, null);
}
Map> map = new LinkedHashMap<>();
addServicesOnStartup(map);
List>> servicesList = new LinkedList<>();
for (Map.Entry> entry : map.entrySet()) {
servicesList.add(asKeyValueService(entry.getKey(), entry.getValue().getKey(), entry.getValue().getValue()));
}
addServicesOnStartup(servicesList);
for (KeyValueHolder> 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 (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 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
// 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 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 currentProperties = config.getProperties();
final Dictionary newProps = new Properties();
if (currentProperties == null) {
currentProperties = newProps;
}
for (Enumeration 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
@Override
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();
}
super.setUp();
// 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.
*
* You can use the builder methods {@link #asService(Object, java.util.Dictionary)}, {@link #asService(Object, String, String)}
* to make it easy to add the services to the map.
*/
protected void addServicesOnStartup(Map> 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 <cm:property-placeholder persistent-id="PID" update-strategy="reload">
*/
protected boolean expectBlueprintContainerReloadOnConfigAdminUpdate() {
boolean expectedReload = false;
DocumentBuilderFactory dbf = new XMLConverterHelper().createDocumentBuilderFactory();
try {
// cm-1.0 doesn't define update-strategy attribute
Set 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.
*
* 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>> services) {
// noop
}
/**
* Creates a holder for the given service, which make it easier to use {@link #addServicesOnStartup(java.util.Map)}
*/
protected KeyValueHolder
© 2015 - 2025 Weber Informatics LLC | Privacy Policy