com.mgmtp.jfunk.core.scripting.ScriptContext.java Source code

Java tutorial

Introduction

Here is the source code for com.mgmtp.jfunk.core.scripting.ScriptContext.java

Source

/*
 * Copyright (c) 2015 mgm technology partners GmbH
 *
 * 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 com.mgmtp.jfunk.core.scripting;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.newArrayListWithExpectedSize;
import static com.google.common.collect.Sets.newHashSetWithExpectedSize;
import groovy.lang.Closure;

import java.awt.Dimension;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.concurrent.NotThreadSafe;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.swing.JFileChooser;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.eventbus.EventBus;
import com.google.common.io.Files;
import com.google.inject.Injector;
import com.mgmtp.jfunk.common.JFunkConstants;
import com.mgmtp.jfunk.common.config.ScriptScoped;
import com.mgmtp.jfunk.common.config.StackedScope;
import com.mgmtp.jfunk.common.exception.JFunkException;
import com.mgmtp.jfunk.common.random.MathRandom;
import com.mgmtp.jfunk.common.random.RandomCollection;
import com.mgmtp.jfunk.common.random.Randomizable;
import com.mgmtp.jfunk.common.util.Configuration;
import com.mgmtp.jfunk.core.event.AfterCommandEvent;
import com.mgmtp.jfunk.core.event.AfterModuleEvent;
import com.mgmtp.jfunk.core.event.BeforeCommandEvent;
import com.mgmtp.jfunk.core.event.BeforeModuleEvent;
import com.mgmtp.jfunk.core.event.ModuleInitializedEvent;
import com.mgmtp.jfunk.core.mail.MailAccount;
import com.mgmtp.jfunk.core.mail.MailAccountManager;
import com.mgmtp.jfunk.core.module.TestModule;
import com.mgmtp.jfunk.core.reporting.Reporter;
import com.mgmtp.jfunk.core.util.CsvDataProcessor;
import com.mgmtp.jfunk.data.DataSet;
import com.mgmtp.jfunk.data.source.DataSource;

/**
 * <p>
 * Provides the whole logic around scripting in jFunk. Groovy script commands delegate to this
 * class.
 * </p>
 * <p>
 * Delegate methods for script commands are annotated with {@link Cmd}. This will automatically
 * trigger {@link BeforeCommandEvent}s and {@link AfterCommandEvent}s around these methods by a
 * Guice AOP method interceptor.
 * </p>
 * 
 * @author rnaegele
 */
@ScriptScoped
@NotThreadSafe
public class ScriptContext {

    private final Logger log = LoggerFactory.getLogger(getClass());

    private final Set<Reporter> reporters = newHashSetWithExpectedSize(2);
    private final List<Throwable> errors = newArrayListWithExpectedSize(1);
    private final Provider<DataSource> dataSourceProvider;
    private final Configuration config;
    private final MathRandom random;
    private final EventBus eventBus;
    private final Injector injector;
    private final Provider<ModuleBuilder> moduleBuilderProvider;
    private final StackedScope moduleScope;
    private final CsvDataProcessor csvDataProcessor;
    private final Charset defaultCharset;
    private final Provider<MailAccountManager> mailAccountManagerProvider;

    private File script;

    private final Provider<ModuleMetaData> moduleMetaDataProvider;

    private final ScriptMetaData scriptMetaData;

    @Inject
    ScriptContext(final Provider<DataSource> dataSourceProvider, final Configuration config,
            final MathRandom random, final EventBus eventBus, final Injector injector,
            final Provider<ModuleBuilder> moduleBuilderProvider, final StackedScope moduleScope,
            final CsvDataProcessor csvDataProcessor, final Charset charset,
            final Provider<MailAccountManager> mailAccountManagerProvider, final ScriptMetaData scriptMetaData,
            final Provider<ModuleMetaData> moduleMetaDataProvider) {
        this.dataSourceProvider = dataSourceProvider;
        this.config = config;
        this.random = random;
        this.eventBus = eventBus;
        this.injector = injector;
        this.moduleBuilderProvider = moduleBuilderProvider;
        this.moduleScope = moduleScope;
        this.csvDataProcessor = csvDataProcessor;
        this.defaultCharset = charset;
        this.mailAccountManagerProvider = mailAccountManagerProvider;
        this.scriptMetaData = scriptMetaData;
        this.moduleMetaDataProvider = moduleMetaDataProvider;
    }

    /**
     * Sets the script file.
     * 
     * @param script
     *            the script to set
     */
    public void setScript(final File script) {
        this.script = script;
    }

    /**
     * Gets the script directory.
     * 
     * @return the script file
     */
    public String getScriptDir() {
        return script != null ? script.getParentFile().getPath() : null;
    }

    /**
     * Gets the script file.
     * 
     * @return the script file
     */
    public File getScriptFile() {
        return script;
    }

    /**
     * @return the reporters
     */
    public Set<Reporter> getReporters() {
        return reporters;
    }

    /**
     * Registers a reporter.
     * 
     * @param reporter
     *            the reporter
     */
    @Cmd
    public Reporter registerReporter(final Reporter reporter) {
        injector.injectMembers(reporter);
        reporters.add(reporter);
        return reporter;
    }

    /**
     * Opens a file chooser dialog which can then be used to choose a file or directory and assign
     * the path of the chosen object to a variable. The name of the variable must be passed as a
     * parameter.
     * 
     * @param fileKey
     *            the key the selected file path is stored under in the configuration
     * @return the chosen file
     */
    @Cmd
    public File chooseFile(final String fileKey) {
        log.debug("Opening file chooser dialog");
        JFileChooser fileChooser = new JFileChooser(System.getProperty(JFunkConstants.USER_DIR));
        fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
        fileChooser.setPreferredSize(new Dimension(600, 326));
        int fileChooserResult = fileChooser.showOpenDialog(null);

        if (fileChooserResult == JFileChooser.APPROVE_OPTION) {
            File file = fileChooser.getSelectedFile();
            String filename = file.toString();
            log.info("Assigning file path '{}' to property '{}'", filename, fileKey);
            config.put(fileKey, filename);
            return file;
        }

        log.error("No file or directory was chosen, execution will abort");
        throw new IllegalArgumentException("No file or directory was chosen");
    }

    /**
     * Randomly selects an item from a list of Strings. List items may contain placeholder tokens.
     * The result is stored in the configuration under the specified key and also returned by this
     * method.
     * 
     * @param propertyKey
     *            the property key under which to store the result
     * @param randomValues
     *            the list of strings
     * @return a randomly chosen string from the list
     */
    @Cmd
    public String chooseRandom(final String propertyKey, final List<String> randomValues) {
        Randomizable<String> choice = new RandomCollection<>(random, randomValues);
        String currentValue = choice.get();
        if (log.isDebugEnabled()) {
            log.debug("Chosen value: " + currentValue);
        }
        currentValue = resolveProperty(currentValue);
        config.put(propertyKey, currentValue);
        if (log.isDebugEnabled()) {
            log.debug("... value for '{}' was set to {}", propertyKey, currentValue);
        }
        return currentValue;
    }

    /**
     * Copies a {@link DataSet} entry from one {@link DataSet} to another.
     * 
     * @param srcDataSetKey
     *            the source data set key
     * @param srcKey
     *            the source entry key
     * @param destDataSetKey
     *            the destination data set key
     * @param destKey
     *            the destination entry key
     */
    @Cmd
    public void copyFormEntry(final String srcDataSetKey, final String srcKey, final String destDataSetKey,
            final String destKey) {
        Map<String, DataSet> currentDataSets = dataSourceProvider.get().getCurrentDataSets();
        DataSet srcDataSet = currentDataSets.get(srcDataSetKey);
        DataSet destDataSet = currentDataSets.get(destDataSetKey);
        destDataSet.setFixedValue(destKey, srcDataSet.getValue(srcKey));
    }

    /**
     * Executes a {@link Closure} instance representing a block in a Groovy script and expects one
     * of the specified exceptions to be thrown. If no exception is thrown, an
     * {@link IllegalStateException} is thrown, any unexpected exception is re-thrown. If the list
     * of excepted exceptions is empty, any thrown exception is considered a success.
     * 
     * @param exceptionsList
     *            the list of exception classes
     * @param closure
     *            the {@link Closure} representing a Groovy block
     */
    @Cmd
    public void exceptional(final List<Class<? extends Exception>> exceptionsList, final Closure<Void> closure) {
        boolean noExceptionOccurred = false;

        try {
            closure.call();
            noExceptionOccurred = true;
        } catch (final RuntimeException ex) {
            if (exceptionsList.isEmpty()) {
                log.info("Exception " + ex.getClass().getName()
                        + " was thrown but not expected in exceptional block.", ex);
            } else {
                for (Class<? extends Exception> exceptionClass : exceptionsList) {
                    Throwable th = ex;
                    while (th != null) {
                        if (th.getClass() == exceptionClass) {
                            log.info("Expected Exception '{}' was thrown", exceptionClass.getName());
                            return;
                        }
                        th = th.getCause();
                    }
                    log.info("Expected Exception '{}' was not thrown", exceptionClass.getName());
                }
                log.error("None of the expected exceptions were thrown.", ex);
                throw ex;
            }
        }

        if (noExceptionOccurred) {
            throw new IllegalStateException("No exception occurred within exceptional block.");
        }
    }

    /**
     * Prepares the next {@link DataSet} with the specified key for use. Depending on the type of
     * {@link DataSource}, this may trigger data generation.
     * 
     * @param dataSetKey
     *            the key of the {@link DataSet}
     * @deprecated use {@link #prepareNextDataSet(String)} instead
     */
    @Cmd
    @Deprecated
    public void generate(final String dataSetKey) {
        prepareNextDataSet(dataSetKey);
    }

    /**
     * Prepares the next {@link DataSet} with the specified key for use. Depending on the type of
     * {@link DataSource}, this may trigger data generation.
     * 
     * @param dataSetKey
     *            the key of the {@link DataSet}
     * @since 3.1.0
     */
    @Cmd
    public void prepareNextDataSet(final String dataSetKey) {
        checkArgument(dataSetKey != null,
                "Global 'prepareNextDataSet' is not allowed. Please specify a data set key.");
        log.info("Preparing new data for dataSetKey={}", dataSetKey);
        dataSourceProvider.get().getNextDataSet(resolveProperty(dataSetKey));
    }

    /**
     * Gets the value for the specified key from the configuration resolving any placeholders tokens
     * within.
     * 
     * @param configKey
     *            the config key
     * @return the config value
     */
    @Cmd
    public String get(final String configKey) {
        return resolveProperty(configKey);
    }

    /**
     * Loads the properties file for the test. This file may reference further configuration files,
     * which are searched relatively to the configuration directory. These extra files must have
     * keys starting with {@code system.properties.}.
     * 
     * @param fileName
     *            the name of the properties file, relative to jFunk's configuration directory
     * @param preserveExistingProps
     *            if {@code true}, already existing properties are preserved
     */
    @Cmd
    public void load(final String fileName, final boolean preserveExistingProps) {
        config.load(fileName, preserveExistingProps);
        String scriptDir = getScriptDir();
        if (scriptDir != null) {
            config.put(JFunkConstants.SCRIPT_DIR, scriptDir);
            config.put(JFunkConstants.SCRIPT_NAME, script.getName());
        }
        config.put(JFunkConstants.THREAD_ID, Thread.currentThread().getName());
    }

    /**
     * Logs the specified message at info level.
     * 
     * @param message
     *            the log message
     */
    @Cmd
    public void log(final String message) {
        log.info(resolveProperty(message));
    }

    /**
     * Creates and runs a dynamic script module.
     * 
     * @param moduleName
     *            the name of the module
     * @param attributes
     *            a map of module attributes; the only attribute that is evaluated is "dataSetKey"
     * @param closure
     *            the Groovy closure containing the logic to be executed within the script module
     */
    @Cmd
    public void module(final String moduleName, final Map<?, ?> attributes, final Closure<Void> closure) {
        moduleBuilderProvider.get().invokeMethod("module", newArrayList(moduleName, attributes, closure));
    }

    /**
     * Executes the specified closure if at least one exception had previously been recorded in the
     * currently executing script.
     * 
     * @param closure
     *            the {@link Closure} representing a Groovy block
     */
    @Cmd
    public void onError(final Closure<Void> closure) {
        int errorCount = errors.size();
        if (errorCount > 0) {
            log.info(errorCount + " error" + (errorCount == 1 ? "" : "s") + " in list --> executing block");
            closure.call();
            log.info("Finished OnError block");
        } else {
            log.info("No errors in list --> skipping block");
        }
    }

    /**
     * Runs the specified closure catching any exception that might occur during execution.
     * 
     * @param closure
     *            the {@link Closure} representing a Groovy block
     */
    @Cmd
    public void optional(final Closure<Void> closure) {
        log.info("Executing optional block ...");
        try {
            closure.call();
        } catch (final Exception ex) {
            log.error("Exception executing optional block: " + ex.getMessage(), ex);
            errors.add(ex);
        } catch (AssertionError err) {
            log.error("Assertion failed executing optional block: " + err.getMessage(), err);
            errors.add(err);
        }
        log.info("... finished execution of optional block");
    }

    /**
     * Processes a CSV file.
     * 
     * @param csvFile
     *            the file
     * @param delimiter
     *            the
     * @param quoteChar
     *            the quote character ('\0' for no quoting)
     * @param charset
     *            the character set
     * @param closure
     *            the {@link Closure} representing a Groovy block
     */
    @Cmd
    public void processCsvFile(final String csvFile, final String delimiter, final char quoteChar,
            final Charset charset, final Closure<Void> closure) {
        File f = new File(csvFile);
        try {
            config.extractFromArchive(f, true);
            checkState(f.exists(), "CSV file not found: " + f);
            Reader reader = Files.newReader(f, charset == null ? defaultCharset : charset);
            csvDataProcessor.processFile(reader, delimiter, quoteChar, closure);
        } catch (IOException ex) {
            throw new IllegalStateException("Error reading CSV file: " + f, ex);
        }
    }

    /**
     * Prompts for closure-line input. the input is stored in the configuration under the specified
     * key.
     * 
     * @param configKey
     *            the key for storing the input in the configuration
     * @param message
     *            the prompt message
     * @return the input
     */
    @Cmd
    public String prompt(final String configKey, final String message) {
        System.out.print(resolveProperty(message) + " "); //NOSONAR
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        try {
            String value = StringUtils.trim(in.readLine());
            config.put(configKey, value);
            return value;
            // Stream nicht schliessen
        } catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

    /**
     * Clears the internal errors list.
     */
    @Cmd
    public void resetError() {
        log.info("Clearing errors");
        errors.clear();
    }

    /**
     * Resets fixed values in the data source for the {@link DataSet} with the specified key and the
     * entry with the specified key.
     * 
     * @param dataSetKey
     *            if {@code null}, all fixed values are reset
     * @param entryKey
     *            if {@code null}, all fixed values in the {@link DataSet} are reset
     */
    @Cmd
    public void resetFixedData(final String dataSetKey, final String entryKey) {
        if (dataSetKey == null) {
            log.info("Resetting all fixed values.");
            dataSourceProvider.get().resetFixedValues();
            return;
        }

        String resolvedDataSetKey = resolveProperty(dataSetKey);
        if (entryKey == null) {
            log.info("Resetting fixed values for data set '{}'", resolvedDataSetKey);
            dataSourceProvider.get().resetFixedValues(resolvedDataSetKey);
            return;
        }

        String resolvedEntryKey = resolveProperty(entryKey);
        log.info("Resetting fixed value for data set '{}' and entry '{}'", resolvedDataSetKey, resolvedEntryKey);
        dataSourceProvider.get().resetFixedValue(resolvedDataSetKey, resolvedEntryKey);
    }

    /**
     * Instantiates and runs a {@link TestModule}.
     * 
     * @param testModuleClassName
     *            the fully qualified class name of the {@link TestModule}
     */
    @Cmd
    public void run(final String testModuleClassName) {
        doRun(createModuleInstance(testModuleClassName));
    }

    /**
     * Runs the specified {@link TestModule}.
     * 
     * @param testModule
     *            the test module
     */
    @Cmd
    public void run(final TestModule testModule) {
        doRun(testModule);
    }

    void doRun(final TestModule testModule) {
        Throwable throwable = null;
        TestModule moduleToRun = testModule;

        try {
            moduleScope.enterScope();

            // perform DI on test module instance
            injector.injectMembers(moduleToRun);

            ModuleMetaData moduleMetaData = moduleMetaDataProvider.get();
            moduleMetaData.setScriptMetaData(scriptMetaData);
            moduleMetaData.setStartDate(new Date());
            moduleMetaData.setModuleClass(moduleToRun.getClass());
            moduleMetaData.setModuleName(moduleToRun.getName());
            scriptMetaData.addModuleMetaData(moduleMetaData);

            eventBus.post(new ModuleInitializedEvent(moduleToRun));
            eventBus.post(new InternalBeforeModuleEvent(moduleToRun));
            eventBus.post(new BeforeModuleEvent(moduleToRun));
            moduleToRun.execute();
        } catch (AssertionError err) {
            throwable = err;

            // We need to log the exception here on module level,
            // so it makes it into the log file in the module's archive
            log.error("Assertion failed in module: " + moduleToRun.getName(), err);

            // Wrap into ModuleExecutionException, so we know later that
            // we don't have to log it again.
            throw new ModuleExecutionException(moduleToRun, err);
        } catch (Throwable th) {
            // We have to catch throwable to make sure any error makes it into a test's archive
            throwable = th;

            // We need to log the exception here on module level,
            // so it makes it into the log file in the module's archive
            log.error("Error executing module: " + moduleToRun.getName(), th);

            // Wrap into ModuleExecutionException, so we know later that
            // we don't have to log it again.
            throw new ModuleExecutionException(moduleToRun, th);
        } finally {
            try {
                moduleToRun.setError(throwable != null);

                ModuleMetaData moduleMetaData = moduleMetaDataProvider.get();
                moduleMetaData.setEndDate(new Date());
                moduleMetaData.setThrowable(throwable);

                eventBus.post(new AfterModuleEvent(moduleToRun, throwable));
                eventBus.post(new InternalAfterModuleEvent(moduleToRun, throwable));
            } finally {
                moduleScope.exitScope();
            }
        }
    }

    private TestModule createModuleInstance(final String moduleClassName) {
        try {
            Class<? extends TestModule> moduleClass = Class.forName(moduleClassName).asSubclass(TestModule.class);
            Constructor<? extends TestModule> constructor = null;
            try {
                constructor = moduleClass.getConstructor();
                return constructor.newInstance();
            } catch (NoSuchMethodException ex) {
                constructor = moduleClass.getConstructor();
                return constructor.newInstance();
            }
        } catch (Exception ex) {
            throw new JFunkException("Error instantiating module from config: " + moduleClassName, ex);
        }
    }

    @Cmd
    public void resetDataSource() {
        dataSourceProvider.get().reset();
    }

    /**
     * Sets a {@link DataSet} entry. In order to affect data generation, this method must be called
     * before calling {@link #generate(String)}.
     * 
     * @param dataSetKey
     *            the {@link DataSet} key
     * @param entryKey
     *            the key of the entry in the specified {@link DataSet}
     * @param value
     *            the value to set
     */
    @Cmd
    public void setFormEntry(final String dataSetKey, final String entryKey, final String value) {
        doSetFormEntry(dataSetKey, entryKey, value);
    }

    /**
     * Sets a {@link DataSet} entry. In order to affect data generation, this method must be called
     * before calling {@link #generate(String)}.
     * 
     * @param dataSetKey
     *            the {@link DataSet} key
     * @param entryKey
     *            the key of the entry in the specified {@link DataSet}
     * @param value
     *            the value to set
     */
    @Cmd
    public void setFormEntry(final String dataSetKey, final String entryKey, final boolean value) {
        doSetFormEntry(dataSetKey, entryKey, String.valueOf(value));
    }

    /**
     * Sets a {@link DataSet} entry. In order to affect data generation, this method must be called
     * before calling {@link #generate(String)}.
     * 
     * @param dataSetKey
     *            the {@link DataSet} key
     * @param entryKey
     *            the key of the entry in the specified {@link DataSet}
     * @param value
     *            the value to set
     */
    @Cmd
    public void setFormEntry(final String dataSetKey, final String entryKey, final int value) {
        doSetFormEntry(dataSetKey, entryKey, String.valueOf(value));
    }

    /**
     * Sets a {@link DataSet} entry. In order to affect data generation, this method must be called
     * before calling {@link #generate(String)}.
     * 
     * @param dataSetKey
     *            the {@link DataSet} key
     * @param entryKey
     *            the key of the entry in the specified {@link DataSet}
     * @param value
     *            the value to set
     */
    @Cmd
    public void setFormEntry(final String dataSetKey, final String entryKey, final long value) {
        doSetFormEntry(dataSetKey, entryKey, String.valueOf(value));
    }

    void doSetFormEntry(final String dataSetKey, final String entryKey, final String value) {
        String resolvedDataSetKey = resolveProperty(dataSetKey);
        String resolvedEntryKey = resolveProperty(entryKey);
        String resolvedValue = resolveProperty(value);

        log.debug("Setting entry '{}' in data set '{}' to fixed value '{}'", resolvedEntryKey, resolvedDataSetKey,
                resolvedValue);
        dataSourceProvider.get().setFixedValue(resolvedDataSetKey, resolvedEntryKey, resolvedValue);
    }

    /**
     * Sets a property to a given value. Both key and value can contain properties. The following
     * rules apply:
     * <ul>
     * <li>the key will be evaluated on execution of the closure</li>
     * <li>the value will be evaluated on querying the property</li>
     * <li>if the value should also be evaluated on execution you need to use
     * {@link #setNow(String, String)}</li>
     * </ul>
     * 
     * @param key
     *            the config key
     * @param value
     *            the value to set
     */
    @Cmd
    public void set(final String key, final String value) {
        doSet(key, value);
    }

    /**
     * Sets a property to a given value. Both key and value can contain properties. The following
     * rules apply:
     * <ul>
     * <li>the key will be evaluated on execution of the closure</li>
     * <li>the value will be evaluated on querying the property</li>
     * <li>if the value should also be evaluated on execution you need to use
     * {@link #setNow(String, String)}</li>
     * </ul>
     * 
     * @param key
     *            the config key
     * @param value
     *            the value to set
     */
    @Cmd
    public void set(final String key, final int value) {
        doSet(key, String.valueOf(value));
    }

    /**
     * Sets a property to a given value. Both key and value can contain properties. The following
     * rules apply:
     * <ul>
     * <li>the key will be evaluated on execution of the closure</li>
     * <li>the value will be evaluated on querying the property</li>
     * <li>if the value should also be evaluated on execution you need to use
     * {@link #setNow(String, String)}</li>
     * </ul>
     * 
     * @param key
     *            the config key
     * @param value
     *            the value to set
     */
    @Cmd
    public void set(final String key, final boolean value) {
        doSet(key, String.valueOf(value));
    }

    /**
     * Sets a property to a given value. Both key and value can contain properties. The following
     * rules apply:
     * <ul>
     * <li>the key will be evaluated on execution of the closure</li>
     * <li>the value will be evaluated on querying the property</li>
     * <li>if the value should also be evaluated on execution you need to use
     * {@link #setNow(String, String)}</li>
     * </ul>
     * 
     * @param key
     *            the config key
     * @param value
     *            the value to set
     */
    @Cmd
    public void set(final String key, final long value) {
        doSet(key, String.valueOf(value));
    }

    /**
     * Sets a property to a given value. Both key and value can contain properties. The following
     * rules apply:
     * <ul>
     * <li>the key will be evaluated on execution of the closure</li>
     * <li>the value will be evaluated on execution of the closure</li>
     * <li>if the value should be evaluated on querying the property you need to use
     * {@link #set(String, String)}</li>
     * </ul>
     * 
     * @param key
     *            the config key
     * @param value
     *            the value to set
     */
    @Cmd
    public void setNow(final String key, final String value) {
        String resolvedValue = resolveProperty(value);
        doSet(key, resolvedValue);
    }

    /**
     * Sets a property to a given value. Both key and value can contain properties. The following
     * rules apply:
     * <ul>
     * <li>the key will be evaluated on execution of the closure</li>
     * <li>the value will be evaluated on execution of the closure</li>
     * <li>if the value should be evaluated on querying the property you need to use
     * {@link #set(String, String)}</li>
     * </ul>
     * 
     * @param key
     *            the config key
     * @param value
     *            the value to set
     */
    @Cmd
    public void setNow(final String key, final long value) {
        String resolvedValue = resolveProperty(String.valueOf(value));
        doSet(key, resolvedValue);
    }

    /**
     * Sets a property to a given value. Both key and value can contain properties. The following
     * rules apply:
     * <ul>
     * <li>the key will be evaluated on execution of the closure</li>
     * <li>the value will be evaluated on execution of the closure</li>
     * <li>if the value should be evaluated on querying the property you need to use
     * {@link #set(String, String)}</li>
     * </ul>
     * 
     * @param key
     *            the config key
     * @param value
     *            the value to set
     */
    @Cmd
    public void setNow(final String key, final int value) {
        String resolvedValue = resolveProperty(String.valueOf(value));
        doSet(key, resolvedValue);
    }

    /**
     * Sets a property to a given value. Both key and value can contain properties. The following
     * rules apply:
     * <ul>
     * <li>the key will be evaluated on execution of the closure</li>
     * <li>the value will be evaluated on execution of the closure</li>
     * <li>if the value should be evaluated on querying the property you need to use
     * {@link #set(String, String)}</li>
     * </ul>
     * 
     * @param key
     *            the config key
     * @param value
     *            the value to set
     */
    @Cmd
    public void setNow(final String key, final boolean value) {
        String resolvedValue = resolveProperty(String.valueOf(value));
        doSet(key, resolvedValue);
    }

    void doSet(final String key, final String value) {
        String resolvedKey = resolveProperty(key);
        if (value == null) {
            log.info("Removing property " + resolvedKey);
            config.remove(resolvedKey);
        } else {
            log.info("Setting property " + resolvedKey + " = " + value);
            config.put(resolvedKey, resolveProperty(value));
        }
    }

    /**
     * Sets a {@link DataSet} entry from a configuration property.
     * 
     * @param configProperty
     *            the configuration key
     * @param dataSetKey
     *            the {@link DataSet} key
     * @param entryKey
     *            the key of the {@link DataSet} entry
     */
    @Cmd
    public void setToFormEntry(final String configProperty, final String dataSetKey, final String entryKey) {
        String resolvedConfigProperty = resolveProperty(configProperty);
        String resolvedDataSetKey = resolveProperty(dataSetKey);
        String resolvedEntryKey = resolveProperty(entryKey);

        DataSet dataSet = dataSourceProvider.get().getCurrentDataSet(resolvedDataSetKey);
        if (dataSet == null) {
            throw new IllegalStateException("DataSet " + resolvedDataSetKey + " was null.");
        }
        String value = dataSet.getValue(resolvedEntryKey);

        log.info("Setting property '{}' to '{}'", resolvedConfigProperty, value);
        config.put(resolvedConfigProperty, value);
    }

    private String resolveProperty(final String configProperty) {
        return config.processPropertyValue(configProperty);
    }

    /**
     * @see MailAccountManager#reserveMailAccount()
     */
    @Cmd
    public MailAccount reserveMailAccount() {
        return mailAccountManagerProvider.get().reserveMailAccount();
    }

    /**
     * @see MailAccountManager#reserveMailAccount(java.lang.String)
     */
    @Cmd
    public MailAccount reserveMailAccount(final String accountReservationKey) {
        return mailAccountManagerProvider.get().reserveMailAccount(accountReservationKey);
    }

    /**
     * @see MailAccountManager#reserveMailAccount(java.lang.String, java.lang.String)
     */
    @Cmd
    public MailAccount reserveMailAccount(final String accountReservationKey, final String pool) {
        return mailAccountManagerProvider.get().reserveMailAccount(accountReservationKey, pool);
    }

    /**
     * 
     * @see MailAccountManager#releaseAllMailAccountsForThread()
     */
    @Cmd
    public void releaseAllMailAccountsForThread() {
        mailAccountManagerProvider.get().releaseAllMailAccountsForThread();
    }

    /**
     * @see MailAccountManager#releaseMailAccountForThread(MailAccount)
     */
    @Cmd
    public void releaseMailAccountForThread(final MailAccount account) {
        mailAccountManagerProvider.get().releaseMailAccountForThread(account);
    }

    /**
     * @see MailAccountManager#releaseMailAccountForThread(java.lang.String)
     */
    @Cmd
    public void releaseMailAccountForThread(final String accountReservationKey) {
        mailAccountManagerProvider.get().releaseMailAccountForThread(accountReservationKey);
    }
}