/*
* Copyright 2011 Harald Wellmann.
*
* Licensed 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.ops4j.pax.exam.forked;
import static org.ops4j.pax.exam.Constants.EXAM_FAIL_ON_UNRESOLVED_KEY;
import static org.ops4j.pax.exam.Constants.START_LEVEL_TEST_BUNDLE;
import static org.ops4j.pax.exam.CoreOptions.systemProperty;
import static org.osgi.framework.Constants.FRAMEWORK_BOOTDELEGATION;
import static org.osgi.framework.Constants.FRAMEWORK_STORAGE;
import static org.osgi.framework.Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.ops4j.io.StreamUtils;
import org.ops4j.pax.exam.ConfigurationManager;
import org.ops4j.pax.exam.ExamSystem;
import org.ops4j.pax.exam.Option;
import org.ops4j.pax.exam.TestAddress;
import org.ops4j.pax.exam.TestContainer;
import org.ops4j.pax.exam.TestContainerException;
import org.ops4j.pax.exam.forked.provision.PlatformImpl;
import org.ops4j.pax.exam.options.BootClasspathLibraryOption;
import org.ops4j.pax.exam.options.BootDelegationOption;
import org.ops4j.pax.exam.options.FrameworkPropertyOption;
import org.ops4j.pax.exam.options.FrameworkStartLevelOption;
import org.ops4j.pax.exam.options.PropagateSystemPropertyOption;
import org.ops4j.pax.exam.options.ProvisionOption;
import org.ops4j.pax.exam.options.SystemPackageOption;
import org.ops4j.pax.exam.options.SystemPropertyOption;
import org.ops4j.pax.exam.options.UrlReference;
import org.ops4j.pax.exam.options.ValueOption;
import org.ops4j.pax.exam.options.extra.RepositoryOption;
import org.ops4j.pax.exam.options.extra.VMOption;
import org.ops4j.pax.swissbox.framework.RemoteFramework;
import org.ops4j.pax.swissbox.framework.RemoteServiceReference;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.launch.FrameworkFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A {@link TestContainer} which launches an OSGi framework in a forked Java VM to isolate the
* framework parent class loader from the application class loader containing Pax Exam and
* additional user classes.
* <p>
* The drawback of this container is that remote debugging is required to debug the tests executed
* by the forked framework.
*
* @author Harald Wellmann
*/
public class ForkedTestContainer implements TestContainer {
private static final Logger LOG = LoggerFactory.getLogger(ForkedTestContainer.class);
private ExamSystem system;
private final ForkedFrameworkFactory frameworkFactory;
private RemoteFramework remoteFramework;
private final PlatformImpl platform;
private final String name;
private Long probeId;
public ForkedTestContainer(ExamSystem system, FrameworkFactory frameworkFactory) {
this.system = system;
this.frameworkFactory = new ForkedFrameworkFactory(frameworkFactory);
this.platform = new PlatformImpl();
this.name = "Forked:" + frameworkFactory.getClass().getSimpleName();
}
@Override
public void call(TestAddress address) {
String filterExpression = "(&(objectClass=org.ops4j.pax.exam.ProbeInvoker)(Probe-Signature="
+ address.root().identifier() + "))";
try {
RemoteServiceReference[] references = remoteFramework.getServiceReferences(
filterExpression, system.getTimeout().getValue(), TimeUnit.MILLISECONDS);
remoteFramework.invokeMethodOnService(references[0], "call",
(Object) address.arguments());
}
// CHECKSTYLE:SKIP
catch (Exception exc) {
throw new TestContainerException(exc);
}
}
@Override
public long install(String location, InputStream stream) {
try {
return remoteFramework.installBundle(location);
}
catch (RemoteException | BundleException exc) {
throw new TestContainerException(exc);
}
}
@Override
public long install(InputStream stream) {
try {
long bundleId = remoteFramework.installBundle("local", pack(stream));
remoteFramework.startBundle(bundleId);
return bundleId;
}
catch (RemoteException | BundleException exc) {
throw new TestContainerException(exc);
}
}
@Override
public TestContainer start() {
try {
system = system.fork(new Option[] { systemProperty("java.protocol.handler.pkgs").value(
"org.ops4j.pax.url") });
List<String> vmArgs = createVmArguments();
Map<String, String> systemProperties = createSystemProperties();
Map<String, Object> frameworkProperties = createFrameworkProperties();
List<String> beforeFrameworkClasspath = new ArrayList<>();
List<String> afterFrameworkClasspath = new ArrayList<>();
BootClasspathLibraryOption[] bootClasspathLibraryOptions = system
.getOptions(BootClasspathLibraryOption.class);
if (bootClasspathLibraryOptions != null && bootClasspathLibraryOptions.length > 0) {
for (BootClasspathLibraryOption bootClasspathLibraryOption : bootClasspathLibraryOptions) {
UrlReference libraryUrl = bootClasspathLibraryOption.getLibraryUrl();
String library = localize(libraryUrl.getURL());
if (bootClasspathLibraryOption.isAfterFramework()) {
afterFrameworkClasspath.add(library);
}
else {
beforeFrameworkClasspath.add(library);
}
}
}
remoteFramework = frameworkFactory.fork(vmArgs, systemProperties, frameworkProperties,
beforeFrameworkClasspath, afterFrameworkClasspath);
remoteFramework.init();
installAndStartBundles();
}
catch (BundleException | IOException exc) {
throw new TestContainerException(exc);
}
return this;
}
@Override
public TestContainer stop() {
try {
remoteFramework.stop();
system.clear();
}
catch (RemoteException | BundleException exc) {
throw new TestContainerException(exc);
}
frameworkFactory.join();
system.clear();
return this;
}
private byte[] pack(InputStream stream) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
StreamUtils.copyStream(stream, out, true);
}
catch (IOException e) {
}
return out.toByteArray();
}
private Map<String, Object> createFrameworkProperties() throws IOException {
final Map<String, Object> p = new HashMap<String, Object>();
p.put(FRAMEWORK_STORAGE, system.getTempFolder().getAbsolutePath());
SystemPackageOption[] systemPackageOptions = system.getOptions(SystemPackageOption.class);
if (systemPackageOptions.length > 0) {
p.put(FRAMEWORK_SYSTEMPACKAGES_EXTRA, buildString(systemPackageOptions));
}
p.put(FRAMEWORK_BOOTDELEGATION, buildString(system.getOptions(BootDelegationOption.class)));
for (FrameworkPropertyOption option : system.getOptions(FrameworkPropertyOption.class)) {
p.put(option.getKey(), option.getValue());
}
for (SystemPropertyOption option : system.getOptions(SystemPropertyOption.class)) {
System.setProperty(option.getKey(), option.getValue());
}
return p;
}
private List<String> createVmArguments() {
VMOption[] options = system.getOptions(VMOption.class);
List<String> args = new ArrayList<String>();
for (VMOption option : options) {
args.add(option.getOption());
}
return args;
}
private Map<String, String> createSystemProperties() {
Map<String, String> p = new HashMap<String, String>();
for (PropagateSystemPropertyOption option : system
.getOptions(PropagateSystemPropertyOption.class)) {
String key = option.getKey();
String value = System.getProperty(key);
if (value != null) {
p.put(key, value);
}
}
for (SystemPropertyOption option : system.getOptions(SystemPropertyOption.class)) {
p.put(option.getKey(), option.getValue());
}
RepositoryOption[] repositories = system.getOptions(RepositoryOption.class);
if (repositories.length != 0) {
System.setProperty("org.ops4j.pax.url.mvn.repositories", buildString(repositories));
}
return p;
}
private String buildString(ValueOption<?>[] options) {
return buildString(new String[0], options, new String[0]);
}
private String buildString(String[] prepend, ValueOption<?>[] options, String[] append) {
StringBuilder builder = new StringBuilder();
for (String a : prepend) {
builder.append(a);
builder.append(",");
}
for (ValueOption<?> option : options) {
builder.append(option.getValue());
builder.append(",");
}
for (String a : append) {
builder.append(a);
builder.append(",");
}
if (builder.length() > 0) {
return builder.substring(0, builder.length() - 1);
}
else {
return "";
}
}
private void installAndStartBundles() throws BundleException, RemoteException {
File workDir = new File(system.getTempFolder(), "pax-exam-downloads");
workDir.mkdirs();
List<Long> bundleIds = new ArrayList<Long>();
ProvisionOption<?>[] options = system.getOptions(ProvisionOption.class);
Map<String, Long> remoteMappings = new HashMap<String, Long>();
Map<Long, String> bundlesById = new HashMap<Long, String>();
for (ProvisionOption<?> bundle : options) {
String localUrl = downloadBundle(workDir, bundle.getURL());
long bundleId = remoteFramework.installBundle(localUrl);
remoteMappings.put(bundle.getURL(), bundleId);
bundlesById.put(bundleId, bundle.getURL());
}
// All bundles are installed, we can now start the framework...
remoteFramework.start();
// iterate over the bundles, set start level and start them
// TODO Simplify with new method in Pax Swissbox 1.7.0:
// remoteFramework.installBundle(localUrl, startLevel, autostart);
for (ProvisionOption<?> bundle : options) {
int startLevel = getStartLevel(bundle);
Long bundleId = remoteMappings.get(bundle.getURL());
remoteFramework.setBundleStartLevel(bundleId, startLevel);
if (bundle.shouldStart()) {
bundleIds.add(bundleId);
remoteFramework.startBundle(bundleId);
LOG.debug("+ Install (start@{}) {}", startLevel, bundle);
}
else {
LOG.debug("+ Install (no start) {}", bundle);
}
}
setFrameworkStartLevel();
verifyThatBundlesAreResolved(bundleIds, bundlesById);
}
private void setFrameworkStartLevel() throws RemoteException {
FrameworkStartLevelOption startLevelOption = system
.getSingleOption(FrameworkStartLevelOption.class);
int startLevel = startLevelOption == null ? START_LEVEL_TEST_BUNDLE : startLevelOption
.getStartLevel();
LOG.debug("Jump to startlevel [{}]", startLevel);
long timeout = system.getTimeout().getValue();
boolean startLevelReached = remoteFramework.setFrameworkStartLevel(startLevel, timeout);
if (!startLevelReached) {
String msg = String.format("start level %d has not been reached within %d ms",
startLevel, timeout);
throw new TestContainerException(msg);
}
}
private void verifyThatBundlesAreResolved(List<Long> bundleIds, Map<Long, String> bundlesById) throws RemoteException {
boolean hasUnresolvedBundles = false;
for (long bundleId : bundleIds) {
try {
if (remoteFramework.getBundleState(bundleId) == Bundle.INSTALLED) {
LOG.error("Bundle [id:{}, url:{}] is not resolved", bundleId, bundlesById.get(bundleId));
hasUnresolvedBundles = true;
}
}
catch (BundleException exc) {
throw new TestContainerException(exc);
}
}
ConfigurationManager cm = new ConfigurationManager();
boolean failOnUnresolved = Boolean.parseBoolean(cm.getProperty(EXAM_FAIL_ON_UNRESOLVED_KEY,
"false"));
if (hasUnresolvedBundles && failOnUnresolved) {
throw new TestContainerException(
"There are unresolved bundles. See previous ERROR log messages for details.");
}
}
private String downloadBundle(File workDir, String url) {
try {
URL realUrl = new URL(url);
if (realUrl.getProtocol().equals("reference")) {
return url;
}
File localBundle = platform.download(workDir, realUrl, url, false, true,
true, false);
return localBundle.toURI().toURL().toString();
}
catch (MalformedURLException exc) {
throw new TestContainerException(exc);
}
}
private String localize(String url) {
try {
URL realUrl = new URL(url);
if (realUrl.getProtocol().equals("reference")) {
// must be "reference:file:..."
return new URL(realUrl.getPath()).getPath();
}
else if (realUrl.getProtocol().equals("file")) {
return realUrl.getPath();
}
File artifact = platform.download(system.getTempFolder(), realUrl, url, false, false,
false, false);
return artifact.getCanonicalPath();
}
catch (MalformedURLException exc) {
throw new TestContainerException(exc);
}
catch (IOException exc) {
throw new TestContainerException(exc);
}
}
private int getStartLevel(ProvisionOption<?> bundle) {
Integer start = bundle.getStartLevel();
if (start == null) {
start = org.ops4j.pax.exam.Constants.START_LEVEL_DEFAULT_PROVISION;
}
return start;
}
@Override
public String toString() {
return name;
}
@Override
public long installProbe(InputStream stream) {
this.probeId = install(stream);
return probeId;
}
@Override
public void uninstallProbe() {
try {
remoteFramework.uninstallBundle(probeId);
}
catch (RemoteException | BundleException exc) {
throw new TestContainerException(exc);
}
}
}