/*
* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* 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.wso2.carbon.container;
import org.apache.commons.io.FileUtils;
import org.ops4j.net.FreePort;
import org.ops4j.pax.exam.ExamSystem;
import org.ops4j.pax.exam.RelativeTimeout;
import org.ops4j.pax.exam.TestAddress;
import org.ops4j.pax.exam.TestContainer;
import org.ops4j.pax.exam.TestContainerException;
import org.ops4j.pax.exam.container.remote.RBCRemoteTarget;
import org.ops4j.pax.exam.options.SystemPropertyOption;
import org.ops4j.pax.exam.options.ValueOption;
import org.ops4j.pax.exam.options.extra.RepositoryOption;
import org.ops4j.pax.exam.rbc.client.RemoteBundleContextClient;
import org.osgi.framework.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wso2.carbon.container.options.CarbonDistributionBaseOption;
import org.wso2.carbon.container.options.CopyFileOption;
import org.wso2.carbon.container.options.CopyOSGiLibBundleOption;
import org.wso2.carbon.container.options.DebugOption;
import org.wso2.carbon.container.options.KeepDirectoryOption;
import org.wso2.carbon.container.runner.CarbonRunner;
import org.wso2.carbon.container.runner.Runner;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.rmi.NoSuchObjectException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import static org.ops4j.pax.exam.CoreOptions.options;
import static org.ops4j.pax.exam.CoreOptions.systemProperty;
import static org.ops4j.pax.exam.rbc.Constants.RMI_HOST_PROPERTY;
import static org.ops4j.pax.exam.rbc.Constants.RMI_NAME_PROPERTY;
import static org.ops4j.pax.exam.rbc.Constants.RMI_PORT_PROPERTY;
/**
* Test Container class to configure the distribution and start the server.
*
* @since 5.2.0
*/
public class CarbonTestContainer implements TestContainer {
private static final Logger logger = LoggerFactory.getLogger(CarbonTestContainer.class);
private static final String CARBON_TEST_CONTAINER = "CarbonTestContainer";
private static final String EXAM_INJECT_PROPERTY = "pax.exam.inject";
private static final String LIB_DIRECTORY = "lib";
private final Runner runner;
private final ExamSystem system;
private CarbonDistributionBaseOption carbonHomeDirectoryOption;
private RBCRemoteTarget target;
private Path targetDirectory;
private Registry registry;
private boolean started;
public CarbonTestContainer(ExamSystem system, CarbonDistributionBaseOption carbonHomeDirectoryOption) {
this.carbonHomeDirectoryOption = carbonHomeDirectoryOption;
this.system = system;
this.runner = new CarbonRunner();
}
/**
* Starts the test container.
*
* @return this container object for api
*/
public synchronized TestContainer start() {
if (carbonHomeDirectoryOption.getDistributionDirectoryPath() == null
&& carbonHomeDirectoryOption.getDistributionMavenURL() == null
&& carbonHomeDirectoryOption.getDistributionZipPath() == null) {
throw new TestContainerException("Distribution path need to be set.");
}
try {
String name = system.createID(CARBON_TEST_CONTAINER);
//get a free port to use for rmi
FreePort freePort = new FreePort(21000, 21099);
int port = freePort.getPort();
logger.debug("using RMI registry at port {}" + name, port);
registry = LocateRegistry.createRegistry(port);
String host = InetAddress.getLocalHost().getHostName();
//Setting RMI related properties
ExamSystem subsystem = system.fork(options(systemProperty(RMI_HOST_PROPERTY).value(host),
systemProperty(RMI_PORT_PROPERTY).value(Integer.toString(port)),
systemProperty(RMI_NAME_PROPERTY).value(name), systemProperty(EXAM_INJECT_PROPERTY).value("true")));
target = new RBCRemoteTarget(name, port, subsystem.getTimeout());
System.setProperty("java.protocol.handler.pkgs", "org.ops4j.pax.url");
//setup repositories if there are any
addRepositories();
targetDirectory = retrieveFinalTargetDirectory();
if (carbonHomeDirectoryOption.getDistributionMavenURL() != null) {
URL sourceDistribution = new URL(carbonHomeDirectoryOption.getDistributionMavenURL().getURL());
ArchiveExtractor.extract(sourceDistribution, targetDirectory.toFile());
} else if (carbonHomeDirectoryOption.getDistributionZipPath() != null) {
Path sourceDistribution = carbonHomeDirectoryOption.getDistributionZipPath();
ArchiveExtractor.extract(sourceDistribution, targetDirectory.toFile());
} else if (carbonHomeDirectoryOption.getDistributionDirectoryPath() != null) {
Path sourceDirectory = carbonHomeDirectoryOption.getDistributionDirectoryPath();
FileUtils.copyDirectory(sourceDirectory.toFile(), targetDirectory.toFile());
}
//install bundles if there are any
copyOSGiLibBundles(targetDirectory);
//copy files to the distributions if there are any
copyFiles(targetDirectory);
Path carbonBin = targetDirectory.resolve("bin");
//make the files in the bin directory to be executable
makeFilesInBinExec(carbonBin.toFile());
List<String> options = new ArrayList<>();
String[] environment = new String[] {};
//set system properties as command line arguments
setupSystemProperties(options, subsystem);
//Setup debug configurations if available
DebugOption debugOption = system.getSingleOption(DebugOption.class);
if (debugOption != null) {
options.add(debugOption.getDebugConfiguration());
}
runner.exec(environment, targetDirectory, options);
logger.debug("Wait for test container to finish its initialization " + subsystem.getTimeout());
//wait for the osgi environment to be active
waitForState(0, Bundle.ACTIVE, subsystem.getTimeout());
started = true;
} catch (IOException e) {
throw new TestContainerException("Problem starting container", e);
}
return this;
}
/**
* Set repositories specified in the Repository Option.
*/
private void addRepositories() {
RepositoryOption[] repositories = system.getOptions(RepositoryOption.class);
if (repositories.length != 0) {
System.setProperty("org.ops4j.pax.url.mvn.repositories", buildString(repositories));
}
}
/**
* Copy dependencies specified as carbon OSGi-lib option in system to the LIB_DIRECTORY.
*
* @param carbonHome carbon home dir
*/
private void copyOSGiLibBundles(Path carbonHome) {
Path targetDirectory = carbonHome.resolve(LIB_DIRECTORY);
Arrays.asList(system.getOptions(CopyOSGiLibBundleOption.class)).forEach(option -> {
try {
copyReferencedArtifactsToDeployDirectory(option.getMavenArtifactUrlReference().getURL(),
targetDirectory);
} catch (IOException e) {
throw new TestContainerException(String.format("Error while copying artifacts to " + LIB_DIRECTORY), e);
}
});
}
/**
* Helper method to copy artifacts to the target directory.
*
* @param url url of the artifact
* @param targetDirectory target directory
*/
private void copyReferencedArtifactsToDeployDirectory(String url, Path targetDirectory) throws IOException {
File target = createUnique(url, targetDirectory.toFile());
FileUtils.copyURLToFile(new URL(url), target);
}
/**
* Create unique id to the file.
*
* @param url url of the artifact
* @param deploy deploy directory
* @return file
*/
private File createUnique(String url, File deploy) {
String prefix = UUID.randomUUID().toString();
String fileName = new File(url).getName();
return new File(deploy, prefix + "_" + fileName + ".jar");
}
/**
* Copy files specified in the carbon file copy option to the destination path.
*
* @param carbonHome carbon home
*/
private void copyFiles(Path carbonHome) {
Arrays.asList(system.getOptions(CopyFileOption.class)).forEach(option -> {
try {
Files.copy(option.getSourcePath(), carbonHome.resolve(option.getDestinationPath()),
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new TestContainerException("Error while copying configuration files", e);
}
});
}
/**
* Setup system properties.
*
* @param options options
* @throws IOException
*/
private void setupSystemProperties(List<String> options, ExamSystem examSystem) throws IOException {
Arrays.asList(examSystem.getOptions(SystemPropertyOption.class)).forEach(systemPropertyOption -> {
String property = String.format("-D%s=%s", systemPropertyOption.getKey(), systemPropertyOption.getValue());
options.add(property);
});
}
/**
* Make all the files in the bin directory to be executable.
*
* @param carbonBin carbonBin
*/
private void makeFilesInBinExec(File carbonBin) {
if (!carbonBin.exists()) {
return;
}
File[] files = carbonBin.listFiles();
if (files != null) {
Arrays.asList(files).forEach(file -> file.setExecutable(true));
}
}
/**
* Generate a random path if the unpack directory is not specified.
*
* @return the path to the carbon home directory
* @throws IOException
*/
private Path retrieveFinalTargetDirectory() throws IOException {
Path unpackDirectory = carbonHomeDirectoryOption.getUnpackDirectory();
if (unpackDirectory == null) {
unpackDirectory = Paths.get("target", UUID.randomUUID().toString());
}
boolean isCreated = unpackDirectory.toFile().exists() || unpackDirectory.toFile().mkdir();
if (!isCreated) {
throw new TestContainerException("Couldn't create the directory: " + unpackDirectory.toFile().toString());
}
return unpackDirectory;
}
/**
* Check whether the container should delete the test directories or not.
*
* @return boolean
*/
private boolean shouldDeleteRuntime() {
boolean deleteRuntime = true;
KeepDirectoryOption keepDirectoryOption = system.getSingleOption(KeepDirectoryOption.class);
if (keepDirectoryOption != null) {
deleteRuntime = false;
}
return deleteRuntime;
}
/**
* Helper method to build string using options.
*
* @param options options to convert to string
* @return the string build by options
*/
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();
Arrays.asList(prepend).forEach(s -> {
builder.append(s);
builder.append(",");
});
Arrays.asList(options).forEach(option -> {
builder.append(option.getValue());
builder.append(",");
});
Arrays.asList(append).forEach(s -> {
builder.append(s);
builder.append(",");
});
if (builder.length() > 0) {
return builder.substring(0, builder.length() - 1);
} else {
return "";
}
}
/**
* Stops the regression container.
*
* @return this container object for api
*/
@Override
public synchronized TestContainer stop() {
logger.debug("Shutting down the test container.");
try {
if (started) {
target.stop();
RemoteBundleContextClient remoteBundleContextClient = target.getClientRBC();
if (remoteBundleContextClient != null) {
remoteBundleContextClient.stop();
}
runner.shutdown();
try {
UnicastRemoteObject.unexportObject(registry, true);
} catch (NoSuchObjectException exc) {
throw new TestContainerException(exc);
}
} else {
throw new TestContainerException("Container never started.");
}
} finally {
started = false;
target = null;
if (shouldDeleteRuntime()) {
system.clear();
try {
FileUtils.forceDelete(targetDirectory.toFile());
} catch (IOException e) {
forceCleanup();
}
}
}
return this;
}
/**
* Force cleanup directories if required.
*/
private void forceCleanup() {
try {
FileUtils.forceDeleteOnExit(targetDirectory.toFile());
} catch (IOException e) {
logger.error("Error occured when deleting the Directory.", e);
}
}
/**
* Wait for the bundle to reach for the specified state.
*
* @param bundleId id of the bundle
* @param state state of the bundle
* @param timeout timeout period
*/
private synchronized void waitForState(final long bundleId, final int state, final RelativeTimeout timeout) {
target.getClientRBC().waitForState(bundleId, state, timeout);
}
@Override
public synchronized void call(TestAddress address) {
target.call(address);
}
@Override
public synchronized long install(InputStream stream) {
return install("local", stream);
}
@Override
public synchronized long install(String location, InputStream stream) {
return target.install(location, stream);
}
@Override
public synchronized long installProbe(InputStream stream) {
return target.installProbe(stream);
}
@Override
public synchronized void uninstallProbe() {
target.uninstallProbe();
}
@Override
public String toString() {
return "CarbonTestContainer";
}
}