All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.camel.test.blueprint.CamelBlueprintTestSupport Maven / Gradle / Ivy

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 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(java.util.List)} */ protected KeyValueHolder> 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(java.util.Map)} */ protected KeyValueHolder 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); } /** *

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 update-strategy="reload|none" in <cm:property-placeholder>

*

This method should be used to simulate configuration update after Blueprint container * is already initialized and started. Don't use this method to initialized ConfigAdmin configuration.

* * @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 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, props will be used as * initial ConfigAdmin configuration */ protected String setConfigAdminInitialConfiguration(Properties props) { return null; } @AfterEach @Override public void tearDown() throws Exception { System.clearProperty("skipStartingCamelContext"); System.clearProperty("registerBlueprintCamelContextEager"); super.tearDown(); // 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); } } @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; LOG.debug("Closing bundle[{}] JarFile: {}", bundle.getBundleId(), jf.getName()); jf.close(); LOG.trace("Closed bundle[{}] JarFile: {}", bundle.getBundleId(), jf.getName()); } } catch (Throwable e) { // ignore } } } /** * Return the system bundle context */ protected BundleContext getBundleContext() { return bundleContext; } /** * Gets the bundle descriptor from the classpath. *

* Return the location(s) of the bundle descriptors from the classpath. * Separate multiple locations by comma, or return a single location. *

* Only one CamelContext is supported per blueprint bundle, * so if you have multiple XML files then only one of them should have <camelContext>. *

* For example override this method and return OSGI-INF/blueprint/camel-context.xml * * @return the location of the bundle descriptor file. */ protected String getBlueprintDescriptor() { return null; } /** * Gets filter expression of bundle descriptors. * Modify this method if you wish to change default behavior. * * @return filter expression for OSGi bundles. */ protected String getBundleFilter() { return CamelBlueprintHelper.BUNDLE_FILTER; } /** * Gets test bundle version. * Modify this method if you wish to change default behavior. * * @return test bundle version */ protected String getBundleVersion() { return CamelBlueprintHelper.BUNDLE_VERSION; } /** * Gets the bundle directives. *

* Modify this method if you wish to add some directives. */ protected String getBundleDirectives() { return null; } /** * Returns how long to wait for Camel Context * to be created. * * @return timeout in milliseconds. */ protected Long getCamelContextCreationTimeout() { String tm = System.getProperty(SPROP_CAMEL_CONTEXT_CREATION_TIMEOUT); if (tm == null) { return null; } try { Long val = Long.valueOf(tm); if (val < 0) { throw new IllegalArgumentException("Value of " + SPROP_CAMEL_CONTEXT_CREATION_TIMEOUT + " cannot be negative."); } return val; } catch (NumberFormatException e) { throw new IllegalArgumentException("Value of " + SPROP_CAMEL_CONTEXT_CREATION_TIMEOUT + " has wrong format.", e); } } /** * Gets filter expression for the Camel context you want to test. * Modify this if you have multiple contexts in the OSGi registry and want to test a specific one. * * @return filter expression for Camel context. */ protected String getCamelContextFilter() { return null; } @Override protected CamelContext createCamelContext() throws Exception { CamelContext answer; Long timeout = getCamelContextCreationTimeout(); if (timeout == null) { answer = CamelBlueprintHelper.getOsgiService(bundleContext, CamelContext.class, getCamelContextFilter()); } else if (timeout >= 0) { answer = CamelBlueprintHelper.getOsgiService(bundleContext, CamelContext.class, getCamelContextFilter(), timeout); } else { throw new IllegalArgumentException("getCamelContextCreationTimeout cannot return a negative value."); } // must override context so we use the correct one in testing context = answer.adapt(ModelCamelContext.class); return answer; } protected T getOsgiService(Class type) { return CamelBlueprintHelper.getOsgiService(bundleContext, type); } protected T getOsgiService(Class type, long timeout) { return CamelBlueprintHelper.getOsgiService(bundleContext, type, timeout); } protected T getOsgiService(Class type, String filter) { return CamelBlueprintHelper.getOsgiService(bundleContext, type, filter); } protected T getOsgiService(Class type, String filter, long timeout) { return CamelBlueprintHelper.getOsgiService(bundleContext, type, filter, timeout); } /** * Create a temporary File with persisted configuration for ConfigAdmin */ private String prepareInitialConfigFile(Properties initialConfiguration) throws IOException { File dir = new File("target/etc"); dir.mkdirs(); File cfg = Files.createTempFile(dir.toPath(), "properties-", ".cfg").toFile(); FileWriter writer = new FileWriter(cfg); try { initialConfiguration.store(writer, null); } finally { IOHelper.close(writer); } return cfg.getAbsolutePath(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy