/**
* 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.karaf;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.function.Consumer;
import javax.inject.Inject;
import org.apache.camel.CamelContext;
import org.apache.camel.Component;
import org.apache.camel.spi.DataFormat;
import org.apache.camel.spi.Language;
import org.apache.karaf.features.FeaturesService;
import org.junit.After;
import org.junit.Before;
import org.ops4j.pax.exam.CoreOptions;
import org.ops4j.pax.exam.Option;
import org.ops4j.pax.exam.ProbeBuilder;
import org.ops4j.pax.exam.TestProbeBuilder;
import org.ops4j.pax.exam.karaf.options.KarafDistributionOption;
import org.ops4j.pax.exam.karaf.options.LogLevelOption;
import org.ops4j.pax.exam.options.UrlReference;
import org.ops4j.pax.tinybundles.core.TinyBundle;
import org.ops4j.pax.tinybundles.core.TinyBundles;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.blueprint.container.BlueprintContainer;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.Assert.assertNotNull;
import static org.ops4j.pax.exam.CoreOptions.maven;
import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
import static org.ops4j.pax.exam.CoreOptions.vmOption;
import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.configureConsole;
import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.editConfigurationFilePut;
import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.features;
import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.keepRuntimeFolder;
import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.logLevel;
public abstract class AbstractFeatureTest {
public static final Long SERVICE_TIMEOUT = 30000L;
protected static final Logger LOG = LoggerFactory.getLogger(AbstractFeatureTest.class);
@Inject
protected BundleContext bundleContext;
@Inject
protected BlueprintContainer blueprintContainer;
@Inject
protected FeaturesService featuresService;
@ProbeBuilder
public TestProbeBuilder probeConfiguration(TestProbeBuilder probe) {
// makes sure the generated Test-Bundle contains this import!
probe.setHeader(Constants.DYNAMICIMPORT_PACKAGE, "*");
return probe;
}
@Before
public void setUp() throws Exception {
LOG.info("setUp() using BundleContext: {}", bundleContext);
}
@After
public void tearDown() throws Exception {
LOG.info("tearDown()");
}
protected Bundle installBlueprintAsBundle(String name, URL url, boolean start) throws BundleException {
return installBlueprintAsBundle(name, url, start, bundle -> { });
}
protected Bundle installBlueprintAsBundle(String name, URL url, boolean start, Consumer<Object> consumer) throws BundleException {
// TODO Type Consumer<TinyBundle> cannot be used for this method signature to avoid bundle dependency to pax tinybundles
TinyBundle bundle = TinyBundles.bundle();
bundle.add("OSGI-INF/blueprint/blueprint-" + name.toLowerCase(Locale.ENGLISH) + ".xml", url);
bundle.set("Manifest-Version", "2")
.set("Bundle-ManifestVersion", "2")
.set("Bundle-SymbolicName", name)
.set("Bundle-Version", "1.0.0");
consumer.accept(bundle);
Bundle answer = bundleContext.installBundle(name, bundle.build());
if (start) {
answer.start();
}
return answer;
}
protected Bundle installSpringAsBundle(String name, URL url, boolean start) throws BundleException {
return installSpringAsBundle(name, url, start, bundle -> { });
}
protected Bundle installSpringAsBundle(String name, URL url, boolean start, Consumer<Object> consumer) throws BundleException {
// TODO Type Consumer<TinyBundle> cannot be used for this method signature to avoid bundle dependency to pax tinybundles
TinyBundle bundle = TinyBundles.bundle();
bundle.add("META-INF/spring/spring-" + name.toLowerCase(Locale.ENGLISH) + ".xml", url);
bundle.set("Manifest-Version", "2")
.set("Bundle-ManifestVersion", "2")
.set("Bundle-SymbolicName", name)
.set("Bundle-Version", "1.0.0");
consumer.accept(bundle);
Bundle answer = bundleContext.installBundle(name, bundle.build());
if (start) {
answer.start();
}
return answer;
}
protected void installCamelFeature(String mainFeature) throws Exception {
if (!mainFeature.startsWith("camel-")) {
mainFeature = "camel-" + mainFeature;
}
LOG.info("Install main feature: {}", mainFeature);
// do not refresh bundles causing out bundle context to be invalid
// TODO: see if we can find a way maybe to install camel.xml as bundle/feature instead of part of unit test (see src/test/resources/OSGI-INF/blueprint)
featuresService.installFeature(mainFeature, EnumSet.of(FeaturesService.Option.NoAutoRefreshBundles));
}
protected void overridePropertiesWithConfigAdmin(String pid, Properties props) throws IOException {
ConfigurationAdmin configAdmin = getOsgiService(bundleContext, ConfigurationAdmin.class);
// passing null as second argument ties the configuration to correct bundle.
Configuration config = configAdmin.getConfiguration(pid, null);
if (config == null) {
throw new IllegalArgumentException("Cannot find configuration with pid " + pid + " in OSGi ConfigurationAdmin service.");
}
// let's merge configurations
Dictionary<String, Object> currentProperties = config.getProperties();
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 : props.stringPropertyNames()) {
newProps.put(p, props.getProperty(p));
}
LOG.info("Updating ConfigAdmin {} by overriding properties {}", config, newProps);
config.update(newProps);
}
protected void testComponent(String component) throws Exception {
testComponent("camel-" + component, component);
}
protected void testComponent(String mainFeature, String component) throws Exception {
LOG.info("Looking up CamelContext(myCamel) in OSGi Service Registry");
installCamelFeature(mainFeature);
CamelContext camelContext = getOsgiService(bundleContext, CamelContext.class, "(camel.context.name=myCamel)", SERVICE_TIMEOUT);
assertNotNull("Cannot find CamelContext with name myCamel", camelContext);
LOG.info("Getting Camel component: {}", component);
// do not auto start the component as it may not have been configured properly and fail in its start method
Component comp = camelContext.getComponent(component, true, false);
assertNotNull("Cannot get component with name: " + component, comp);
LOG.info("Found Camel component: {} instance: {} with className: {}", component, comp, comp.getClass());
}
protected void testDataFormat(String dataFormat) throws Exception {
testDataFormat("camel-" + dataFormat, dataFormat);
}
protected void testDataFormat(String mainFeature, String dataFormat) throws Exception {
LOG.info("Looking up CamelContext(myCamel) in OSGi Service Registry");
installCamelFeature(mainFeature);
CamelContext camelContext = getOsgiService(bundleContext, CamelContext.class, "(camel.context.name=myCamel)", SERVICE_TIMEOUT);
assertNotNull("Cannot find CamelContext with name myCamel", camelContext);
LOG.info("Getting Camel dataformat: {}", dataFormat);
DataFormat df = camelContext.resolveDataFormat(dataFormat);
assertNotNull("Cannot get dataformat with name: " + dataFormat, df);
LOG.info("Found Camel dataformat: {} instance: {} with className: {}", dataFormat, df, df.getClass());
}
protected void testLanguage(String language) throws Exception {
testLanguage("camel-" + language, language);
}
protected void testLanguage(String mainFeature, String language) throws Exception {
LOG.info("Looking up CamelContext(myCamel) in OSGi Service Registry");
installCamelFeature(mainFeature);
CamelContext camelContext = getOsgiService(bundleContext, CamelContext.class, "(camel.context.name=myCamel)", 20000);
assertNotNull("Cannot find CamelContext with name myCamel", camelContext);
LOG.info("Getting Camel language: {}", language);
Language lan = camelContext.resolveLanguage(language);
assertNotNull("Cannot get language with name: " + language, lan);
LOG.info("Found Camel language: {} instance: {} with className: {}", language, lan, lan.getClass());
}
public static String extractName(Class<?> clazz) {
String name = clazz.getName();
int id0 = name.indexOf("Camel") + "Camel".length();
int id1 = name.indexOf("Test");
StringBuilder sb = new StringBuilder();
for (int i = id0; i < id1; i++) {
char c = name.charAt(i);
if (Character.isUpperCase(c) && sb.length() > 0) {
sb.append("-");
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
}
public static UrlReference getCamelKarafFeatureUrl() {
return mavenBundle().
groupId("org.apache.camel.karaf").
artifactId("apache-camel").
version(getCamelKarafFeatureVersion()).
type("xml/features");
}
private static String getCamelKarafFeatureVersion() {
String camelKarafFeatureVersion = System.getProperty("camelKarafFeatureVersion");
if (camelKarafFeatureVersion == null) {
throw new RuntimeException("Please specify the maven artifact version to use for org.apache.camel.karaf/apache-camel through the camelKarafFeatureVersion System property");
}
return camelKarafFeatureVersion;
}
private static void switchPlatformEncodingToUTF8() {
try {
System.setProperty("file.encoding", "UTF-8");
Field charset = Charset.class.getDeclaredField("defaultCharset");
charset.setAccessible(true);
charset.set(null, null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static String getKarafVersion() {
InputStream ins = AbstractFeatureTest.class.getResourceAsStream("/META-INF/maven/dependencies.properties");
Properties p = new Properties();
try {
p.load(ins);
} catch (Throwable t) {
// ignore
}
String karafVersion = p.getProperty("org.apache.karaf/apache-karaf/version");
if (karafVersion == null) {
karafVersion = System.getProperty("karafVersion");
}
if (karafVersion == null) {
// setup the default version of it
karafVersion = "4.1.0";
}
return karafVersion;
}
public static Option[] configure(String... extra) {
List<String> camel = new ArrayList<>();
camel.add("camel");
if (extra != null && extra.length > 0) {
for (String e : extra) {
camel.add(e);
}
}
final String[] camelFeatures = camel.toArray(new String[camel.size()]);
switchPlatformEncodingToUTF8();
String karafVersion = getKarafVersion();
LOG.info("*** Apache Karaf version is " + karafVersion + " ***");
Option[] options = new Option[]{
// for remote debugging
//org.ops4j.pax.exam.CoreOptions.vmOption("-Xdebug"),
//org.ops4j.pax.exam.CoreOptions.vmOption("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5008"),
KarafDistributionOption.karafDistributionConfiguration()
.frameworkUrl(maven().groupId("org.apache.karaf").artifactId("apache-karaf").type("tar.gz").versionAsInProject())
.karafVersion(karafVersion)
.name("Apache Karaf")
.useDeployFolder(false).unpackDirectory(new File("target/paxexam/unpack/")),
logLevel(LogLevelOption.LogLevel.INFO),
// keep the folder so we can look inside when something fails
keepRuntimeFolder(),
// Disable the SSH port
configureConsole().ignoreRemoteShell(),
// need to modify the jre.properties to export some com.sun packages that some features rely on
// KarafDistributionOption.replaceConfigurationFile("etc/jre.properties", new File("src/test/resources/jre.properties")),
vmOption("-Dfile.encoding=UTF-8"),
// Disable the Karaf shutdown port
editConfigurationFilePut("etc/custom.properties", "karaf.shutdown.port", "-1"),
// Assign unique ports for Karaf
// editConfigurationFilePut("etc/org.ops4j.pax.web.cfg", "org.osgi.service.http.port", Integer.toString(AvailablePortFinder.getNextAvailable())),
// editConfigurationFilePut("etc/org.apache.karaf.management.cfg", "rmiRegistryPort", Integer.toString(AvailablePortFinder.getNextAvailable())),
// editConfigurationFilePut("etc/org.apache.karaf.management.cfg", "rmiServerPort", Integer.toString(AvailablePortFinder.getNextAvailable())),
// install junit
CoreOptions.junitBundles(),
// install camel
features(getCamelKarafFeatureUrl(), camelFeatures),
// install camel-test-karaf as bundle (not feature as the feature causes a bundle refresh that invalidates the @Inject bundleContext)
mavenBundle().groupId("org.apache.camel").artifactId("camel-test-karaf").versionAsInProject()
};
return options;
}
protected <T> T getOsgiService(BundleContext bundleContext, Class<T> type) {
return getOsgiService(bundleContext, type, null, SERVICE_TIMEOUT);
}
protected <T> T getOsgiService(BundleContext bundleContext, Class<T> type, long timeout) {
return getOsgiService(bundleContext, type, null, timeout);
}
@SuppressWarnings("unchecked")
public static <T> T getOsgiService(BundleContext bundleContext, Class<T> type, String filter, long timeout) {
ServiceTracker tracker;
try {
String flt;
if (filter != null) {
if (filter.startsWith("(")) {
flt = "(&(" + Constants.OBJECTCLASS + "=" + type.getName() + ")" + filter + ")";
} else {
flt = "(&(" + Constants.OBJECTCLASS + "=" + type.getName() + ")(" + filter + "))";
}
} else {
flt = "(" + Constants.OBJECTCLASS + "=" + type.getName() + ")";
}
Filter osgiFilter = FrameworkUtil.createFilter(flt);
tracker = new ServiceTracker(bundleContext, osgiFilter, null);
tracker.open(true);
// Note that the tracker is not closed to keep the reference
// This is buggy, as the service reference may change i think
Object svc = tracker.waitForService(timeout);
if (svc == null) {
Dictionary<?, ?> dic = bundleContext.getBundle().getHeaders();
LOG.warn("Test bundle headers: " + explode(dic));
for (ServiceReference ref : asCollection(bundleContext.getAllServiceReferences(null, null))) {
LOG.warn("ServiceReference: " + ref + ", bundle: " + ref.getBundle() + ", symbolicName: " + ref.getBundle().getSymbolicName());
}
for (ServiceReference ref : asCollection(bundleContext.getAllServiceReferences(null, flt))) {
LOG.warn("Filtered ServiceReference: " + ref + ", bundle: " + ref.getBundle() + ", symbolicName: " + ref.getBundle().getSymbolicName());
}
throw new RuntimeException("Gave up waiting for service " + flt);
}
return type.cast(svc);
} catch (InvalidSyntaxException e) {
throw new IllegalArgumentException("Invalid filter", e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
/**
* Explode the dictionary into a <code>,</code> delimited list of <code>key=value</code> pairs.
*/
private static String explode(Dictionary<?, ?> dictionary) {
Enumeration<?> keys = dictionary.keys();
StringBuilder result = new StringBuilder();
while (keys.hasMoreElements()) {
Object key = keys.nextElement();
result.append(String.format("%s=%s", key, dictionary.get(key)));
if (keys.hasMoreElements()) {
result.append(", ");
}
}
return result.toString();
}
/**
* Provides an iterable collection of references, even if the original array is <code>null</code>.
*/
private static Collection<ServiceReference> asCollection(ServiceReference[] references) {
return references == null ? new ArrayList<ServiceReference>(0) : Arrays.asList(references);
}
}