com.mirth.connect.server.controllers.DefaultConfigurationController.java Source code

Java tutorial

Introduction

Here is the source code for com.mirth.connect.server.controllers.DefaultConfigurationController.java

Source

/*
 * Copyright (c) Mirth Corporation. All rights reserved.
 * 
 * http://www.mirthcorp.com
 * 
 * The software in this package is published under the terms of the MPL license a copy of which has
 * been included with this distribution in the LICENSE.txt file.
 */

package com.mirth.connect.server.controllers;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.sql.Connection;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.SortedMap;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.UUID;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.net.ssl.SSLSocketFactory;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.configuration.ConfigurationConverter;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.PropertiesConfigurationLayout;
import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.mail.Email;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.SimpleEmail;
import org.apache.log4j.Logger;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.mirth.commons.encryption.Digester;
import com.mirth.commons.encryption.Encryptor;
import com.mirth.commons.encryption.KeyEncryptor;
import com.mirth.commons.encryption.Output;
import com.mirth.connect.client.core.ControllerException;
import com.mirth.connect.donkey.server.StartException;
import com.mirth.connect.donkey.server.StopException;
import com.mirth.connect.donkey.server.data.DonkeyStatisticsUpdater;
import com.mirth.connect.donkey.util.DonkeyElement;
import com.mirth.connect.model.Channel;
import com.mirth.connect.model.ChannelDependency;
import com.mirth.connect.model.ChannelGroup;
import com.mirth.connect.model.CodeTemplate;
import com.mirth.connect.model.CodeTemplateLibrary;
import com.mirth.connect.model.DatabaseSettings;
import com.mirth.connect.model.DriverInfo;
import com.mirth.connect.model.EncryptionSettings;
import com.mirth.connect.model.LibraryProperties;
import com.mirth.connect.model.PasswordRequirements;
import com.mirth.connect.model.PluginMetaData;
import com.mirth.connect.model.ResourceProperties;
import com.mirth.connect.model.ResourcePropertiesList;
import com.mirth.connect.model.ServerConfiguration;
import com.mirth.connect.model.ServerEventContext;
import com.mirth.connect.model.ServerSettings;
import com.mirth.connect.model.UpdateSettings;
import com.mirth.connect.model.alert.AlertModel;
import com.mirth.connect.model.converters.DocumentSerializer;
import com.mirth.connect.model.converters.ObjectXMLSerializer;
import com.mirth.connect.plugins.directoryresource.DirectoryResourceProperties;
import com.mirth.connect.server.ExtensionLoader;
import com.mirth.connect.server.mybatis.KeyValuePair;
import com.mirth.connect.server.tools.ClassPathResource;
import com.mirth.connect.server.util.DatabaseUtil;
import com.mirth.connect.server.util.PasswordRequirementsChecker;
import com.mirth.connect.server.util.ResourceUtil;
import com.mirth.connect.server.util.SqlConfig;
import com.mirth.connect.util.ChannelDependencyException;
import com.mirth.connect.util.ChannelDependencyGraph;
import com.mirth.connect.util.ConfigurationProperty;
import com.mirth.connect.util.ConnectionTestResponse;
import com.mirth.connect.util.MigrationUtil;
import com.mirth.connect.util.MirthSSLUtil;

/**
 * The ConfigurationController provides access to the Mirth configuration.
 * 
 */
public class DefaultConfigurationController extends ConfigurationController {
    public static final String PROPERTIES_CORE = "core";
    public static final String PROPERTIES_RESOURCES = "resources";
    public static final String PROPERTIES_DEPENDENCIES = "channelDependencies";
    public static final String SECRET_KEY_ALIAS = "encryption";

    private Logger logger = Logger.getLogger(this.getClass());
    private String appDataDir = null;
    private String baseDir = null;
    private String configurationFile = null;
    private static String serverId = null;
    private String serverName = null;
    private int status = ConfigurationController.STATUS_UNAVAILABLE;
    private ScriptController scriptController = ControllerFactory.getFactory().createScriptController();
    private PasswordRequirements passwordRequirements;
    private String[] httpsClientProtocols;
    private String[] httpsServerProtocols;
    private String[] httpsCipherSuites;
    private boolean startupDeploy;
    private volatile Map<String, String> configurationMap = Collections
            .unmodifiableMap(new HashMap<String, String>());
    private volatile Map<String, String> commentMap = Collections.unmodifiableMap(new HashMap<String, String>());
    private static PropertiesConfiguration versionConfig = new PropertiesConfiguration();
    private static PropertiesConfiguration mirthConfig = new PropertiesConfiguration();
    private static EncryptionSettings encryptionConfig;
    private static DatabaseSettings databaseConfig;
    private static String apiBypassword;
    private static int statsUpdateInterval;

    private static KeyEncryptor encryptor = null;
    private static Digester digester = null;

    private static final String CHARSET = "ca.uhn.hl7v2.llp.charset";
    private static final String PROPERTY_TEMP_DIR = "dir.tempdata";
    private static final String PROPERTY_APP_DATA_DIR = "dir.appdata";
    private static final String CONFIGURATION_MAP_PATH = "configurationmap.path";
    private static final String HTTPS_CLIENT_PROTOCOLS = "https.client.protocols";
    private static final String HTTPS_SERVER_PROTOCOLS = "https.server.protocols";
    private static final String HTTPS_CIPHER_SUITES = "https.ciphersuites";
    private static final String STARTUP_DEPLOY = "server.startupdeploy";
    private static final String API_BYPASSWORD = "server.api.bypassword";
    private static final String STATS_UPDATE_INTERVAL = "donkey.statsupdateinterval";

    // singleton pattern
    private static ConfigurationController instance = null;

    private DefaultConfigurationController() {

    }

    public static ConfigurationController create() {
        synchronized (DefaultConfigurationController.class) {
            if (instance == null) {
                instance = ExtensionLoader.getInstance().getControllerInstance(ConfigurationController.class);

                if (instance == null) {
                    instance = new DefaultConfigurationController();
                    ((DefaultConfigurationController) instance).initialize();
                }
            }

            return instance;
        }
    }

    private void initialize() {
        try {
            // Disable delimiter parsing so getString() returns the whole
            // property, even if there are commas
            mirthConfig.setDelimiterParsingDisabled(true);
            mirthConfig.setFile(new File(ClassPathResource.getResourceURI("mirth.properties")));
            mirthConfig.load();

            MigrationController.getInstance().migrateConfiguration(mirthConfig);

            // load the server version
            versionConfig.setDelimiterParsingDisabled(true);
            InputStream versionPropertiesStream = ResourceUtil.getResourceStream(this.getClass(),
                    "version.properties");
            versionConfig.load(versionPropertiesStream);
            IOUtils.closeQuietly(versionPropertiesStream);

            if (mirthConfig.getString(PROPERTY_TEMP_DIR) != null) {
                File tempDataDirFile = new File(mirthConfig.getString(PROPERTY_TEMP_DIR));

                if (!tempDataDirFile.exists()) {
                    if (tempDataDirFile.mkdirs()) {
                        logger.debug("created tempdir: " + tempDataDirFile.getAbsolutePath());
                    } else {
                        logger.error("error creating tempdir: " + tempDataDirFile.getAbsolutePath());
                    }
                }

                System.setProperty("java.io.tmpdir", tempDataDirFile.getAbsolutePath());
                logger.debug("set temp data dir: " + tempDataDirFile.getAbsolutePath());
            }

            File appDataDirFile = null;

            if (mirthConfig.getString(PROPERTY_APP_DATA_DIR) != null) {
                appDataDirFile = new File(mirthConfig.getString(PROPERTY_APP_DATA_DIR));

                if (!appDataDirFile.exists()) {
                    if (appDataDirFile.mkdir()) {
                        logger.debug("created app data dir: " + appDataDirFile.getAbsolutePath());
                    } else {
                        logger.error("error creating app data dir: " + appDataDirFile.getAbsolutePath());
                    }
                }
            } else {
                appDataDirFile = new File(".");
            }

            appDataDir = appDataDirFile.getAbsolutePath();
            logger.debug("set app data dir: " + appDataDir);

            baseDir = new File(ClassPathResource.getResourceURI("mirth.properties")).getParentFile().getParent();
            logger.debug("set base dir: " + baseDir);

            if (mirthConfig.getString(CHARSET) != null) {
                System.setProperty(CHARSET, mirthConfig.getString(CHARSET));
            }

            String[] httpsClientProtocolsArray = mirthConfig.getStringArray(HTTPS_CLIENT_PROTOCOLS);
            if (ArrayUtils.isNotEmpty(httpsClientProtocolsArray)) {
                // Support both comma separated and multiline values
                List<String> httpsClientProtocolsList = new ArrayList<String>();
                for (String protocol : httpsClientProtocolsArray) {
                    httpsClientProtocolsList.addAll(Arrays.asList(StringUtils.split(protocol, ',')));
                }
                httpsClientProtocols = httpsClientProtocolsList
                        .toArray(new String[httpsClientProtocolsList.size()]);
            } else {
                httpsClientProtocols = MirthSSLUtil.DEFAULT_HTTPS_CLIENT_PROTOCOLS;
            }

            String[] httpsServerProtocolsArray = mirthConfig.getStringArray(HTTPS_SERVER_PROTOCOLS);
            if (ArrayUtils.isNotEmpty(httpsServerProtocolsArray)) {
                // Support both comma separated and multiline values
                List<String> httpsServerProtocolsList = new ArrayList<String>();
                for (String protocol : httpsServerProtocolsArray) {
                    httpsServerProtocolsList.addAll(Arrays.asList(StringUtils.split(protocol, ',')));
                }
                httpsServerProtocols = httpsServerProtocolsList
                        .toArray(new String[httpsServerProtocolsList.size()]);
            } else {
                httpsServerProtocols = MirthSSLUtil.DEFAULT_HTTPS_SERVER_PROTOCOLS;
            }

            String[] httpsCipherSuitesArray = mirthConfig.getStringArray(HTTPS_CIPHER_SUITES);
            if (ArrayUtils.isNotEmpty(httpsCipherSuitesArray)) {
                // Support both comma separated and multiline values
                List<String> httpsCipherSuitesList = new ArrayList<String>();
                for (String cipherSuite : httpsCipherSuitesArray) {
                    httpsCipherSuitesList.addAll(Arrays.asList(StringUtils.split(cipherSuite, ',')));
                }
                httpsCipherSuites = httpsCipherSuitesList.toArray(new String[httpsCipherSuitesList.size()]);
            } else {
                httpsCipherSuites = MirthSSLUtil.DEFAULT_HTTPS_CIPHER_SUITES;
            }

            String deploy = String.valueOf(mirthConfig.getProperty(STARTUP_DEPLOY));
            if (StringUtils.isNotBlank(deploy)) {
                startupDeploy = Boolean.parseBoolean(deploy);
            }

            // Check for server GUID and generate a new one if it doesn't exist
            PropertiesConfiguration serverIdConfig = new PropertiesConfiguration(
                    new File(getApplicationDataDir(), "server.id"));

            if ((serverIdConfig.getString("server.id") != null)
                    && (serverIdConfig.getString("server.id").length() > 0)) {
                serverId = serverIdConfig.getString("server.id");
            } else {
                serverId = generateGuid();
                logger.debug("generated new server id: " + serverId);
                serverIdConfig.setProperty("server.id", serverId);
                serverIdConfig.save();
            }

            passwordRequirements = PasswordRequirementsChecker.getInstance().loadPasswordRequirements(mirthConfig);

            apiBypassword = mirthConfig.getString(API_BYPASSWORD);
            if (StringUtils.isNotBlank(apiBypassword)) {
                apiBypassword = new String(Base64.decodeBase64(apiBypassword), "US-ASCII");
            }

            statsUpdateInterval = NumberUtils.toInt(mirthConfig.getString(STATS_UPDATE_INTERVAL),
                    DonkeyStatisticsUpdater.DEFAULT_UPDATE_INTERVAL);

            // Check for configuration map properties
            if (mirthConfig.getString(CONFIGURATION_MAP_PATH) != null) {
                configurationFile = mirthConfig.getString(CONFIGURATION_MAP_PATH);
            } else {
                configurationFile = getApplicationDataDir() + File.separator + "configuration.properties";
            }

            PropertiesConfiguration configurationMapProperties = new PropertiesConfiguration();
            configurationMapProperties.setDelimiterParsingDisabled(true);
            configurationMapProperties.setListDelimiter((char) 0);
            try {
                configurationMapProperties.load(new File(configurationFile));
            } catch (ConfigurationException e) {
                logger.warn("Failed to find configuration map file");
            }

            Map<String, ConfigurationProperty> configurationMap = new HashMap<String, ConfigurationProperty>();
            Iterator<String> iterator = configurationMapProperties.getKeys();

            while (iterator.hasNext()) {
                String key = iterator.next();
                String value = configurationMapProperties.getString(key);
                String comment = configurationMapProperties.getLayout().getCanonicalComment(key, false);

                configurationMap.put(key, new ConfigurationProperty(value, comment));
            }

            setConfigurationProperties(configurationMap, false);
        } catch (Exception e) {
            logger.error("Failed to initialize configuration controller", e);
        }
    }

    /*
     * Return the server GUID
     */
    @Override
    public String getServerId() {
        return serverId;
    }

    @Override
    public String getServerName() {
        return serverName;
    }

    /*
     * Return the server timezone in the following format: PDT (UTC -7)
     */
    @Override
    public String getServerTimezone(Locale locale) {
        TimeZone timeZone = TimeZone.getDefault();
        boolean daylight = timeZone.inDaylightTime(new Date());

        // Get the short timezone display name with respect to DST
        String timeZoneDisplay = timeZone.getDisplayName(daylight, TimeZone.SHORT, locale);

        // Get the offset in hours (divide by number of milliseconds in an hour)
        int offset = timeZone.getOffset(System.currentTimeMillis()) / (3600000);

        // Get the offset display in either UTC -x or UTC +x
        String offsetDisplay = (offset < 0) ? String.valueOf(offset) : "+" + offset;
        timeZoneDisplay += " (UTC " + offsetDisplay + ")";

        return timeZoneDisplay;
    }

    /*
     * Return the server time
     */
    @Override
    public Calendar getServerTime() {
        return Calendar.getInstance();
    }

    // ast: Get the list of all available encodings for this JVM
    @Override
    public List<String> getAvailableCharsetEncodings() throws ControllerException {
        logger.debug("Retrieving avaiable character encodings");

        try {
            SortedMap<String, Charset> avaiablesCharsets = Charset.availableCharsets();
            List<String> simpleAvaiableCharsets = new ArrayList<String>();

            for (Charset charset : avaiablesCharsets.values()) {
                String charsetName = charset.name();

                try {
                    if (StringUtils.isEmpty(charsetName)) {
                        charsetName = charset.aliases().iterator().next();
                    }
                } catch (Exception e) {
                    charsetName = "UNKNOWN";
                }

                simpleAvaiableCharsets.add(charsetName);
            }

            return simpleAvaiableCharsets;
        } catch (Exception e) {
            throw new ControllerException("Error retrieving available charset encodings.", e);
        }
    }

    @Override
    public ServerSettings getServerSettings() throws ControllerException {
        serverName = getProperty(PROPERTIES_CORE + "." + serverId, "server.name");
        Properties serverSettings = getPropertiesForGroup(PROPERTIES_CORE);
        return new ServerSettings(serverName, serverSettings);
    }

    @Override
    public EncryptionSettings getEncryptionSettings() throws ControllerException {
        return encryptionConfig;
    }

    @Override
    public DatabaseSettings getDatabaseSettings() throws ControllerException {
        return databaseConfig;
    }

    @Override
    public void setServerSettings(ServerSettings settings) throws ControllerException {
        String serverName = settings.getServerName();
        if (serverName != null) {
            saveProperty(PROPERTIES_CORE + "." + serverId, "server.name", serverName);
            this.serverName = serverName;
        }

        Properties properties = settings.getProperties();
        for (Object name : properties.keySet()) {
            saveProperty(PROPERTIES_CORE, (String) name, (String) properties.get(name));
        }
    }

    @Override
    public UpdateSettings getUpdateSettings() throws ControllerException {
        return new UpdateSettings(getPropertiesForGroup(PROPERTIES_CORE));
    }

    @Override
    public void setUpdateSettings(UpdateSettings settings) throws ControllerException {
        Properties properties = settings.getProperties();
        for (Object name : properties.keySet()) {
            saveProperty(PROPERTIES_CORE, (String) name, (String) properties.get(name));
        }
    }

    @Override
    public String generateGuid() {
        return UUID.randomUUID().toString();
    }

    @Override
    public String getDatabaseType() {
        return mirthConfig.getString("database");
    }

    @Override
    public Encryptor getEncryptor() {
        return encryptor;
    }

    @Override
    public Digester getDigester() {
        return digester;
    }

    @Override
    public List<DriverInfo> getDatabaseDrivers() throws ControllerException {
        logger.debug("retrieving database driver list");
        File driversFile = new File(ClassPathResource.getResourceURI("dbdrivers.xml"));

        if (driversFile.exists()) {
            try {
                ArrayList<DriverInfo> drivers = new ArrayList<DriverInfo>();
                Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(driversFile);
                Element driversElement = document.getDocumentElement();

                for (int i = 0; i < driversElement.getElementsByTagName("driver").getLength(); i++) {
                    Element driverElement = (Element) driversElement.getElementsByTagName("driver").item(i);
                    DriverInfo driver = new DriverInfo(driverElement.getAttribute("name"),
                            driverElement.getAttribute("class"), driverElement.getAttribute("template"),
                            driverElement.getAttribute("selectLimit"));
                    logger.debug("found database driver: " + driver);
                    drivers.add(driver);
                }

                return drivers;
            } catch (Exception e) {
                throw new ControllerException(
                        "Error during loading of database drivers file: " + driversFile.getAbsolutePath(), e);
            }
        } else {
            throw new ControllerException(
                    "Could not locate database drivers file: " + driversFile.getAbsolutePath());
        }
    }

    @Override
    public String getServerVersion() {
        return versionConfig.getString("mirth.version");
    }

    @Override
    public String getBuildDate() {
        return versionConfig.getString("mirth.date");
    }

    @Override
    public String[] getHttpsClientProtocols() {
        return ArrayUtils.clone(httpsClientProtocols);
    }

    @Override
    public String[] getHttpsServerProtocols() {
        return ArrayUtils.clone(httpsServerProtocols);
    }

    @Override
    public String[] getHttpsCipherSuites() {
        return ArrayUtils.clone(httpsCipherSuites);
    }

    @Override
    public boolean isStartupDeploy() {
        return startupDeploy;
    }

    @Override
    public int getStatsUpdateInterval() {
        return statsUpdateInterval;
    }

    @Override
    public int getStatus() {
        logger.debug("getting Mirth status");

        // If the database isn't running or the engine isn't running (only if it isn't starting) return STATUS_UNAVAILABLE.
        if (!isDatabaseRunning() || (!ControllerFactory.getFactory().createEngineController().isRunning()
                && status != STATUS_ENGINE_STARTING)) {
            return STATUS_UNAVAILABLE;
        }

        return status;
    }

    @Override
    public ServerConfiguration getServerConfiguration() throws ControllerException {
        ChannelController channelController = ControllerFactory.getFactory().createChannelController();
        AlertController alertController = ControllerFactory.getFactory().createAlertController();
        CodeTemplateController codeTemplateController = ControllerFactory.getFactory()
                .createCodeTemplateController();

        ServerConfiguration serverConfiguration = new ServerConfiguration();
        serverConfiguration.setChannelGroups(channelController.getChannelGroups(null));
        serverConfiguration.setChannels(channelController.getChannels(null));
        serverConfiguration.setAlerts(alertController.getAlerts());
        serverConfiguration.setCodeTemplateLibraries(codeTemplateController.getLibraries(null, true));
        serverConfiguration.setServerSettings(getServerSettings());
        serverConfiguration.setUpdateSettings(getUpdateSettings());
        serverConfiguration.setGlobalScripts(scriptController.getGlobalScripts());

        // Put the properties for every plugin with properties in a map.
        Map<String, Properties> pluginProperties = new HashMap<String, Properties>();
        ExtensionController extensionController = ControllerFactory.getFactory().createExtensionController();

        for (PluginMetaData pluginMetaData : extensionController.getPluginMetaData().values()) {
            String pluginName = pluginMetaData.getName();
            Properties properties = extensionController.getPluginProperties(pluginName);

            if (MapUtils.isNotEmpty(properties)) {
                pluginProperties.put(pluginName, properties);
            }
        }

        serverConfiguration.setPluginProperties(pluginProperties);
        serverConfiguration.setResourceProperties(
                ObjectXMLSerializer.getInstance().deserialize(getResources(), ResourcePropertiesList.class));
        serverConfiguration.setChannelDependencies(getChannelDependencies());

        return serverConfiguration;
    }

    @Override
    public void setServerConfiguration(ServerConfiguration serverConfiguration, boolean deploy)
            throws StartException, StopException, ControllerException, InterruptedException {
        ChannelController channelController = ControllerFactory.getFactory().createChannelController();
        AlertController alertController = ControllerFactory.getFactory().createAlertController();
        CodeTemplateController codeTemplateController = ControllerFactory.getFactory()
                .createCodeTemplateController();
        EngineController engineController = ControllerFactory.getFactory().createEngineController();

        /*
         * Make sure users aren't deploying or undeploying channels while the server configuration
         * is being restored.
         */
        synchronized (engineController) {
            Set<ChannelGroup> channelGroups = new HashSet<ChannelGroup>();
            if (serverConfiguration.getChannelGroups() != null) {
                channelGroups.addAll(serverConfiguration.getChannelGroups());
            }
            channelController.updateChannelGroups(channelGroups, new HashSet<String>(), true);

            if (serverConfiguration.getChannels() != null) {
                // Undeploy all channels before updating or removing them
                engineController.undeployChannels(engineController.getDeployedIds(),
                        ServerEventContext.SYSTEM_USER_EVENT_CONTEXT, null);

                // Remove channels that don't exist in the new configuration
                for (Channel channel : channelController.getChannels(null)) {
                    boolean found = false;

                    for (Channel newChannel : serverConfiguration.getChannels()) {
                        if (newChannel.getId().equals(channel.getId())) {
                            found = true;
                        }
                    }

                    if (!found) {
                        channelController.removeChannel(channel, ServerEventContext.SYSTEM_USER_EVENT_CONTEXT);
                    }
                }

                // Update all channels from the server configuration
                for (Channel channel : serverConfiguration.getChannels()) {
                    channelController.updateChannel(channel, ServerEventContext.SYSTEM_USER_EVENT_CONTEXT, true);
                }
            }

            if (serverConfiguration.getAlerts() != null) {
                // Remove all existing alerts
                for (AlertModel alert : alertController.getAlerts()) {
                    alertController.removeAlert(alert.getId());
                }

                for (AlertModel alert : serverConfiguration.getAlerts()) {
                    alertController.updateAlert(alert);
                }
            }

            if (serverConfiguration.getCodeTemplateLibraries() != null) {
                Set<CodeTemplateLibrary> clonedLibraries = new HashSet<CodeTemplateLibrary>();
                for (CodeTemplateLibrary library : serverConfiguration.getCodeTemplateLibraries()) {
                    clonedLibraries.add(new CodeTemplateLibrary(library));
                }

                // Update all libraries from the server configuration
                codeTemplateController.updateLibraries(clonedLibraries,
                        ServerEventContext.SYSTEM_USER_EVENT_CONTEXT, true);

                // Remove code templates that don't exist in the new configuration
                for (CodeTemplate codeTemplate : codeTemplateController.getCodeTemplates(null)) {
                    boolean found = false;

                    for (CodeTemplateLibrary newLibrary : serverConfiguration.getCodeTemplateLibraries()) {
                        if (newLibrary.getCodeTemplates() != null) {
                            for (CodeTemplate newCodeTemplate : newLibrary.getCodeTemplates()) {
                                if (newCodeTemplate.getId().equals(codeTemplate.getId())) {
                                    found = true;
                                    break;
                                }
                            }
                        }

                        if (found) {
                            break;
                        }
                    }

                    if (!found) {
                        codeTemplateController.removeCodeTemplate(codeTemplate.getId(),
                                ServerEventContext.SYSTEM_USER_EVENT_CONTEXT);
                    }
                }

                // Update all code templates from the server configuration
                for (CodeTemplateLibrary library : serverConfiguration.getCodeTemplateLibraries()) {
                    if (library.getCodeTemplates() != null) {
                        for (CodeTemplate codeTemplate : library.getCodeTemplates()) {
                            codeTemplateController.updateCodeTemplate(codeTemplate,
                                    ServerEventContext.SYSTEM_USER_EVENT_CONTEXT, true);
                        }
                    }
                }
            }

            if (serverConfiguration.getServerSettings() != null) {
                // The server name must not be restored.
                ServerSettings serverSettings = serverConfiguration.getServerSettings();
                serverSettings.setServerName(null);

                setServerSettings(serverSettings);
            }

            if (serverConfiguration.getUpdateSettings() != null) {
                setUpdateSettings(serverConfiguration.getUpdateSettings());
            }

            // Set the properties for all plugins in the server configuration,
            // whether or not the plugin is actually installed on this server.
            if (serverConfiguration.getPluginProperties() != null) {
                ExtensionController extensionController = ControllerFactory.getFactory()
                        .createExtensionController();

                for (Entry<String, Properties> pluginEntry : serverConfiguration.getPluginProperties().entrySet()) {
                    extensionController.setPluginProperties(pluginEntry.getKey(), pluginEntry.getValue());
                }
            }

            if (serverConfiguration.getResourceProperties() != null) {
                setResources(
                        ObjectXMLSerializer.getInstance().serialize(serverConfiguration.getResourceProperties()));

                try {
                    List<LibraryProperties> libraryResources = new ArrayList<LibraryProperties>();
                    for (ResourceProperties resource : serverConfiguration.getResourceProperties().getList()) {
                        if (resource instanceof LibraryProperties) {
                            libraryResources.add((LibraryProperties) resource);
                        }
                    }

                    ControllerFactory.getFactory().createContextFactoryController()
                            .updateResources(libraryResources, false);
                } catch (Exception e) {
                    logger.error("Unable to update libraries: " + e.getMessage(), e);
                }
            }

            if (serverConfiguration.getChannelDependencies() != null) {
                setChannelDependencies(serverConfiguration.getChannelDependencies());
            } else {
                setChannelDependencies(new HashSet<ChannelDependency>());
            }

            if (serverConfiguration.getGlobalScripts() != null) {
                scriptController.setGlobalScripts(serverConfiguration.getGlobalScripts());
            }

            // Deploy all channels
            if (deploy) {
                engineController.deployChannels(channelController.getChannelIds(),
                        ServerEventContext.SYSTEM_USER_EVENT_CONTEXT, null);
            }
        }
    }

    @Override
    public Map<String, String> getConfigurationMap() {
        return configurationMap;
    }

    @Override
    public synchronized Map<String, ConfigurationProperty> getConfigurationProperties() {
        Map<String, ConfigurationProperty> map = new HashMap<String, ConfigurationProperty>();

        for (Entry<String, String> entry : configurationMap.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            String comment = commentMap.get(key);

            map.put(key, new ConfigurationProperty(value, comment));
        }

        return map;
    }

    @Override
    public synchronized void setConfigurationProperties(Map<String, ConfigurationProperty> map, boolean persist)
            throws ControllerException {
        if (persist) {
            saveConfigurationProperties(map);
        }

        Map<String, String> valueMap = new HashMap<String, String>();
        Map<String, String> commentMap = new HashMap<String, String>();

        for (Entry<String, ConfigurationProperty> entry : map.entrySet()) {
            valueMap.put(entry.getKey(), entry.getValue().getValue());
            commentMap.put(entry.getKey(), entry.getValue().getComment());
        }

        configurationMap = Collections.unmodifiableMap(valueMap);
        this.commentMap = Collections.unmodifiableMap(commentMap);
    }

    @Override
    public void setStatus(int status) {
        this.status = status;
    }

    @Override
    public String getBaseDir() {
        return baseDir;
    }

    @Override
    public String getApplicationDataDir() {
        return appDataDir;
    }

    @Override
    public String getConfigurationDir() {
        return baseDir + File.separator + "conf";
    }

    @Override
    public PasswordRequirements getPasswordRequirements() {
        return passwordRequirements;
    }

    @Override
    public boolean isBypasswordEnabled() {
        return StringUtils.isNotBlank(apiBypassword);
    }

    @Override
    public boolean checkBypassword(String password) {
        return isBypasswordEnabled() && StringUtils.equals(password, apiBypassword);
    }

    @Override
    public Properties getPropertiesForGroup(String category) {
        logger.debug("retrieving properties: category=" + category);
        Properties properties = new Properties();

        try {
            List<KeyValuePair> result = SqlConfig.getSqlSessionManager()
                    .selectList("Configuration.selectPropertiesForCategory", category);

            for (KeyValuePair pair : result) {
                properties.setProperty(pair.getKey(), StringUtils.defaultString(pair.getValue()));
            }
        } catch (Exception e) {
            logger.error("Could not retrieve properties: category=" + category, e);
        }

        return properties;
    }

    public void removePropertiesForGroup(String category) {
        logger.debug("deleting all properties: category=" + category);

        try {
            Map<String, Object> parameterMap = new HashMap<String, Object>();
            parameterMap.put("category", category);
            SqlConfig.getSqlSessionManager().delete("Configuration.deleteProperty", parameterMap);
        } catch (Exception e) {
            logger.error("Could not delete properties: category=" + category);
        }
    }

    @Override
    public String getProperty(String category, String name) {
        logger.debug("retrieving property: category=" + category + ", name=" + name);

        try {
            Map<String, Object> parameterMap = new HashMap<String, Object>();
            parameterMap.put("category", category);
            parameterMap.put("name", name);
            return (String) SqlConfig.getSqlSessionManager().selectOne("Configuration.selectProperty",
                    parameterMap);
        } catch (Exception e) {
            logger.warn("Could not retrieve property: category=" + category + ", name=" + name, e);
        }

        return null;
    }

    @Override
    public void saveProperty(String category, String name, String value) {
        logger.debug("storing property: category=" + category + ", name=" + name);

        try {
            Map<String, Object> parameterMap = new HashMap<String, Object>();
            parameterMap.put("category", category);
            parameterMap.put("name", name);
            parameterMap.put("value", value);

            if (getProperty(category, name) == null) {
                SqlConfig.getSqlSessionManager().insert("Configuration.insertProperty", parameterMap);
            } else {
                SqlConfig.getSqlSessionManager().insert("Configuration.updateProperty", parameterMap);
            }

            if (DatabaseUtil.statementExists("Configuration.vacuumConfigurationTable")) {
                SqlConfig.getSqlSessionManager().update("Configuration.vacuumConfigurationTable");
            }
        } catch (Exception e) {
            logger.error("Could not store property: category=" + category + ", name=" + name, e);
        }
    }

    @Override
    public void removeProperty(String category, String name) {
        logger.debug("deleting property: category=" + category + ", name=" + name);

        try {
            Map<String, Object> parameterMap = new HashMap<String, Object>();
            parameterMap.put("category", category);
            parameterMap.put("name", name);
            SqlConfig.getSqlSessionManager().delete("Configuration.deleteProperty", parameterMap);
        } catch (Exception e) {
            logger.error("Could not delete property: category=" + category + ", name=" + name, e);
        }
    }

    @Override
    public String getResources() {
        String resources = getProperty(PROPERTIES_CORE, PROPERTIES_RESOURCES);

        if (StringUtils.isBlank(resources)) {
            ResourcePropertiesList list = new ResourcePropertiesList();

            DirectoryResourceProperties defaultResource = new DirectoryResourceProperties();
            defaultResource.setId(ResourceProperties.DEFAULT_RESOURCE_ID);
            defaultResource.setName(ResourceProperties.DEFAULT_RESOURCE_NAME);
            defaultResource.setDescription(
                    "Loads libraries from the custom-lib folder in the Mirth Connect home directory.");
            defaultResource.setIncludeWithGlobalScripts(true);
            defaultResource.setDirectory("custom-lib");

            list.getList().add(defaultResource);
            resources = ObjectXMLSerializer.getInstance().serialize(list);
            saveProperty(PROPERTIES_CORE, PROPERTIES_RESOURCES, resources);
        }

        return resources;
    }

    @Override
    public void setResources(String resources) {
        saveProperty(PROPERTIES_CORE, PROPERTIES_RESOURCES, resources);
    }

    @Override
    public Set<ChannelDependency> getChannelDependencies() {
        String dependenciesXml = getProperty(PROPERTIES_CORE, PROPERTIES_DEPENDENCIES);
        Set<ChannelDependency> dependencies;

        if (StringUtils.isNotBlank(dependenciesXml)) {
            dependencies = ObjectXMLSerializer.getInstance().deserialize(dependenciesXml, Set.class);
        } else {
            dependencies = new HashSet<ChannelDependency>();
            setChannelDependencies(dependencies);
        }

        return dependencies;
    }

    @Override
    public synchronized void setChannelDependencies(Set<ChannelDependency> dependencies) {
        try {
            new ChannelDependencyGraph(dependencies);
        } catch (ChannelDependencyException e) {
            logger.error("Error saving channel dependencies: " + e.getMessage(), e);
            return;
        }

        saveProperty(PROPERTIES_CORE, PROPERTIES_DEPENDENCIES,
                ObjectXMLSerializer.getInstance().serialize(dependencies));
    }

    @Override
    public void initializeSecuritySettings() {
        try {
            /*
             * Load the encryption settings so that they can be referenced client side.
             */
            encryptionConfig = new EncryptionSettings(ConfigurationConverter.getProperties(mirthConfig));

            File keyStoreFile = new File(mirthConfig.getString("keystore.path"));
            char[] keyStorePassword = mirthConfig.getString("keystore.storepass").toCharArray();
            char[] keyPassword = mirthConfig.getString("keystore.keypass").toCharArray();
            Provider provider = (Provider) Class.forName(encryptionConfig.getSecurityProvider()).newInstance();

            KeyStore keyStore = null;

            // if the current server version is pre-2.2, load the keystore as JKS
            if (MigrationUtil.compareVersions("2.2.0", getServerVersion()) == 1) {
                keyStore = KeyStore.getInstance("JKS");
            } else {
                keyStore = KeyStore.getInstance("JCEKS");
            }

            if (keyStoreFile.exists()) {
                keyStore.load(new FileInputStream(keyStoreFile), keyStorePassword);
                logger.debug("found and loaded keystore: " + keyStoreFile.getAbsolutePath());
            } else {
                keyStore.load(null, keyStorePassword);
                logger.debug("keystore file not found, created new one");
            }

            configureEncryption(provider, keyStore, keyPassword);
            generateDefaultCertificate(provider, keyStore, keyPassword);

            // write the kesytore back to the file
            FileOutputStream fos = new FileOutputStream(keyStoreFile);
            keyStore.store(fos, keyStorePassword);
            IOUtils.closeQuietly(fos);
        } catch (Exception e) {
            logger.error("Could not initialize security settings.", e);
        }
    }

    @Override
    public void initializeDatabaseSettings() {
        try {
            databaseConfig = new DatabaseSettings(ConfigurationConverter.getProperties(mirthConfig));

            // dir.base is not included in mirth.properties, so set it manually
            databaseConfig.setDirBase(getBaseDir());

            String password = databaseConfig.getDatabasePassword();

            if (StringUtils.isNotEmpty(password)) {
                ConfigurationController configurationController = ControllerFactory.getFactory()
                        .createConfigurationController();
                EncryptionSettings encryptionSettings = configurationController.getEncryptionSettings();
                Encryptor encryptor = configurationController.getEncryptor();

                if (encryptionSettings.getEncryptProperties()) {
                    if (StringUtils.startsWith(password, EncryptionSettings.ENCRYPTION_PREFIX)) {
                        String encryptedPassword = StringUtils.removeStart(password,
                                EncryptionSettings.ENCRYPTION_PREFIX);
                        String decryptedPassword = encryptor.decrypt(encryptedPassword);
                        databaseConfig.setDatabasePassword(decryptedPassword);
                    } else if (StringUtils.isNotBlank(password)) {
                        // encrypt the password and write it back to the file
                        String encryptedPassword = EncryptionSettings.ENCRYPTION_PREFIX
                                + encryptor.encrypt(password);
                        mirthConfig.setProperty("database.password", encryptedPassword);

                        /*
                         * Save using a FileOutputStream so that the file will be saved to the
                         * proper location, even if running from the IDE.
                         */
                        File confDir = new File(ControllerFactory.getFactory().createConfigurationController()
                                .getConfigurationDir());
                        OutputStream os = new FileOutputStream(new File(confDir, "mirth.properties"));

                        try {
                            mirthConfig.save(os);
                        } finally {
                            IOUtils.closeQuietly(os);
                        }
                    }
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /*
     * If we have the encryption key property in the database, that means the previous keystore was
     * of type JKS, so we want to delete it so that a new JCEKS one can be created.
     * 
     * If we migrated from a version prior to 2.2, then the key from the ENCRYTPION_KEY table has
     * been added to the CONFIGURATION table. We want to deserialize it and put it in the new
     * keystore. We also need to delete the property.
     * 
     * NOTE that this method should only execute once.
     */

    public void migrateKeystore() {
        PropertiesConfiguration properties = new PropertiesConfiguration();
        properties.setDelimiterParsingDisabled(true);

        try {
            if (getProperty(PROPERTIES_CORE, "encryption.key") != null) {
                // load the keystore path and passwords
                properties.load(ResourceUtil.getResourceStream(this.getClass(), "mirth.properties"));
                File keyStoreFile = new File(properties.getString("keystore.path"));
                char[] keyStorePassword = properties.getString("keystore.storepass").toCharArray();
                char[] keyPassword = properties.getString("keystore.keypass").toCharArray();

                // delete the old JKS keystore
                keyStoreFile.delete();

                // create and load a new one as type JCEKS
                KeyStore keyStore = KeyStore.getInstance("JCEKS");
                keyStore.load(null, keyStorePassword);

                // deserialize the XML secret key to an Object
                ObjectXMLSerializer serializer = ObjectXMLSerializer.getInstance();
                String xml = getProperty(PROPERTIES_CORE, "encryption.key");

                /*
                 * This is a fix to account for an error that occurred when testing migration from
                 * version 1.8.2 to 3.0.0. The key was serialized as an instance of
                 * com.sun.crypto.provider.DESedeKey, but fails to correctly deserialize as an
                 * instance of java.security.KeyRep. The fix below extracts the "<default>" node
                 * from the serialized xml and uses that to deserialize to java.security.KeyRep.
                 * (MIRTH-2552)
                 */
                Document document = new DocumentSerializer().fromXML(xml);
                DonkeyElement root = new DonkeyElement(document.getDocumentElement());
                DonkeyElement keyRep = root.getChildElement("java.security.KeyRep");

                if (keyRep != null) {
                    DonkeyElement defaultElement = keyRep.getChildElement("default");

                    if (defaultElement != null) {
                        defaultElement.setNodeName("java.security.KeyRep");
                        xml = defaultElement.toXml();
                    }
                }

                SecretKey secretKey = serializer.deserialize(xml, SecretKey.class);

                // add the secret key entry to the new keystore
                KeyStore.SecretKeyEntry entry = new KeyStore.SecretKeyEntry(secretKey);
                keyStore.setEntry(SECRET_KEY_ALIAS, entry, new KeyStore.PasswordProtection(keyPassword));

                // save the keystore to the filesystem
                OutputStream keyStoreOuputStream = new FileOutputStream(keyStoreFile);

                try {
                    keyStore.store(keyStoreOuputStream, keyStorePassword);
                } finally {
                    IOUtils.closeQuietly(keyStoreOuputStream);
                }

                // remove the property from CONFIGURATION
                removeProperty(PROPERTIES_CORE, "encryption.key");

                // reinitialize the security settings
                initializeSecuritySettings();
            }
        } catch (Exception e) {
            logger.error("Error migrating encryption key from database to keystore.", e);
        }
    }

    /**
     * Instantiates the encryptor and digester using the configuration properties. If the properties
     * are not found, reasonable defaults are used.
     * 
     * @param provider
     *            The provider to use (ex. BC)
     * @param keyStore
     *            The keystore from which to load the secret encryption key
     * @param keyPassword
     *            The secret key password
     * @throws Exception
     */
    private void configureEncryption(Provider provider, KeyStore keyStore, char[] keyPassword) throws Exception {
        SecretKey secretKey = null;

        if (!keyStore.containsAlias(SECRET_KEY_ALIAS)) {
            logger.debug("encryption key not found, generating new one");
            KeyGenerator keyGenerator = KeyGenerator.getInstance(encryptionConfig.getEncryptionAlgorithm(),
                    provider);
            keyGenerator.init(encryptionConfig.getEncryptionKeyLength());
            secretKey = keyGenerator.generateKey();
            KeyStore.SecretKeyEntry entry = new KeyStore.SecretKeyEntry(secretKey);
            keyStore.setEntry(SECRET_KEY_ALIAS, entry, new KeyStore.PasswordProtection(keyPassword));
        } else {
            logger.debug("found encryption key in keystore");
            secretKey = (SecretKey) keyStore.getKey(SECRET_KEY_ALIAS, keyPassword);
        }

        /*
         * Now that we have a secret key, store it in the encryption settings so that we can use it
         * to encryption things client side.
         */
        encryptionConfig.setSecretKey(secretKey.getEncoded());

        encryptor = new KeyEncryptor();
        encryptor.setProvider(provider);
        encryptor.setKey(secretKey);
        encryptor.setFormat(Output.BASE64);

        digester = new Digester();
        digester.setProvider(provider);
        digester.setAlgorithm(encryptionConfig.getDigestAlgorithm());
        digester.setFormat(Output.BASE64);
    }

    /**
     * Checks for an existing certificate to use for secure communication between the server and
     * client. If no certficate exists, this will generate a new one.
     * 
     */
    private void generateDefaultCertificate(Provider provider, KeyStore keyStore, char[] keyPassword)
            throws Exception {
        final String certificateAlias = "mirthconnect";

        if (!keyStore.containsAlias(certificateAlias)) {
            // Common CA and SSL cert attributes
            Date startDate = new Date(); // time from which certificate is valid
            Date expiryDate = DateUtils.addYears(startDate, 50); // time after which certificate is not valid
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", provider);
            keyPairGenerator.initialize(2048);

            KeyPair caKeyPair = keyPairGenerator.generateKeyPair();
            logger.debug("generated new key pair for CA cert using provider: " + provider.getName());

            // Generate CA cert
            X500Name caSubjectName = new X500Name("CN=Mirth Connect Certificate Authority");
            SubjectPublicKeyInfo caSubjectKey = new SubjectPublicKeyInfo(
                    ASN1Sequence.getInstance(caKeyPair.getPublic().getEncoded()));
            X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(caSubjectName, BigInteger.ONE,
                    startDate, expiryDate, caSubjectName, caSubjectKey);
            certBuilder.addExtension(org.bouncycastle.asn1.x509.Extension.basicConstraints, true,
                    new BasicConstraints(0));
            ContentSigner sigGen = new JcaContentSignerBuilder("SHA256withRSA").setProvider(provider)
                    .build(caKeyPair.getPrivate());
            Certificate caCert = new JcaX509CertificateConverter().setProvider(provider)
                    .getCertificate(certBuilder.build(sigGen));

            // Generate SSL cert
            KeyPair sslKeyPair = keyPairGenerator.generateKeyPair();
            logger.debug("generated new key pair for SSL cert using provider: " + provider.getName());

            X500Name sslSubjectName = new X500Name("CN=mirth-connect");
            SubjectPublicKeyInfo sslSubjectKey = new SubjectPublicKeyInfo(
                    ASN1Sequence.getInstance(sslKeyPair.getPublic().getEncoded()));
            X509v3CertificateBuilder sslCertBuilder = new X509v3CertificateBuilder(caSubjectName,
                    new BigInteger(50, new SecureRandom()), startDate, expiryDate, sslSubjectName, sslSubjectKey);
            sslCertBuilder.addExtension(org.bouncycastle.asn1.x509.Extension.authorityKeyIdentifier, false,
                    new AuthorityKeyIdentifier(caCert.getEncoded()));
            sslCertBuilder.addExtension(org.bouncycastle.asn1.x509.Extension.subjectKeyIdentifier, false,
                    new SubjectKeyIdentifier(sslKeyPair.getPublic().getEncoded()));

            sigGen = new JcaContentSignerBuilder("SHA256withRSA").setProvider(provider)
                    .build(caKeyPair.getPrivate());
            Certificate sslCert = new JcaX509CertificateConverter().setProvider(provider)
                    .getCertificate(sslCertBuilder.build(sigGen));

            logger.debug("generated new certificate with serial number: "
                    + ((X509Certificate) sslCert).getSerialNumber());

            // add the generated SSL cert to the keystore using the key password
            keyStore.setKeyEntry(certificateAlias, sslKeyPair.getPrivate(), keyPassword,
                    new Certificate[] { sslCert });
        } else {
            logger.debug("found certificate in keystore");
        }
    }

    private boolean isDatabaseRunning() {
        Statement statement = null;
        Connection connection = null;
        SqlConfig.getSqlSessionManager().startManagedSession();

        try {
            connection = SqlConfig.getSqlSessionManager().getConnection();
            statement = connection.createStatement();
            statement.execute("SELECT 1 FROM CHANNEL");
            return true;
        } catch (Exception e) {
            logger.warn("could not retrieve status of database", e);
            return false;
        } finally {
            DbUtils.closeQuietly(statement);
            DbUtils.closeQuietly(connection);
            if (SqlConfig.getSqlSessionManager().isManagedSessionStarted()) {
                SqlConfig.getSqlSessionManager().close();
            }
        }
    }

    private void saveConfigurationProperties(Map<String, ConfigurationProperty> map) throws ControllerException {
        try {
            PropertiesConfiguration configurationMapProperties = new PropertiesConfiguration();
            configurationMapProperties.setDelimiterParsingDisabled(true);
            configurationMapProperties.setListDelimiter((char) 0);
            configurationMapProperties.clear();

            PropertiesConfigurationLayout layout = configurationMapProperties.getLayout();

            Map<String, ConfigurationProperty> sortedMap = new TreeMap<String, ConfigurationProperty>(
                    String.CASE_INSENSITIVE_ORDER);
            sortedMap.putAll(map);

            for (Entry<String, ConfigurationProperty> entry : sortedMap.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue().getValue();
                String comment = entry.getValue().getComment();

                if (StringUtils.isNotBlank(key)) {
                    configurationMapProperties.addProperty(key, value);
                    layout.setComment(key, StringUtils.isBlank(comment) ? null : comment);
                }
            }

            configurationMapProperties.save(new File(configurationFile));
        } catch (Exception e) {
            throw new ControllerException(e);
        }
    }

    @Override
    public ConnectionTestResponse sendTestEmail(Properties properties) throws Exception {
        String portString = properties.getProperty("port");
        String encryption = properties.getProperty("encryption");
        String host = properties.getProperty("host");
        String timeoutString = properties.getProperty("timeout");
        Boolean authentication = Boolean.parseBoolean(properties.getProperty("authentication"));
        String username = properties.getProperty("username");
        String password = properties.getProperty("password");
        String to = properties.getProperty("toAddress");
        String from = properties.getProperty("fromAddress");

        int port = -1;
        try {
            port = Integer.parseInt(portString);
        } catch (NumberFormatException e) {
            return new ConnectionTestResponse(ConnectionTestResponse.Type.FAILURE,
                    "Invalid port: \"" + portString + "\"");
        }

        Email email = new SimpleEmail();
        email.setDebug(true);
        email.setHostName(host);
        email.setSmtpPort(port);

        try {
            int timeout = Integer.parseInt(timeoutString);
            email.setSocketTimeout(timeout);
            email.setSocketConnectionTimeout(timeout);
        } catch (NumberFormatException e) {
            // Don't set if the value is invalid
        }

        if ("SSL".equalsIgnoreCase(encryption)) {
            email.setSSLOnConnect(true);
            email.setSslSmtpPort(portString);
        } else if ("TLS".equalsIgnoreCase(encryption)) {
            email.setStartTLSEnabled(true);
        }

        if (authentication) {
            email.setAuthentication(username, password);
        }

        // These have to be set after the authenticator, so that a new mail session isn't created
        ConfigurationController configurationController = ControllerFactory.getFactory()
                .createConfigurationController();
        String protocols = properties.getProperty("protocols", StringUtils.join(
                MirthSSLUtil.getEnabledHttpsProtocols(configurationController.getHttpsClientProtocols()), ' '));
        String cipherSuites = properties.getProperty("cipherSuites", StringUtils.join(
                MirthSSLUtil.getEnabledHttpsCipherSuites(configurationController.getHttpsCipherSuites()), ' '));
        email.getMailSession().getProperties().setProperty("mail.smtp.ssl.protocols", protocols);
        email.getMailSession().getProperties().setProperty("mail.smtp.ssl.ciphersuites", cipherSuites);

        SSLSocketFactory socketFactory = (SSLSocketFactory) properties.get("socketFactory");
        if (socketFactory != null) {
            email.getMailSession().getProperties().put("mail.smtp.ssl.socketFactory", socketFactory);
            if ("SSL".equalsIgnoreCase(encryption)) {
                email.getMailSession().getProperties().put("mail.smtp.socketFactory", socketFactory);
            }
        }

        email.setSubject("Mirth Connect Test Email");

        try {
            for (String toAddress : StringUtils.split(to, ",")) {
                email.addTo(toAddress);
            }

            email.setFrom(from);
            email.setMsg(
                    "Receipt of this email confirms that mail originating from this Mirth Connect Server is capable of reaching its intended destination.\n\nSMTP Configuration:\n- Host: "
                            + host + "\n- Port: " + port);

            email.send();
            return new ConnectionTestResponse(ConnectionTestResponse.Type.SUCCESS,
                    "Sucessfully sent test email to: " + to);
        } catch (EmailException e) {
            return new ConnectionTestResponse(ConnectionTestResponse.Type.FAILURE, e.getMessage());
        }
    }
}