/*
* Copyright 2010 Toni Menzel.
*
* 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.spi;
import static org.ops4j.pax.exam.OptionUtils.combine;
import static org.ops4j.pax.exam.OptionUtils.expand;
import static org.ops4j.pax.exam.OptionUtils.filter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import java.util.UUID;
import org.ops4j.io.FileUtils;
import org.ops4j.pax.exam.ConfigurationFactory;
import org.ops4j.pax.exam.ExamSystem;
import org.ops4j.pax.exam.Info;
import org.ops4j.pax.exam.Option;
import org.ops4j.pax.exam.RelativeTimeout;
import org.ops4j.pax.exam.TestContainerException;
import org.ops4j.pax.exam.TestProbeBuilder;
import org.ops4j.pax.exam.options.TimeoutOption;
import org.ops4j.pax.exam.options.WarProbeOption;
import org.ops4j.pax.exam.options.extra.CleanCachesOption;
import org.ops4j.pax.exam.options.extra.WorkingDirectoryOption;
import org.ops4j.pax.exam.spi.intern.TestProbeBuilderImpl;
import org.ops4j.pax.exam.spi.war.WarTestProbeBuilderImpl;
import org.ops4j.spi.ServiceProviderFinder;
import org.ops4j.store.Store;
import org.ops4j.store.intern.TemporaryStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@literal DefaultExamSystem} represents the default implementation of {@link ExamSystem}.
*
* It takes care of options (including implicit defaults), temporary folders (and their cleanup) and
* cross cutting parameters that are frequently used like "timeout" values.
*/
public class DefaultExamSystem implements ExamSystem {
private static final Logger LOG = LoggerFactory.getLogger(DefaultExamSystem.class);
/** Maximum loop count when creating temp directories. */
private static final int TEMP_DIR_ATTEMPTS = 10000;
private final Store<InputStream> store;
private final File configDirectory;
private final Stack<ExamSystem> subsystems;
private final RelativeTimeout timeout;
private final Set<Class<?>> requestedOptionTypes = new HashSet<Class<?>>();
private final CleanCachesOption clean;
private final File cache;
private Option[] combinedOptions;
/**
* Creates a fresh ExamSystem. Your options will be combined with internal defaults. If you need
* to change the system after it has been created, you fork() it. Forking will not add default
* options again.
*
* @param options
* options to be used to define the new system.
*
* @throws IOException
* in case of an instantiation problem. (IO related)
*/
private DefaultExamSystem(Option[] options) throws IOException {
subsystems = new Stack<ExamSystem>();
combinedOptions = expand(options);
configDirectory = new File(System.getProperty("user.home") + "/.pax/exam/");
configDirectory.mkdirs();
WorkingDirectoryOption work = getSingleOption(WorkingDirectoryOption.class);
// make sure that working directory gets propagated to forked systems
if (work == null) {
work = new WorkingDirectoryOption(createTemp(null).getAbsolutePath());
combinedOptions = combine(combinedOptions, work);
}
cache = createTemp(new File(work.getWorkingDirectory()));
store = new TemporaryStore(cache, false);
TimeoutOption timeoutOption = getSingleOption(TimeoutOption.class);
if (timeoutOption != null) {
timeout = new RelativeTimeout(timeoutOption.getTimeout());
}
else {
timeout = RelativeTimeout.TIMEOUT_DEFAULT;
}
clean = getSingleOption(CleanCachesOption.class);
}
/**
* Creates a fresh ExamSystem. Your options will be combined with internal defaults. If you need
* to change the system after it has been created, you fork() it. Forking will not add default
* options again.
*
* @param options
* options to be used to define the new system.
*
* @return a fresh instance of {@literal DefaultExamSystem}
*
* @throws IOException
* in case of an instantiation problem. (IO related)
*/
public static ExamSystem create(Option[] options) throws IOException {
LOG.info("Pax Exam System (Version: " + Info.getPaxExamVersion() + ") created.");
return new DefaultExamSystem(options);
}
/**
* Create a new system based on *this*. The forked System remembers the forked instances in
* order to clear resources up (if desired).
*/
@Override
public ExamSystem fork(Option[] options) {
try {
ExamSystem sys = new DefaultExamSystem(combine(combinedOptions, options));
subsystems.add(sys);
return sys;
}
catch (IOException exc) {
throw new TestContainerException(exc);
}
}
/**
* Creates a fresh temp folder under the mentioned working folder. If workingFolder is null,
* system wide temp location will be used.
*
* @param workingDirectory
*/
private synchronized File createTemp(File workingDirectory) throws IOException {
if (workingDirectory == null) {
return createTempDir();
}
else {
workingDirectory.mkdirs();
return workingDirectory;
}
}
/**
* Helper method for single options. Last occurence has precedence. (support overwrite).
*
*/
@Override
public <T extends Option> T getSingleOption(final Class<T> optionType) {
requestedOptionTypes.add(optionType);
T[] filter = filter(optionType, combinedOptions);
if (filter.length > 0) {
return filter[filter.length - 1];
}
else {
return null;
}
}
public <T extends Option> T getSingleOption(final Class<T> optionType, Option[] options) {
T[] filter = filter(optionType, options);
if (filter.length > 0) {
return filter[filter.length - 1];
}
else {
return null;
}
}
@Override
public <T extends Option> T[] getOptions(final Class<T> optionType) {
requestedOptionTypes.add(optionType);
return filter(optionType, combinedOptions);
}
/**
* @return the basic directory that Exam should use to look at user-defaults.
*/
@Override
public File getConfigFolder() {
return configDirectory;
}
/**
* @return the basic directory that Exam should use for all IO write activities.
*/
@Override
public File getTempFolder() {
return cache;
}
/**
* @return a relative indication of how to deal with timeouts.
*/
@Override
public RelativeTimeout getTimeout() {
return timeout;
}
/**
* Clears up resources taken by system (like temporary files).
*/
@Override
public void clear() {
try {
warnUnusedOptions();
if (clean == null || clean.getValue() == Boolean.TRUE) {
for (ExamSystem sys : subsystems) {
sys.clear();
}
FileUtils.delete(cache.getCanonicalFile());
}
}
catch (IOException e) {
// LOG.info("Clearing " + this.toString() + " failed." ,e);
}
}
private void warnUnusedOptions() {
if (subsystems.isEmpty()) {
for (String skipped : findOptionTypes()) {
LOG.warn("Option " + skipped + " has not been recognized.");
}
}
}
private Set<String> findOptionTypes() {
Set<String> missing = new HashSet<String>();
for (Option option : combinedOptions) {
boolean found = false;
for (Class<?> c : requestedOptionTypes) {
if (c.isAssignableFrom(option.getClass())) {
found = true;
break;
}
}
if (!found) {
missing.add(option.getClass().getCanonicalName());
}
}
return missing;
}
@Override
public TestProbeBuilder createProbe() throws IOException {
WarProbeOption warProbeOption = getSingleOption(WarProbeOption.class);
if (warProbeOption == null) {
LOG.debug("creating default probe");
TestProbeBuilderImpl testProbeBuilder = new TestProbeBuilderImpl(cache, store);
testProbeBuilder.setHeader("Bundle-SymbolicName", "PAXEXAM-PROBE-"
+ createID("created probe"));
return testProbeBuilder;
}
else {
ConfigurationFactory configurationFactory = ServiceProviderFinder
.findAnyServiceProvider(ConfigurationFactory.class);
LOG.debug("creating WAR probe");
if (configurationFactory == null) {
return new WarTestProbeBuilderImpl(getTempFolder(), this);
}
else {
Option[] configuration = configurationFactory.createConfiguration();
Option[] tempOptions = combine(combinedOptions, configuration);
warProbeOption = getSingleOption(WarProbeOption.class, tempOptions);
return new WarTestProbeBuilderImpl(getTempFolder(), warProbeOption);
}
}
}
@Override
public String createID(String purposeText) {
return UUID.randomUUID().toString();
}
@Override
public String toString() {
return "ExamSystem:options=" + combinedOptions.length + ";queried="
+ requestedOptionTypes.size();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(combinedOptions);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
DefaultExamSystem other = (DefaultExamSystem) obj;
if (!Arrays.equals(combinedOptions, other.combinedOptions)) {
return false;
}
return true;
}
public static File createTempDir() {
File baseDir = new File(System.getProperty("java.io.tmpdir"));
String baseName = System.currentTimeMillis() + "-";
for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) {
File tempDir = new File(baseDir, baseName + counter);
if (tempDir.mkdir()) {
return tempDir;
}
}
throw new IllegalStateException("Failed to create directory within " + TEMP_DIR_ATTEMPTS
+ " attempts (tried " + baseName + "0 to " + baseName + (TEMP_DIR_ATTEMPTS - 1) + ')');
}
public WarProbeOption getLatestWarProbeOption() {
return subsystems.peek().getSingleOption(WarProbeOption.class);
}
}