org.rhq.enterprise.server.installer.ServerInstallUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.rhq.enterprise.server.installer.ServerInstallUtil.java

Source

/*
 * RHQ Management Platform
 * Copyright (C) 2005-2012 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.enterprise.server.installer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.security.auth.login.AppConfigurationEntry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.helper.ProjectHelper2;

import org.jboss.as.controller.client.ModelControllerClient;
import org.jboss.dmr.ModelNode;
import org.jboss.sasl.util.UsernamePasswordHashUtil;

import org.rhq.common.jbossas.client.controller.Address;
import org.rhq.common.jbossas.client.controller.CoreJBossASClient;
import org.rhq.common.jbossas.client.controller.DatasourceJBossASClient;
import org.rhq.common.jbossas.client.controller.FailureException;
import org.rhq.common.jbossas.client.controller.InfinispanJBossASClient;
import org.rhq.common.jbossas.client.controller.JBossASClient;
import org.rhq.common.jbossas.client.controller.LoggingJBossASClient;
import org.rhq.common.jbossas.client.controller.MessagingJBossASClient;
import org.rhq.common.jbossas.client.controller.SecurityDomainJBossASClient;
import org.rhq.common.jbossas.client.controller.SocketBindingJBossASClient;
import org.rhq.common.jbossas.client.controller.TransactionsJBossASClient;
import org.rhq.common.jbossas.client.controller.VaultJBossASClient;
import org.rhq.common.jbossas.client.controller.WebJBossASClient;
import org.rhq.common.jbossas.client.controller.WebJBossASClient.ConnectorConfiguration;
import org.rhq.common.jbossas.client.controller.WebJBossASClient.SSLConfiguration;
import org.rhq.core.db.DatabaseType;
import org.rhq.core.db.DatabaseTypeFactory;
import org.rhq.core.db.DbUtil;
import org.rhq.core.db.OracleDatabaseType;
import org.rhq.core.db.PostgresqlDatabaseType;
import org.rhq.core.db.setup.DBSetup;
import org.rhq.core.domain.cloud.StorageNode;
import org.rhq.core.domain.cloud.StorageNode.OperationMode;
import org.rhq.core.util.PropertiesFileUpdate;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.core.util.file.FileUtil;
import org.rhq.core.util.obfuscation.ObfuscatedPreferences.RestrictedFormat;
import org.rhq.core.util.obfuscation.PropertyObfuscationVault;
import org.rhq.core.util.stream.StreamUtil;
import org.rhq.enterprise.communications.util.SecurityUtil;

/**
 * Provides utility methods necessary to complete the server installation.
 *
 * @author John Mazzitelli
 */
public class ServerInstallUtil {
    private static final Log LOG = LogFactory.getLog(ServerInstallUtil.class);

    public enum ExistingSchemaOption {
        OVERWRITE, KEEP, SKIP
    };

    public enum SupportedDatabaseType {
        POSTGRES, ORACLE
    };

    private static class SocketBindingInfo {
        public String name;
        public String sysprop;
        public int port;
        public boolean required = true;
        public String interfaceName = null; // not null if we know we want it to be changed from its default setting

        public SocketBindingInfo(String n, String s, int p) {
            this.name = n;
            this.sysprop = s;
            this.port = p;
        }

        public SocketBindingInfo(String n, String s, int p, String i) {
            this.name = n;
            this.sysprop = s;
            this.port = p;
            this.interfaceName = i;
        }

        public SocketBindingInfo(String name, String sysprop, int port, String interfaceName, boolean required) {
            this.name = name;
            this.sysprop = sysprop;
            this.port = port;
            this.interfaceName = interfaceName;
            this.required = required;
        }
    }

    private static final ArrayList<SocketBindingInfo> defaultSocketBindings;
    static {
        // all ports are -1000 from out-of-box AS7 defaults
        // except for the jboss.management ones - those are -3000 from their out-of-box defaults
        defaultSocketBindings = new ArrayList<SocketBindingInfo>();
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_AJP,
                "rhq.server.socket.binding.port.ajp", 7009));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_HTTP,
                "rhq.server.socket.binding.port.http", 7080));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_HTTPS,
                "rhq.server.socket.binding.port.https", 7443));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_JACORB,
                "rhq.server.socket.binding.port.jacorb", 2528));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_JACORB_SSL,
                "rhq.server.socket.binding.port.jacorb-ssl", 2529));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_MESSAGING,
                "rhq.server.socket.binding.port.messaging", 4449, "management"));
        defaultSocketBindings
                .add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_MESSAGING_THRUPUT,
                        "rhq.server.socket.binding.port.messaging-throughput", 4455, "management"));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_MGMT_HTTP,
                "jboss.management.http.port", 6990));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_MGMT_HTTPS,
                "jboss.management.https.port", 6443));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_MGMT_NATIVE,
                "jboss.management.native.port", 6999));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_REMOTING,
                "rhq.server.socket.binding.port.remoting", 3447, "management"));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_TXN_RECOVERY_ENV,
                "rhq.server.socket.binding.port.txn-recovery-environment", 3712));
        defaultSocketBindings.add(new SocketBindingInfo(SocketBindingJBossASClient.DEFAULT_BINDING_TXN_STATUS_MGR,
                "rhq.server.socket.binding.port.txn-status-manager", 3713));
    }

    private static final String RHQ_DATASOURCE_NAME_NOTX = "NoTxRHQDS";
    private static final String RHQ_DATASOURCE_NAME_XA = "RHQDS";
    private static final String RHQ_DS_SECURITY_DOMAIN_NOTX = "RHQDSSecurityDomainNoTx";
    private static final String RHQ_DS_SECURITY_DOMAIN_XA = "RHQDSSecurityDomainXa";
    private static final String RHQ_USER_SECURITY_DOMAIN = "RHQUserSecurityDomain";
    private static final String RHQ_REST_SECURITY_DOMAIN = "RHQRESTSecurityDomain";
    private static final String JDBC_LOGIN_MODULE_NAME = "org.rhq.enterprise.server.core.jaas.JDBCLoginModule";
    private static final String DELEGATIG_LOGIN_MODULE_NAME = "org.rhq.enterprise.server.core.jaas.DelegatingLoginModule";
    private static final String JDBC_DRIVER_POSTGRES = "postgres";
    private static final String JDBC_DRIVER_ORACLE = "oracle";
    private static final String JMS_ALERT_CONDITION_QUEUE = "AlertConditionQueue";
    private static final String JMS_DRIFT_CHANGESET_QUEUE = "DriftChangesetQueue";
    private static final String JMS_DRIFT_FILE_QUEUE = "DriftFileQueue";
    private static final String RHQ_CACHE_CONTAINER = "rhq";
    private static final String RHQ_CACHE = "rhqCache";
    private static final String RHQ_MGMT_USER = "rhqadmin";
    private static final String XA_DATASOURCE_CLASS_POSTGRES = "org.postgresql.xa.PGXADataSource";
    private static final String XA_DATASOURCE_CLASS_ORACLE = "oracle.jdbc.xa.client.OracleXADataSource";

    /**
     * Configure the logging subsystem.
     * @param mcc JBossAS management client
     * @param serverProperties the server properties, which includes the default log level to use
     * @throws Exception
     */
    public static void configureLogging(ModelControllerClient mcc, HashMap<String, String> serverProperties)
            throws Exception {
        LoggingJBossASClient client = new LoggingJBossASClient(mcc);

        // we want to create our own category
        String val = buildExpression(ServerProperties.PROP_LOG_LEVEL, serverProperties, true);
        client.setLoggerLevel("org.rhq", val);
        LOG.info("Logging category org.rhq set to [" + val + "]");

        client.setLoggerLevel("org.jboss.as.config", "INFO"); // BZ 1004730

        // BZ 1026786, 1078500
        StringBuilder sb = new StringBuilder("not(any(");
        sb.append("match(\"JBAS015960\")");
        sb.append(",");
        sb.append("match(\"JBAS018567\")");
        sb.append(",");
        sb.append("match(\"JBAS018568\")");
        sb.append(",");
        sb.append("match(\"JSF1051\")");
        sb.append("))");
        client.setFilterSpec(sb.toString());
    }

    /**
     * Configure the transaction manager.
     * @param mcc JBossAS management client
     * @throws Exception
     */
    public static void configureTransactionManager(ModelControllerClient mcc) throws Exception {
        TransactionsJBossASClient client = new TransactionsJBossASClient(mcc);

        // we want to bump up the transaction timeout
        client.setDefaultTransactionTimeout(600);
        LOG.info("Default transaction timeout set to 600 seconds.");
    }

    /**
     * Configure the deployment scanner.
     * @param mcc JBossAS management client
     * @throws Exception
     */
    public static void configureDeploymentScanner(ModelControllerClient mcc) throws Exception {
        CoreJBossASClient client = new CoreJBossASClient(mcc);

        // we do not want our RHQ Server to support hot deployments via the scanner
        client.setAppServerDefaultDeploymentScanEnabled(false);
        LOG.info("Deployment scanner turned off.");
    }

    /**
     * Prepares the mail service by configuring the SMTP settings.
     *
     * @param mcc JBossAS management client
     * @param serverProperties the server's properties
     * @throws Exception
     */
    public static void setupMailService(ModelControllerClient mcc, HashMap<String, String> serverProperties)
            throws Exception {

        String fromAddressExpr = buildExpression(ServerProperties.PROP_EMAIL_FROM_ADDRESS, serverProperties, true);
        String smtpHostExpr = buildExpression(ServerProperties.PROP_EMAIL_SMTP_HOST, serverProperties, true);
        String smtpPortExpr = buildExpression(ServerProperties.PROP_EMAIL_SMTP_PORT, serverProperties, true);

        // Tweek the mail configuration that comes out of box. Setup a batch request to write the proper attributes.

        // First, the from address (TODO: there is also a "ssl", "username" and "password" attribute we could set for authz)
        Address addr = Address.root().add(JBossASClient.SUBSYSTEM, "mail", "mail-session",
                "java:jboss/mail/Default");
        ModelNode writeFromAddr = JBossASClient.createRequest(JBossASClient.WRITE_ATTRIBUTE, addr);
        writeFromAddr.get(JBossASClient.NAME).set("from");
        writeFromAddr.get(JBossASClient.VALUE).setExpression(fromAddressExpr);

        // now the SMTP host
        addr = Address.root().add("socket-binding-group", "standard-sockets",
                "remote-destination-outbound-socket-binding", "mail-smtp");
        ModelNode writeHost = JBossASClient.createRequest(JBossASClient.WRITE_ATTRIBUTE, addr);
        writeHost.get(JBossASClient.NAME).set("host");
        JBossASClient.setPossibleExpression(writeHost, JBossASClient.VALUE, smtpHostExpr);

        // now the SMTP port
        addr = Address.root().add("socket-binding-group", "standard-sockets",
                "remote-destination-outbound-socket-binding", "mail-smtp");
        ModelNode writePort = JBossASClient.createRequest(JBossASClient.WRITE_ATTRIBUTE, addr);
        writePort.get(JBossASClient.NAME).set("port");
        JBossASClient.setPossibleExpression(writePort, JBossASClient.VALUE, smtpPortExpr);

        ModelNode batch = JBossASClient.createBatchRequest(writeFromAddr, writeHost, writePort);
        JBossASClient client = new JBossASClient(mcc);
        ModelNode response = client.execute(batch);
        if (!JBossASClient.isSuccess(response)) {
            throw new FailureException(response, "Failed to setup mail service");
        }
        LOG.info("Mail service has been configured.");
        return;
    }

    /**
     * Give the server properties, this returns the type of database that will be connected to.
     *
     * @param serverProperties
     * @return the type of DB
     */
    public static SupportedDatabaseType getSupportedDatabaseType(HashMap<String, String> serverProperties) {
        return getSupportedDatabaseType(serverProperties.get(ServerProperties.PROP_DATABASE_TYPE));
    }

    /**
     * Give the database type string, this returns the type of database that it refers to.
     *
     * @param dbType the database type string
     * @return the type of DB
     */
    public static SupportedDatabaseType getSupportedDatabaseType(String dbType) {
        if (dbType == null) {
            return null;
        }
        if (dbType.toLowerCase().indexOf("postgres") > -1) {
            return SupportedDatabaseType.POSTGRES;
        } else if (dbType.toLowerCase().indexOf("oracle") > -1) {
            return SupportedDatabaseType.ORACLE;
        }
        return null;
    }

    /**
     * Creates the security domain for the datasources. This is needed to support
     * obfuscation of the password in the configuration file.
     *
     * @param mcc the JBossAS management client
     * @param serverProperties contains the obfuscated password to store in the security domain
     * @throws Exception
     */
    public static void createDatasourceSecurityDomain(ModelControllerClient mcc,
            HashMap<String, String> serverProperties) throws Exception {

        final String dbUsername = buildExpression(ServerProperties.PROP_DATABASE_USERNAME, serverProperties, true);
        final String obfuscatedPassword = buildExpression(ServerProperties.PROP_DATABASE_PASSWORD, serverProperties,
                true);
        final SecurityDomainJBossASClient client = new SecurityDomainJBossASClient(mcc);
        String securityDomain = RHQ_DS_SECURITY_DOMAIN_XA;
        if (!client.isSecurityDomain(securityDomain)) {
            client.createNewSecureIdentitySecurityDomain72(securityDomain, dbUsername, obfuscatedPassword);
            LOG.info("Security domain [" + securityDomain + "] created");
        } else {
            LOG.info("Security domain [" + securityDomain + "] already exists, skipping the creation request");
            client.updateSecureIdentitySecurityDomainCredentials(securityDomain, dbUsername, obfuscatedPassword);
            LOG.info("Credentials have been updated for security domain [" + securityDomain + "]");
        }

        // we need separate security domains per datasource due to BZ 1102332
        securityDomain = RHQ_DS_SECURITY_DOMAIN_NOTX;
        if (!client.isSecurityDomain(securityDomain)) {
            client.createNewSecureIdentitySecurityDomain72(securityDomain, dbUsername, obfuscatedPassword);
            LOG.info("Security domain [" + securityDomain + "] created");
        } else {
            LOG.info("Security domain [" + securityDomain + "] already exists, skipping the creation request");
            client.updateSecureIdentitySecurityDomainCredentials(securityDomain, dbUsername, obfuscatedPassword);
            LOG.info("Credentials have been updated for security domain [" + securityDomain + "]");
        }
    }

    /**
     * Create the standard user security domain with the JDBCLogin module installed
     *
     * @param mcc ModelControllerClient to talk to the underlying AS
     * @throws Exception If anything goes wrong
     */
    public static void createUserSecurityDomain(ModelControllerClient mcc) throws Exception {

        Map<String, String> options = new HashMap<String, String>(2);
        options.put("hashAlgorithm", "MD5");
        options.put("hashEncoding", "base64");

        SecurityDomainJBossASClient.LoginModuleRequest loginModuleRequest = new SecurityDomainJBossASClient.LoginModuleRequest(
                JDBC_LOGIN_MODULE_NAME, AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT, options);

        SecurityDomainJBossASClient client = new SecurityDomainJBossASClient(mcc);
        client.createNewSecurityDomain(RHQ_USER_SECURITY_DOMAIN, loginModuleRequest);

    }

    /**
     * Create a security domain for container managed security used with the rhq-rest.war
     * @param mcc ModelControllerClient to talk to the underlying AS.
     * @throws Exception If anything goes wrong
     */
    public static void createRestSecurityDomain(ModelControllerClient mcc) throws Exception {

        Map<String, String> options = new HashMap<String, String>(2);
        options.put("delegateTo", RHQ_USER_SECURITY_DOMAIN);
        options.put("roles", "rest-user");

        SecurityDomainJBossASClient.LoginModuleRequest loginModuleRequest = new SecurityDomainJBossASClient.LoginModuleRequest(
                DELEGATIG_LOGIN_MODULE_NAME, AppConfigurationEntry.LoginModuleControlFlag.SUFFICIENT, options);

        SecurityDomainJBossASClient client = new SecurityDomainJBossASClient(mcc);
        client.createNewSecurityDomain(RHQ_REST_SECURITY_DOMAIN, loginModuleRequest);
    }

    /**
     * Creates the Vault required for RHQ property obfuscation.
     *
     * @param mcc the JBossAS management client
     * @param serverProperties server properties
     * @throws Exception
     */
    public static void createObfuscationVault(ModelControllerClient mcc, HashMap<String, String> serverProperties)
            throws Exception {

        final VaultJBossASClient client = new VaultJBossASClient(mcc);

        if (!client.isVault()) {
            ModelNode request = client.createNewVaultRequest(PropertyObfuscationVault.class.getName());
            ModelNode results = client.execute(request);
            if (!VaultJBossASClient.isSuccess(results)) {
                String vaultClass = client.getVaultClass();
                if (PropertyObfuscationVault.class.getName().equals(vaultClass)) {
                    LOG.info("RHQ Vault already configured, vault detected on the second read attempt");
                } else {
                    throw new FailureException(results, "Failed to create the RHQ vault");
                }
            } else {
                LOG.info("RHQ Vault created");
            }
        } else {
            String vaultClass = client.getVaultClass();
            if (PropertyObfuscationVault.class.getName().equals(vaultClass)) {
                LOG.info("RHQ vault already configured, skipping the creation process");
            } else {
                throw new FailureException(
                        "Failed to create the RHQ vault; a different vault is already configured");
            }
        }
    }

    /**
     * Creates the JMS Queues required for Drift and Alerting.
     *
     * @param mcc the JBossAS management client
     * @param serverProperties contains the obfuscated password to store in the security domain
     * @throws Exception
     */
    public static void createNewJMSQueues(ModelControllerClient mcc, HashMap<String, String> serverProperties)
            throws Exception {

        final MessagingJBossASClient client = new MessagingJBossASClient(mcc);
        final List<String> entryNames = new ArrayList<String>();

        // TODO (jshaughn): Prior to HornetQ we set recoveryRetries to 0: "don't redeliver messages on failure. It
        // just causes more failures. just go straight to the dead messages by setting recoveryRetries to 0.
        // This is equivalent to setting the dLQMaxResent property to 0 in the MessageDriven annotation in
        // the class definition."
        // HornetQ has different semantics, and may behave well with default settings. If not, we'll
        // likely need to add specific <address-setting> elements for our queues, which set
        // max-delivery-attempts to 0.  The documented default is 10.

        String queueName = JMS_ALERT_CONDITION_QUEUE;
        if (!client.isQueue(queueName)) {
            entryNames.clear();
            entryNames.add("queue/" + queueName);
            ModelNode request = client.createNewQueueRequest(queueName, true, entryNames);
            ModelNode results = client.execute(request);
            if (!MessagingJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create JMS Queue [" + queueName + "]");
            } else {
                LOG.info("JMS queue [" + queueName + "] created");
            }
        } else {
            LOG.info("JMS Queue [" + queueName + "] already exists, skipping the creation request");
        }

        queueName = JMS_DRIFT_CHANGESET_QUEUE;
        if (!client.isQueue(queueName)) {
            entryNames.clear();
            entryNames.add("queue/" + queueName);
            ModelNode request = client.createNewQueueRequest(queueName, true, entryNames);
            ModelNode results = client.execute(request);
            if (!MessagingJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create JMS Queue [" + queueName + "]");
            } else {
                LOG.info("JMS queue [" + queueName + "] created");
            }
        } else {
            LOG.info("JMS Queue [" + queueName + "] already exists, skipping the creation request");
        }

        queueName = JMS_DRIFT_FILE_QUEUE;
        if (!client.isQueue(queueName)) {
            entryNames.clear();
            entryNames.add("queue/" + queueName);
            ModelNode request = client.createNewQueueRequest(queueName, true, entryNames);
            ModelNode results = client.execute(request);
            if (!MessagingJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create JMS Queue [" + queueName + "]");
            } else {
                LOG.info("JMS queue [" + queueName + "] created");
            }
        } else {
            LOG.info("JMS Queue [" + queueName + "] already exists, skipping the creation request");
        }

        return;
    }

    /**
     * Creates the Infinispan caches for RHQ.
     *
     * @param mcc the JBossAS management client
     * @param serverProperties contains the obfuscated password to store in the security domain
     * @throws Exception
     */
    public static void createNewCaches(ModelControllerClient mcc, HashMap<String, String> serverProperties)
            throws Exception {

        final InfinispanJBossASClient client = new InfinispanJBossASClient(mcc);
        final String cacheContainerName = RHQ_CACHE_CONTAINER;
        final String localCacheName = RHQ_CACHE;
        if (!client.isCacheContainer(cacheContainerName)) {
            ModelNode request = client.createNewCacheContainerRequest(cacheContainerName, localCacheName);
            ModelNode results = client.execute(request);
            if (!MessagingJBossASClient.isSuccess(results)) {
                throw new FailureException(results,
                        "Failed to create Cache container [" + cacheContainerName + "]");
            } else {
                LOG.info("Cache container [" + cacheContainerName + "] created");
            }

        } else {
            LOG.info("Cache container [" + cacheContainerName + "] already exists, skipping the creation request");
        }

        if (!client.isLocalCache(cacheContainerName, localCacheName)) {
            ModelNode request = client.createNewLocalCacheRequest(cacheContainerName, localCacheName, null, null,
                    null, null, null);
            ModelNode results = client.execute(request);
            if (!MessagingJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create Local Cache [" + localCacheName + "]");
            } else {
                LOG.info("Local Cache [" + localCacheName + "] created");
            }

        } else {
            LOG.info("Local Cache [" + localCacheName + "] already exists, skipping the creation request");
        }
    }

    /**
     * Creates JDBC driver configurations so the datasources can properly connect to the backend databases.
     * This will attempt to create drivers for all supported databases, not just for the database type that
     * is currently configured.
     *
     * @param mcc
     * @param serverProperties
     * @throws Exception
     */
    public static void createNewJdbcDrivers(ModelControllerClient mcc, HashMap<String, String> serverProperties)
            throws Exception {

        final DatasourceJBossASClient client = new DatasourceJBossASClient(mcc);

        final ModelNode postgresDriverRequest = client.createNewJdbcDriverRequest(JDBC_DRIVER_POSTGRES,
                "org.rhq.postgres", "org.postgresql.xa.PGXADataSource");
        final ModelNode oracleDriverRequest = client.createNewJdbcDriverRequest(JDBC_DRIVER_ORACLE,
                "org.rhq.oracle", "oracle.jdbc.xa.client.OracleXADataSource");

        // if we are to use Oracle, we throw an exception if we can't create the Oracle datasource. We also try to
        // create the Postgres datasource but because it isn't needed, we don't throw exceptions if that fails, we
        // just log a warning.
        // The reverse is true if we are to use Postgres (that is, we ensure Postgres driver is created, but not Oracle).
        ModelNode results;
        final SupportedDatabaseType supportedDbType = getSupportedDatabaseType(serverProperties);
        switch (supportedDbType) {
        case POSTGRES: {
            if (client.isJDBCDriver(JDBC_DRIVER_POSTGRES)) {
                LOG.info("Postgres JDBC driver is already deployed");
            } else {
                results = client.execute(postgresDriverRequest);
                if (!DatasourceJBossASClient.isSuccess(results)) {
                    throw new FailureException(results, "Failed to create postgres database driver");
                } else {
                    LOG.info("Deployed Postgres JDBC driver");
                }
            }

            if (client.isJDBCDriver(JDBC_DRIVER_ORACLE)) {
                LOG.info("Oracle JDBC driver is already deployed");
            } else {
                results = client.execute(oracleDriverRequest);
                if (!DatasourceJBossASClient.isSuccess(results)) {
                    LOG.warn(
                            "Could not create Oracle JDBC Driver - you will not be able to switch to an Oracle DB later: "
                                    + JBossASClient.getFailureDescription(results));
                } else {
                    LOG.info("Deployed Oracle JDBC driver for future use");
                }
            }
            break;
        }
        case ORACLE: {
            if (client.isJDBCDriver(JDBC_DRIVER_ORACLE)) {
                LOG.info("Oracle JDBC driver is already deployed");
            } else {
                results = client.execute(oracleDriverRequest);
                if (!DatasourceJBossASClient.isSuccess(results)) {
                    throw new FailureException(results, "Failed to create oracle database driver");
                } else {
                    LOG.info("Deployed Oracle JDBC driver");
                }
            }
            if (client.isJDBCDriver(JDBC_DRIVER_POSTGRES)) {
                LOG.info("Postgres JDBC driver is already deployed");
            } else {
                results = client.execute(postgresDriverRequest);
                if (!DatasourceJBossASClient.isSuccess(results)) {
                    LOG.warn(
                            "Could not create Postgres JDBC Driver - you will not be able to switch to a Postgres DB later: "
                                    + JBossASClient.getFailureDescription(results));
                } else {
                    LOG.info("Deployed Postgres JDBC driver for future use");
                }
            }
            break;
        }
        default:
            throw new RuntimeException("bad db type"); // this should never happen; should have never gotten to this point with a bad type
        }
    }

    /**
     * Creates the datasources needed by the RHQ Server.
     *
     * @param mcc the JBossAS management client
     * @param serverProperties properties to help determine the properties of the datasources to be created
     * @throws Exception
     */
    public static void createNewDatasources(ModelControllerClient mcc, HashMap<String, String> serverProperties)
            throws Exception {

        final SupportedDatabaseType supportedDbType = getSupportedDatabaseType(serverProperties);
        switch (supportedDbType) {
        case POSTGRES: {
            createNewDatasources_Postgres(mcc);
            break;
        }
        case ORACLE: {
            createNewDatasources_Oracle(mcc);
            break;
        }
        default:
            throw new RuntimeException("bad db type"); // this should never happen; should have never gotten to this point with a bad type
        }
        LOG.info("Created datasources");

        final DatasourceJBossASClient client = new DatasourceJBossASClient(mcc);
        client.enableDatasource(RHQ_DATASOURCE_NAME_NOTX);
        client.enableXADatasource(RHQ_DATASOURCE_NAME_XA);
        LOG.info("Enabled datasources");

    }

    private static void createNewDatasources_Postgres(ModelControllerClient mcc) throws Exception {
        final HashMap<String, String> props = new HashMap<String, String>(4);
        final DatasourceJBossASClient client = new DatasourceJBossASClient(mcc);

        ModelNode noTxDsRequest = null;
        ModelNode xaDsRequest = null;

        if (!client.isDatasource(RHQ_DATASOURCE_NAME_NOTX)) {
            props.put("char.encoding", "UTF-8");

            noTxDsRequest = client.createNewDatasourceRequest(RHQ_DATASOURCE_NAME_NOTX, 30000,
                    "${rhq.server.database.connection-url:jdbc:postgresql://127.0.0.1:5432/rhq}",
                    JDBC_DRIVER_POSTGRES,
                    "org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLExceptionSorter", 15, false, 2, 5,
                    75, RHQ_DS_SECURITY_DOMAIN_NOTX, "-unused-stale-conn-checker-", "TRANSACTION_READ_COMMITTED",
                    "org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker", true,
                    props);
            noTxDsRequest.get("steps").get(0).remove("stale-connection-checker-class-name"); // we don't have one of these for postgres
        } else {
            LOG.info("Postgres datasource [" + RHQ_DATASOURCE_NAME_NOTX + "] already exists");
        }

        if (!client.isXADatasource(RHQ_DATASOURCE_NAME_XA)) {
            props.clear();
            props.put("ServerName", "${rhq.server.database.server-name:127.0.0.1}");
            props.put("PortNumber", "${rhq.server.database.port:5432}");
            props.put("DatabaseName", "${rhq.server.database.db-name:rhq}");

            xaDsRequest = client.createNewXADatasourceRequest(RHQ_DATASOURCE_NAME_XA, 30000, JDBC_DRIVER_POSTGRES,
                    XA_DATASOURCE_CLASS_POSTGRES,
                    "org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLExceptionSorter", 15, 5, 50,
                    (Boolean) null, (Boolean) null, 75, (String) null, RHQ_DS_SECURITY_DOMAIN_XA, (String) null,
                    "TRANSACTION_READ_COMMITTED",
                    "org.jboss.jca.adapters.jdbc.extensions.postgres.PostgreSQLValidConnectionChecker", props);

        } else {
            LOG.info("Postgres XA datasource [" + RHQ_DATASOURCE_NAME_XA + "] already exists");
        }

        if (noTxDsRequest != null || xaDsRequest != null) {
            ModelNode batch = DatasourceJBossASClient.createBatchRequest(noTxDsRequest, xaDsRequest);
            ModelNode results = client.execute(batch);
            if (!DatasourceJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create Postgres datasources");
            }
        }
    }

    private static void createNewDatasources_Oracle(ModelControllerClient mcc) throws Exception {
        final HashMap<String, String> props = new HashMap<String, String>(2);
        final DatasourceJBossASClient client = new DatasourceJBossASClient(mcc);

        ModelNode noTxDsRequest = null;
        ModelNode xaDsRequest = null;

        if (!client.isDatasource(RHQ_DATASOURCE_NAME_NOTX)) {
            props.put("char.encoding", "UTF-8");

            noTxDsRequest = client.createNewDatasourceRequest(RHQ_DATASOURCE_NAME_NOTX, 30000,
                    "${rhq.server.database.connection-url:jdbc:oracle:thin:@127.0.0.1:1521:rhq}",
                    JDBC_DRIVER_ORACLE, "org.jboss.jca.adapters.jdbc.extensions.oracle.OracleExceptionSorter", 15,
                    false, 2, 5, 75, RHQ_DS_SECURITY_DOMAIN_NOTX,
                    "org.jboss.jca.adapters.jdbc.extensions.oracle.OracleStaleConnectionChecker",
                    "TRANSACTION_READ_COMMITTED",
                    "org.jboss.jca.adapters.jdbc.extensions.oracle.OracleValidConnectionChecker", true, props);
        } else {
            LOG.info("Oracle datasource [" + RHQ_DATASOURCE_NAME_NOTX + "] already exists");
        }

        if (!client.isDatasource(RHQ_DATASOURCE_NAME_XA)) {
            props.clear();
            props.put("URL", "${rhq.server.database.connection-url:jdbc:oracle:thin:@127.0.0.1:1521:rhq}");

            xaDsRequest = client.createNewXADatasourceRequest(RHQ_DATASOURCE_NAME_XA, 30000, JDBC_DRIVER_ORACLE,
                    XA_DATASOURCE_CLASS_ORACLE,
                    "org.jboss.jca.adapters.jdbc.extensions.oracle.OracleExceptionSorter", 15, 5, 50,
                    (Boolean) null, Boolean.TRUE, 75, (String) null, RHQ_DS_SECURITY_DOMAIN_XA,
                    "org.jboss.jca.adapters.jdbc.extensions.oracle.OracleStaleConnectionChecker",
                    "TRANSACTION_READ_COMMITTED",
                    "org.jboss.jca.adapters.jdbc.extensions.oracle.OracleValidConnectionChecker", props);
        } else {
            LOG.info("Oracle XA datasource [" + RHQ_DATASOURCE_NAME_XA + "] already exists");
        }

        if (noTxDsRequest != null || xaDsRequest != null) {
            ModelNode batch = DatasourceJBossASClient.createBatchRequest(noTxDsRequest, xaDsRequest);
            ModelNode results = client.execute(batch);
            if (!DatasourceJBossASClient.isSuccess(results)) {
                throw new FailureException(results, "Failed to create Oracle datasources");
            }
        }
    }

    /**
     * Determines if we are in auto-install mode. This means the properties file is
     * fully configured and the installation can begin without asking the user
     * for more input.
     *
     * @param serverProperties the full set of server properties
     *
     * @return true if we are in auto-install mode; false if the user must give us more
     *         information before we can complete the installation.
     */
    public static boolean isAutoinstallEnabled(HashMap<String, String> serverProperties) {
        String enableProp = serverProperties.get(ServerProperties.PROP_AUTOINSTALL_ENABLE);
        if (enableProp != null) {
            return Boolean.parseBoolean(enableProp);
        }
        return false;
    }

    /**
     * Returns <code>true</code> if the database already has the database schema created for it. It will not be known
     * what version of schema or if its the latest, all this method tells you is that some RHQ database schema exists.
     *
     * @param connectionUrl
     * @param username
     * @param password
     * @return <code>true</code> if the database can be connected to
     *
     * @throws Exception if failed to communicate with the database
     */
    public static boolean isDatabaseSchemaExist(String connectionUrl, String username, String password)
            throws Exception {

        Connection conn = getDatabaseConnection(connectionUrl, username, password);
        DatabaseType db = DatabaseTypeFactory.getDatabaseType(conn);

        try {
            return db.checkTableExists(conn, "RHQ_PRINCIPAL");
        } catch (IllegalStateException e) {
            return false;
        } finally {
            db.closeConnection(conn);
        }
    }

    /**
     * Get the list of existing servers from an existing schema.
     *
     * @param connectionUrl
     * @param username
     * @param password
     * @return List of server names registered in the database. Empty list if the table does not exist or there are no entries in the table.
     *
     * @throws Exception if failed to communicate with the database
     */
    public static ArrayList<String> getServerNames(String connectionUrl, String username, String password)
            throws Exception {
        DatabaseType db = null;
        Connection conn = null;
        Statement stm = null;
        ResultSet rs = null;
        ArrayList<String> result = new ArrayList<String>();

        try {
            conn = getDatabaseConnection(connectionUrl, username, password);
            db = DatabaseTypeFactory.getDatabaseType(conn);

            if (db.checkTableExists(conn, "rhq_server")) {

                stm = conn.createStatement();
                rs = stm.executeQuery("SELECT name FROM rhq_server ORDER BY name asc");

                while (rs.next()) {
                    result.add(rs.getString(1));
                }
            }
        } catch (IllegalStateException e) {
            // table does not exist
        } catch (SQLException e) {
            LOG.info("Unable to fetch existing server info: " + e.getMessage());
        } finally {
            if (null != db) {
                db.closeJDBCObjects(conn, stm, rs);
            }
        }

        return result;
    }

    /**
     * Returns information on the server as found in the database (port numbers, affinity group, etc).
     *
     * @param connectionUrl
     * @param username
     * @param password
     * @param serverName the server whose details are to be returned
     * @return the information on the named server
     */
    public static ServerDetails getServerDetails(String connectionUrl, String username, String password,
            String serverName) {

        DatabaseType db = null;
        Connection conn = null;
        ServerDetails result = null;

        try {
            conn = getDatabaseConnection(connectionUrl, username, password);
            db = DatabaseTypeFactory.getDatabaseType(conn);

            result = getServerDetails(db, conn, serverName);

        } catch (Exception e) {
            LOG.info("Unable to get server detail: " + e.getMessage());
        } finally {
            if (null != db) {
                db.closeConnection(conn);
            }
        }

        return result;
    }

    private static ServerDetails getServerDetails(DatabaseType db, Connection conn, String serverName) {
        PreparedStatement stm = null;
        ResultSet rs = null;
        ServerDetails result = null;

        if (null == serverName) {
            return result;
        }

        try {
            stm = conn.prepareStatement("" //
                    + "SELECT s.address, s.port, s.secure_port " //
                    + "  FROM rhq_server s " //
                    + " WHERE s.name = ?");
            stm.setString(1, serverName.trim());

            rs = stm.executeQuery();

            if (rs.next()) {
                result = new ServerDetails(serverName, rs.getString(1), rs.getInt(2), rs.getInt(3));
            }
        } catch (SQLException e) {
            LOG.info("Unable to get server details for server [" + serverName + "]: " + e.getMessage());
        } finally {
            if (null != db) {
                db.closeResultSet(rs);
                db.closeStatement(stm);
            }
        }

        return result;
    }

    /**
     * Tests to make sure the server can be connected to with the given settings.
     * If the test is successful, <code>null</code>. If the test fails, the returned string
     * will be the error message to indicate the problem.
     *
     * @param connectionUrl
     * @param username
     * @param password
     * @return error message if test failed; <code>null</code> if test succeeded
     */
    public static String testConnection(String connectionUrl, String username, String password) {

        // its possible the JDBC URL was changed, clear the factory cache in case the DB version is different now
        DatabaseTypeFactory.clearDatabaseTypeCache();

        try {
            ensureDatabaseIsSupported(connectionUrl, username, password);
            return null;
        } catch (Exception e) {
            LOG.warn("Installer failed to test connection", e);
            return ThrowableUtil.getAllMessages(e);
        }
    }

    /**
     * Call this when you need to confirm that the database is supported.
     *
     * @param connectionUrl
     * @param username
     * @param password
     *
     * @throws Exception if the database is not supported
     */
    public static void ensureDatabaseIsSupported(String connectionUrl, String username, String password)
            throws Exception {
        Connection conn = null;
        DatabaseType db = null;

        try {
            conn = getDatabaseConnection(connectionUrl, username, password);
            db = DatabaseTypeFactory.getDatabaseType(conn);

            String version = db.getVersion();

            if (DatabaseTypeFactory.isPostgres(db)) {
                if (version.startsWith("7") || version.equals("8") || version.startsWith("8.0")
                        || version.startsWith("8.1")) {
                    throw new Exception("Unsupported PostgreSQL [" + db + "]");
                }
            } else if (DatabaseTypeFactory.isOracle(db)) {
                if (version.startsWith("8") || version.startsWith("9")) {
                    throw new Exception("Unsupported Oracle [" + db + "]");
                }
            } else {
                throw new Exception("Unsupported DB [" + db + "]");
            }

            LOG.info("Database is supported: " + db);
        } finally {
            if (db != null) {
                db.closeConnection(conn);
            }
        }

        return;
    }

    /**
     * Returns a database connection with the given set of properties providing the settings that allow for a successful
     * database connection. If <code>props</code> is <code>null</code>, it will use the server properties from
     * {@link #getServerProperties}.
     *
     * @param connectionUrl
     * @param userName
     * @param password
     * @return the database connection
     *
     * @throws SQLException if cannot successfully connect to the database
     */
    public static Connection getDatabaseConnection(String connectionUrl, String userName, String password)
            throws SQLException {
        return DbUtil.getConnection(connectionUrl, userName, password);
    }

    /**
     * @param serverProperties the server properties
     * @param dbpassword clear text password to connect to the database
     * @throws Exception
     */
    public static void persistAdminPasswordIfNecessary(HashMap<String, String> serverProperties, String dbpassword)
            throws Exception {
        DatabaseType db = null;
        Connection connection = null;
        Statement queryStatement = null;
        Statement insertStatement = null;
        ResultSet resultSet = null;

        try {
            String dbUrl = serverProperties.get(ServerProperties.PROP_DATABASE_CONNECTION_URL);
            String userName = serverProperties.get(ServerProperties.PROP_DATABASE_USERNAME);
            connection = getDatabaseConnection(dbUrl, userName, dbpassword);
            db = DatabaseTypeFactory.getDatabaseType(connection);

            if (!(db instanceof PostgresqlDatabaseType || db instanceof OracleDatabaseType)) {
                throw new IllegalArgumentException("Unknown database type, can't continue: " + db);
            }

            queryStatement = connection.createStatement();
            resultSet = queryStatement.executeQuery("SELECT count(*) FROM rhq_principal WHERE id=2");
            resultSet.next();

            if (resultSet.getInt(1) == 0) {
                connection.setAutoCommit(false);

                try {
                    LOG.info(
                            "Persisting admin password to database for property [rhq.autoinstall.server.admin.password]");

                    insertStatement = connection.createStatement();
                    insertStatement.executeUpdate("INSERT INTO rhq_principal VALUES (2, 'rhqadmin', '"
                            + serverProperties.get(ServerProperties.PROP_AUTOINSTALL_ADMIN_PASSWORD) + "')");

                    connection.commit();
                } catch (SQLException e) {
                    LOG.error(
                            "Failed to persist admin password to database for property [rhq.autoinstall.server.admin.password]. Transaction will be rolled back.",
                            e);
                    connection.rollback();
                    throw e;
                }
            } else {
                LOG.info(
                        "Admin user password is already set, property [rhq.autoinstall.server.admin.password] will be ignored.");
            }

        } finally {
            if (db != null) {
                db.closeResultSet(resultSet);
                db.closeStatement(queryStatement);
                db.closeStatement(insertStatement);
                db.closeConnection(connection);
            }
        }
    }

    /**
     * Persists the storage nodes to the database only if no storage node entities already exist. This method is used
     * to persist storage nodes created from the rhq.storage.nodes server configuration property. The only time those
     * seed nodes should be created is during an initial server installation. After the initial installation storage
     * nodes should be created using <code>rhqctl install</code>. This ensures that any necessary cluster maintenance
     * tasks will be performed.
     *
     * @param serverProperties the server properties
     * @param password clear text password to connect to the database
     * @param storageNodes the {@link StorageNode storage nodes} to persist
     * @throws Exception
     */
    public static void persistStorageNodesIfNecessary(HashMap<String, String> serverProperties, String password,
            Set<StorageNode> storageNodes) throws Exception {
        DatabaseType db = null;
        Connection connection = null;
        Statement queryStatement = null;
        ResultSet resultSet = null;
        PreparedStatement insertStorageNode = null;

        try {
            String dbUrl = serverProperties.get(ServerProperties.PROP_DATABASE_CONNECTION_URL);
            String userName = serverProperties.get(ServerProperties.PROP_DATABASE_USERNAME);
            connection = getDatabaseConnection(dbUrl, userName, password);
            db = DatabaseTypeFactory.getDatabaseType(connection);

            if (!(db instanceof PostgresqlDatabaseType || db instanceof OracleDatabaseType)) {
                throw new IllegalArgumentException("Unknown database type, can't continue: " + db);
            }

            queryStatement = connection.createStatement();
            resultSet = queryStatement.executeQuery("SELECT count(id) FROM rhq_storage_node");
            resultSet.next();

            if (resultSet.getInt(1) == 0) {
                connection.setAutoCommit(false);

                try {
                    LOG.info(
                            "Persisting to database new storage nodes for values specified in server configuration property [rhq.storage.nodes]");

                    insertStorageNode = connection.prepareStatement(
                            "INSERT INTO rhq_storage_node (id, address, cql_port, operation_mode, ctime, mtime, maintenance_pending) "
                                    + "VALUES (?, ?, ?, ?, ?, ?, ?)");

                    int id = 1001;
                    for (StorageNode storageNode : storageNodes) {
                        insertStorageNode.setInt(1, id);
                        insertStorageNode.setString(2, storageNode.getAddress());
                        insertStorageNode.setInt(3, storageNode.getCqlPort());
                        insertStorageNode.setString(4, StorageNode.OperationMode.INSTALLED.toString());
                        insertStorageNode.setLong(5, System.currentTimeMillis());
                        insertStorageNode.setLong(6, System.currentTimeMillis());
                        insertStorageNode.setBoolean(7, false);

                        insertStorageNode.executeUpdate();
                        id += 1;
                    }

                    connection.commit();
                } catch (SQLException e) {
                    LOG.error("Failed to persist to database the storage nodes specified by server configuration "
                            + "property [rhq.storage.nodes]. Transaction will be rolled back.", e);
                    connection.rollback();
                    throw e;
                }
            } else {
                LOG.info(
                        "Storage nodes already exist in database. Server configuration property [rhq.storage.nodes] will be ignored.");
            }

        } finally {
            if (db != null) {
                db.closeResultSet(resultSet);
                db.closeStatement(queryStatement);
                db.closeStatement(insertStorageNode);
                db.closeConnection(connection);
            }
        }
    }

    public static Map<String, String> fetchStorageClusterSettings(HashMap<String, String> serverProperties,
            String password) throws Exception {

        Map<String, String> result = new HashMap<String, String>(5);
        DatabaseType db = null;
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;

        try {
            String dbUrl = serverProperties.get(ServerProperties.PROP_DATABASE_CONNECTION_URL);
            String userName = serverProperties.get(ServerProperties.PROP_DATABASE_USERNAME);
            connection = getDatabaseConnection(dbUrl, userName, password);
            db = DatabaseTypeFactory.getDatabaseType(connection);

            if (!(db instanceof PostgresqlDatabaseType || db instanceof OracleDatabaseType)) {
                throw new IllegalArgumentException("Unknown database type, can't continue: " + db);
            }

            try {
                statement = connection.prepareStatement("" //
                        + "SELECT property_key, property_value FROM rhq_system_config " //
                        + " WHERE property_key LIKE 'STORAGE%' " //
                        + "   AND NOT property_value IS NULL ");
                resultSet = statement.executeQuery();

                while (resultSet.next()) {
                    String key = resultSet.getString(1);
                    String value = resultSet.getString(2);

                    if (key.equals("STORAGE_USERNAME")) {
                        result.put(ServerProperties.PROP_STORAGE_USERNAME, value);
                    } else if (key.equals("STORAGE_PASSWORD")) {
                        result.put(ServerProperties.PROP_STORAGE_PASSWORD, value);
                    } else if (key.equals("STORAGE_GOSSIP_PORT")) {
                        result.put(ServerProperties.PROP_STORAGE_GOSSIP_PORT, value);
                    } else if (key.equals("STORAGE_CQL_PORT")) {
                        result.put(ServerProperties.PROP_STORAGE_CQL_PORT, value);
                    }
                }
            } finally {
                db.closeResultSet(resultSet);
                db.closeStatement(statement);
            }

            try {
                statement = connection.prepareStatement("" //
                        + "SELECT address FROM rhq_storage_node " //
                        + " WHERE operation_mode in " + "('" + OperationMode.NORMAL.name() + "', '"
                        + OperationMode.INSTALLED.name() + "') ");
                resultSet = statement.executeQuery();

                StringBuffer addressList = new StringBuffer();
                while (resultSet.next()) {
                    String address = resultSet.getString(1);

                    if (address != null && !address.trim().isEmpty()) {
                        if (addressList.length() != 0) {
                            addressList.append(',');
                        }
                        addressList.append(address);
                    }
                }

                if (addressList.length() != 0) {
                    result.put(ServerProperties.PROP_STORAGE_NODES, addressList.toString());
                }
            } finally {
                db.closeResultSet(resultSet);
                db.closeStatement(statement);
            }
        } catch (SQLException e) {
            LOG.error("Failed to fetch storage cluster settings.", e);
            throw e;
        } finally {
            if (db != null) {
                db.closeConnection(connection);
            }
        }

        return result;
    }

    public static void persistStorageClusterSettingsIfNecessary(HashMap<String, String> serverProperties,
            String password) throws Exception {
        DatabaseType db = null;
        Connection connection = null;
        PreparedStatement updateClusterSetting = null;

        try {
            String dbUrl = serverProperties.get(ServerProperties.PROP_DATABASE_CONNECTION_URL);
            String userName = serverProperties.get(ServerProperties.PROP_DATABASE_USERNAME);
            connection = getDatabaseConnection(dbUrl, userName, password);
            db = DatabaseTypeFactory.getDatabaseType(connection);

            if (!(db instanceof PostgresqlDatabaseType || db instanceof OracleDatabaseType)) {
                throw new IllegalArgumentException("Unknown database type, can't continue: " + db);
            }

            connection = getDatabaseConnection(dbUrl, userName, password);
            connection.setAutoCommit(false);

            updateClusterSetting = connection.prepareStatement("" //
                    + "UPDATE rhq_system_config " //
                    + "   SET property_value = ?, default_property_value = ? " //
                    + " WHERE property_key = ? " //
                    + "   AND ( property_value IS NULL OR property_value = '' OR property_value = 'UNSET' ) ");

            updateClusterSetting.setString(1, serverProperties.get(ServerProperties.PROP_STORAGE_USERNAME));
            updateClusterSetting.setString(2, serverProperties.get(ServerProperties.PROP_STORAGE_USERNAME));
            updateClusterSetting.setString(3, "STORAGE_USERNAME");
            updateClusterSetting.executeUpdate();

            updateClusterSetting.setString(1, serverProperties.get(ServerProperties.PROP_STORAGE_PASSWORD));
            updateClusterSetting.setString(2, serverProperties.get(ServerProperties.PROP_STORAGE_PASSWORD));
            updateClusterSetting.setString(3, "STORAGE_PASSWORD");
            updateClusterSetting.executeUpdate();

            updateClusterSetting.setString(1, serverProperties.get(ServerProperties.PROP_STORAGE_CQL_PORT));
            updateClusterSetting.setString(2, serverProperties.get(ServerProperties.PROP_STORAGE_CQL_PORT));
            updateClusterSetting.setString(3, "STORAGE_CQL_PORT");
            updateClusterSetting.executeUpdate();

            updateClusterSetting.setString(1, serverProperties.get(ServerProperties.PROP_STORAGE_GOSSIP_PORT));
            updateClusterSetting.setString(2, serverProperties.get(ServerProperties.PROP_STORAGE_GOSSIP_PORT));
            updateClusterSetting.setString(3, "STORAGE_GOSSIP_PORT");
            updateClusterSetting.executeUpdate();

            connection.commit();
        } catch (SQLException e) {
            LOG.error("Failed to initialize storage cluster settings. Transaction will be rolled back.", e);
            connection.rollback();
            throw e;
        } finally {
            if (db != null) {
                db.closeStatement(updateClusterSetting);
                db.closeConnection(connection);
            }
        }
    }

    /**
     * Stores the server details (such as the public endpoint) in the database. If the server definition already
     * exists, it will be updated; otherwise, a new server will be added to the HA cloud.
     *
     * @param serverProperties the server properties
     * @param password clear text password to connect to the database
     * @param serverDetails the details of the server to put into the database
     * @throws Exception
     */
    public static void storeServerDetails(HashMap<String, String> serverProperties, String password,
            ServerDetails serverDetails) throws Exception {

        DatabaseType db = null;
        Connection conn = null;

        try {
            String dbUrl = serverProperties.get(ServerProperties.PROP_DATABASE_CONNECTION_URL);
            String userName = serverProperties.get(ServerProperties.PROP_DATABASE_USERNAME);
            conn = getDatabaseConnection(dbUrl, userName, password);
            db = DatabaseTypeFactory.getDatabaseType(conn);

            updateOrInsertServer(db, conn, serverDetails);

        } catch (SQLException e) {
            // TODO: should we throw an exception here? This would abort the rest of the installation
            LOG.info("Unable to store server entry in the database: " + ThrowableUtil.getAllMessages(e));
        } finally {
            if (null != db) {
                db.closeConnection(conn);
            }
        }
    }

    private static void updateOrInsertServer(DatabaseType db, Connection conn, ServerDetails serverDetails) {
        PreparedStatement stm = null;
        ResultSet rs = null;

        if (null == serverDetails || isEmpty(serverDetails.getName())) {
            return;
        }

        try {
            stm = conn.prepareStatement("UPDATE rhq_server SET address=?, port=?, secure_port=? WHERE name=?");
            stm.setString(1, serverDetails.getEndpointAddress());
            stm.setInt(2, serverDetails.getEndpointPort());
            stm.setInt(3, serverDetails.getEndpointSecurePort());
            stm.setString(4, serverDetails.getName());
            if (0 == stm.executeUpdate()) {
                stm.close();

                // set all new servers to operation_mode=INSTALLED
                int i = 1;
                if (db instanceof PostgresqlDatabaseType || db instanceof OracleDatabaseType) {
                    stm = conn.prepareStatement("INSERT INTO rhq_server " //
                            + " ( id, name, address, port, secure_port, ctime, mtime, operation_mode, compute_power ) " //
                            + "VALUES ( ?, ?, ?, ?, ?, ?, ?, 'INSTALLED', 1 )");
                    stm.setInt(i++, db.getNextSequenceValue(conn, "rhq_server", "id"));
                } else {
                    throw new IllegalArgumentException("Unknown database type, can't continue: " + db);
                }

                stm.setString(i++, serverDetails.getName());
                stm.setString(i++, serverDetails.getEndpointAddress());
                stm.setInt(i++, serverDetails.getEndpointPort());
                stm.setInt(i++, serverDetails.getEndpointSecurePort());
                long now = System.currentTimeMillis();
                stm.setLong(i++, now);
                stm.setLong(i++, now);
                stm.executeUpdate();
            }

        } catch (SQLException e) {
            LOG.info("Unable to put the server details in the database: " + ThrowableUtil.getAllMessages(e));
        } finally {
            if (null != db) {
                db.closeResultSet(rs);
                db.closeStatement(stm);
            }
        }
    }

    /**
     * This will create the database schema in the database. <code>props</code> define the connection to the database -
     *
     * <p>Note that if the {@link #isDatabaseSchemaExist schema already exists}, it will be purged of all
     * data/tables and recreated.</p>
     *
     * @param props the full set of server properties
     * @param serverDetails additional information about the server being installed
     * @param password the database password in clear text
     * @param logDir a directory where the db schema upgrade logs can be written
     *
     * @throws Exception if failed to create the new schema for some reason
     */
    public static void createNewDatabaseSchema(HashMap<String, String> props, ServerDetails serverDetails,
            String password, String logDir) throws Exception {
        String dbUrl = props.get(ServerProperties.PROP_DATABASE_CONNECTION_URL);
        String userName = props.get(ServerProperties.PROP_DATABASE_USERNAME);

        try {
            // extract the dbsetup files which are located in the dbutils jar
            String dbsetupSchemaXmlFile = extractDatabaseXmlFile("db-schema-combined.xml", props, serverDetails,
                    logDir);
            String dbsetupDataXmlFile = extractDatabaseXmlFile("db-data-combined.xml", props, serverDetails,
                    logDir);

            // first uninstall any old existing schema, then create the tables then insert the data
            DBSetup dbsetup = new DBSetup(dbUrl, userName, password);
            dbsetup.uninstall(dbsetupSchemaXmlFile);
            dbsetup.setup(dbsetupSchemaXmlFile);
            dbsetup.setup(dbsetupDataXmlFile, null, true, false);
        } catch (Exception e) {
            LOG.fatal("Cannot install the database schema - the server will not run properly.", e);
            throw e;
        }

        return;
    }

    /**
     * This will update an existing database schema so it can be upgraded to the latest schema version.
     *
     * <p>Note that if the {@link #isDatabaseSchemaExist schema does not already exist}, errors will
     * occur.</p>
     *
     * @param props the full set of server properties
     * @param serverDetails additional information about the server being installed
     * @param password the database password in clear text
     * @param logDir a directory where the db schema upgrade logs can be written
     *
     * @throws Exception if the upgrade failed for some reason
     */
    public static void upgradeExistingDatabaseSchema(HashMap<String, String> props, ServerDetails serverDetails,
            String password, String logDir) throws Exception {
        String dbUrl = props.get(ServerProperties.PROP_DATABASE_CONNECTION_URL);
        String userName = props.get(ServerProperties.PROP_DATABASE_USERNAME);

        File logfile = new File(logDir, "rhq-installer-dbupgrade.log");

        logfile.delete(); // do not keep logs from previous dbupgrade runs

        try {
            // extract the dbupgrade ANT script which is located in the dbutils jar
            String dbupgradeXmlFile = extractDatabaseXmlFile("db-upgrade.xml", props, serverDetails, logDir);

            Properties antProps = new Properties();
            antProps.setProperty("jdbc.url", dbUrl);
            antProps.setProperty("jdbc.user", userName);
            antProps.setProperty("jdbc.password", password);
            antProps.setProperty("target.schema.version", "LATEST");

            startAnt(new File(dbupgradeXmlFile), "db-ant-tasks.properties", antProps, logfile);
        } catch (Exception e) {
            LOG.fatal("Cannot upgrade the database schema - the server will not run properly.", e);
            throw e;
        }

        return;
    }

    /**
     * Given a server property value string, returns true if it is not specified.
     *
     * @param s the property string value
     *
     * @return true if it is null or empty
     */
    public static boolean isEmpty(String s) {
        return s == null || s.trim().length() == 0;
    }

    /**
     * Takes the named XML file from the classloader and writes the file to the log directory. This is meant to extract
     * the schema/data xml files from the dbutils jar file. It can also be used to extract the db upgrade XML file.
     *
     * @param xmlFileName the name of the XML file, as found in the classloader
     * @param props properties whose values are used to replace the replacement strings found in the XML file
     * @param serverDetails additional information about the server being installed
     * @param logDir a directory where the db schema upgrade logs can be written
     *
     * @return the absolute path to the extracted file
     *
     * @throws IOException if failed to extract the file to the log directory
     */
    private static String extractDatabaseXmlFile(String xmlFileName, HashMap<String, String> props,
            ServerDetails serverDetails, String logDir) throws IOException {

        // first slurp the file contents in memory
        InputStream resourceInStream = ServerInstallUtil.class.getClassLoader().getResourceAsStream(xmlFileName);
        ByteArrayOutputStream contentOutStream = new ByteArrayOutputStream();
        StreamUtil.copy(resourceInStream, contentOutStream);

        // now replace their replacement strings with values from the properties
        String emailFromAddress = props.get(ServerProperties.PROP_EMAIL_FROM_ADDRESS);
        if (isEmpty(emailFromAddress)) {
            emailFromAddress = "rhqadmin@localhost";
        }

        String httpPort = props.get(ServerProperties.PROP_WEB_HTTP_PORT);
        if (isEmpty(httpPort)) {
            httpPort = String.valueOf(ServerDetails.DEFAULT_ENDPOINT_PORT);
        }

        String publicEndpoint = serverDetails.getEndpointAddress();
        if (isEmpty(publicEndpoint)) {
            try {
                publicEndpoint = props.get(ServerProperties.PROP_JBOSS_BIND_ADDRESS);
                if (isEmpty(publicEndpoint) || ("0.0.0.0".equals(publicEndpoint))) {
                    publicEndpoint = InetAddress.getLocalHost().getHostAddress();
                }
            } catch (Exception e) {
                publicEndpoint = "127.0.0.1";
            }
        }

        String content = contentOutStream.toString();
        content = content.replaceAll("@@@LARGE_TABLESPACE_FOR_DATA@@@", "DEFAULT");
        content = content.replaceAll("@@@LARGE_TABLESPACE_FOR_INDEX@@@", "DEFAULT");
        content = content.replaceAll("@@@ADMINUSERNAME@@@", "rhqadmin");
        content = content.replaceAll("@@@ADMINPASSWORD@@@", "x1XwrxKuPvYUILiOnOZTLg=="); // rhqadmin
        content = content.replaceAll("@@@ADMINEMAIL@@@", emailFromAddress);
        content = content.replaceAll("@@@BASEURL@@@", "http://" + publicEndpoint + ":" + httpPort + "/");
        content = content.replaceAll("@@@JAASPROVIDER@@@", "JDBC");
        content = content.replaceAll("@@@LDAPURL@@@", "ldap://localhost/");
        content = content.replaceAll("@@@LDAPPROTOCOL@@@", "");
        content = content.replaceAll("@@@LDAPLOGINPROP@@@", "cn");
        content = content.replaceAll("@@@LDAPBASEDN@@@", "o=JBoss,c=US");
        content = content.replaceAll("@@@LDAPSEARCHFILTER@@@", "");
        content = content.replaceAll("@@@LDAPBINDDN@@@", "");
        content = content.replaceAll("@@@LDAPBINDPW@@@", "");
        content = content.replaceAll("@@@MULTICAST_ADDR@@@", "");
        content = content.replaceAll("@@@MULTICAST_PORT@@@", "");

        // we now have the finished XML content - write out the file to the log directory
        File xmlFile = new File(logDir, xmlFileName);
        FileOutputStream xmlFileOutStream = new FileOutputStream(xmlFile);
        ByteArrayInputStream contentInStream = new ByteArrayInputStream(content.getBytes());
        StreamUtil.copy(contentInStream, xmlFileOutStream);

        return xmlFile.getAbsolutePath();
    }

    /**
     * Launches ANT and runs the default target in the given build file.
     *
     * @param  buildFile      the build file that ANT will run
     * @param  customTaskDefs the properties file found in classloader that contains all the taskdef definitions
     * @param  properties     set of properties to set for the ANT task to access
     * @param  logFile        where ANT messages will be logged (in addition to the app server's log file)
     *
     * @throws RuntimeException
     */
    private static void startAnt(File buildFile, String customTaskDefs, Properties properties, File logFile) {
        PrintWriter logFileOutput = null;

        try {
            logFileOutput = new PrintWriter(new FileOutputStream(logFile));

            ClassLoader classLoader = ServerInstallUtil.class.getClassLoader();

            Properties taskDefs = new Properties();
            InputStream taskDefsStream = classLoader.getResourceAsStream(customTaskDefs);
            try {
                taskDefs.load(taskDefsStream);
            } finally {
                taskDefsStream.close();
            }

            Project project = new Project();
            project.setCoreLoader(classLoader);
            project.init();

            for (Map.Entry<Object, Object> property : properties.entrySet()) {
                project.setProperty(property.getKey().toString(), property.getValue().toString());
            }

            // notice we add our listener after we set the properties - we do not want the password to be in the log file
            // our dbupgrade script will echo the property settings, so we can still get the other values
            project.addBuildListener(new LoggerAntBuildListener(logFileOutput));

            for (Map.Entry<Object, Object> taskDef : taskDefs.entrySet()) {
                project.addTaskDefinition(taskDef.getKey().toString(),
                        Class.forName(taskDef.getValue().toString(), true, classLoader));
            }

            new ProjectHelper2().parse(project, buildFile);
            project.executeTarget(project.getDefaultTarget());

        } catch (Exception e) {
            throw new RuntimeException("Cannot run ANT on script [" + buildFile + "]. Cause: " + e, e);
        } finally {
            if (logFileOutput != null) {
                logFileOutput.close();
            }
        }
    }

    /**
     * Ensures our web connectors are configured properly.
     *
     * @param mcc the AS client
     * @param configDirStr location of a configuration directory where the keystore is to be stored
     * @param serverProperties the full set of server properties
     * @throws Exception
     */
    public static void setupWebConnectors(ModelControllerClient mcc, String configDirStr,
            HashMap<String, String> serverProperties) throws Exception {

        // 6.3.0.Alpha1 (aka 7.4.0.Final-redhat-4) has a bug (BZ 1133061) that can be worked around by changing the connector protocol
        // This bug was fixed in 6.3.0.GA (aka 7.4.0.Final-redhat-19) so we don't need the workaround for anything after that.
        // AFAIK, it isn't bad if we install the workaround, but we want to keep the original config if we can help it so we try to
        // determine if we don't need it - if we don't, we won't install the workaround.
        String appServerVersion = getAppServerReleaseVersion(mcc);
        if (appServerVersion == null) {
            appServerVersion = "7.4.0.Final-redhat-4";
            LOG.warn("Will assume app server release version is [" + appServerVersion + "]");
        }
        boolean needProtocolWorkaround = false;
        if (appServerVersion.startsWith("7.4.0")) {
            if (appServerVersion.endsWith("redhat-4")) {
                needProtocolWorkaround = true;
            }
        }
        if (needProtocolWorkaround) {
            LOG.info("App server version is [" + appServerVersion
                    + "] - will apply workaround to http and https connectors as per BZ 1133061");
        } else {
            LOG.debug(
                    "No BZ 1133061 workaround will be applied to http and https connectors since app server version is: "
                            + appServerVersion);
        }

        // out of box, we always get a non-secure connector (called "http")...
        final String connectorName = "http";

        // ...but we want a secure SSL connector, too.
        // This is the name of the secure connector we want to create.
        final String sslConnectorName = "https";

        WebJBossASClient client = new WebJBossASClient(mcc);

        // because some of the connector attributes do not (yet) support expressions, let's remove any existing
        // connector we may have created before and create it again with our current attribute values.
        client.removeConnector(sslConnectorName);

        LOG.info("Creating https connector...");
        ConnectorConfiguration connector = buildSecureConnectorConfiguration(configDirStr, serverProperties);

        // verify that we have a truststore file - if user is relying on our self-signed certs, we'll have to create one for them
        String truststoreFileString = connector.getSslConfiguration().getCaCertificateFile();
        truststoreFileString = resolveExpression(mcc, truststoreFileString);
        if (truststoreFileString == null) {
            LOG.warn("Missing a valid truststore location - you must specify a valid truststore location!");
        } else {
            File truststoreFile = new File(truststoreFileString);
            if (!truststoreFile.exists()) {
                // user didn't provide a truststore file, copy the keystore and use it as the truststore; tell the user about this
                String keystoreFileString = connector.getSslConfiguration().getCertificateKeyFile();
                keystoreFileString = resolveExpression(mcc, keystoreFileString);
                File keystoreFile = new File(keystoreFileString);
                if (!keystoreFile.isFile()) {
                    LOG.warn(
                            "Missing both keystore [" + keystoreFile + "] and truststore [" + truststoreFile + "]");
                } else {
                    LOG.warn("Missing the truststore [" + truststoreFile + "] - will copy the keystore ["
                            + keystoreFile + "] and make the copy the truststore.");
                    try {
                        FileUtil.copyFile(keystoreFile, truststoreFile);
                    } catch (Exception e) {
                        LOG.error("Failed to copy keystore to make truststore - a truststore still does not exist",
                                e);
                    }
                }
            }
        }

        if (needProtocolWorkaround) {
            connector.setProtocol("org.apache.coyote.http11.Http11Protocol");
        }

        client.addConnector("https", connector);
        LOG.info("https connector created.");

        if (client.isConnector(connectorName)) {
            client.changeConnector(connectorName, "max-connections",
                    buildExpression("rhq.server.startup.web.max-connections", serverProperties, true));
            client.changeConnector(connectorName, "redirect-port",
                    buildExpression("rhq.server.socket.binding.port.https", serverProperties, true));

            if (needProtocolWorkaround) {
                client.changeConnector(connectorName, "protocol", "org.apache.coyote.http11.Http11Protocol");
            }
        } else {
            LOG.warn("There doesn't appear to be a http connector configured already - this is strange.");
        }
    }

    private static String resolveExpression(ModelControllerClient mcc, String expression) {
        if (expression == null) {
            return null;
        }

        CoreJBossASClient client = new CoreJBossASClient(mcc);
        String resolvedExpression;
        try {
            resolvedExpression = client.resolveExpression(expression);

            // https://issues.jboss.org/browse/WFLY-1177 - app server doesn't do recursive resolving, we have to do it here
            while (resolvedExpression != null && resolvedExpression.contains("${")
                    && !resolvedExpression.equals(expression)) {
                expression = resolvedExpression;
                resolvedExpression = client.resolveExpression(expression);
            }
        } catch (Exception e) {
            LOG.warn("Cannot resolve expression [" + expression
                    + "]; will use it as-is but errors may occur later.");
            resolvedExpression = expression;
        }
        return resolvedExpression;
    }

    private static ConnectorConfiguration buildSecureConnectorConfiguration(String configDirStr,
            HashMap<String, String> serverProperties) {

        SSLConfiguration ssl = new SSLConfiguration();

        // Because of https://issues.jboss.org/browse/WFLY-1177 we cannot build expressions for key/truststore files.
        // Otherwise, we end up with recursive expressions (${${x}:a}) which is what's broken. For now, just use ${x} which is allowed.

        // truststore
        ssl.setCaCertificateFile(
                buildExpression("rhq.server.tomcat.security.truststore.file", serverProperties, false));
        ssl.setCaCertificationPassword(buildExpression("rhq.server.tomcat.security.truststore.password",
                serverProperties, true, true, true));
        ssl.setTruststoreType(
                buildExpression("rhq.server.tomcat.security.truststore.type", serverProperties, true, true, false));

        // keystore
        ssl.setCertificateKeyFile(
                buildExpression("rhq.server.tomcat.security.keystore.file", serverProperties, false));
        ssl.setPassword(buildExpression("rhq.server.tomcat.security.keystore.password", serverProperties, true,
                true, true));
        ssl.setKeyAlias(buildExpression("rhq.server.tomcat.security.keystore.alias", serverProperties, true));
        ssl.setKeystoreType(buildExpression("rhq.server.tomcat.security.keystore.type", serverProperties, true));

        // SSL protocol config
        ssl.setProtocol(
                buildExpression("rhq.server.tomcat.security.secure-socket-protocol", serverProperties, true));
        ssl.setVerifyClient(buildExpression("rhq.server.tomcat.security.client-auth-mode", serverProperties, true));

        // note: there doesn't appear to be a way for AS7 to support algorithm, like SunX509 or IbmX509
        // so I think it just uses the JVM's default. This means "rhq.server.tomcat.security.algorithm" is unused

        ConnectorConfiguration connector = new ConnectorConfiguration();
        connector.setMaxConnections(
                buildExpression("rhq.server.startup.web.max-connections", serverProperties, true));
        connector.setScheme("https");
        connector.setSocketBinding("https");
        connector.setSslConfiguration(ssl);
        return connector;
    }

    /**
     * For a property whose value might be a file, return that file's absolute path. If the property
     * has a value whose pathname is already absolute, return it. If the property has a value whose path
     * is relative, it is considered relative to defaultRootDir and its absolute path based on that root dir
     * is returned.
     *
     * @param propertyName the property whose value in properties is considered a pathname (which may
     *                     relative or it may be absolute).
     * @param properties where to find the named property
     * @param defaultRootDir if the property value is a relative file path, this is what it is relative to
     * @return the absolute path of the file
     */
    private static String getAbsoluteFileLocation(String propertyName, HashMap<String, String> properties,
            String defaultRootDir) {

        if (properties == null || !properties.containsKey(propertyName)) {
            return null;
        }

        String propertyValue = properties.get(propertyName);
        File path = new File(propertyValue);
        if (path.isAbsolute()) {
            return path.getAbsolutePath();
        } else {
            return new File(defaultRootDir, propertyValue).getAbsolutePath();
        }
    }

    private static String buildExpression(String propName, HashMap<String, String> defaultProperties,
            boolean supportsExpression) {
        return buildExpression(propName, defaultProperties, supportsExpression, false, false);
    }

    /**
     * You would think this would be simple - just set a value to ${propName:defaultVal} but some
     * JBossAS attributes don't support expressions when you think they could or should. In this
     * case, we'll pass in supportsExpression=false until we support AS versions that support
     * expressions (at which time we'll change to code to pass in true for those attributes).
     * If supportsExpression is true, the returned string will be ${propName} if defaultProperties is
     * null or doesn't have that property defined; ${propName:defaultVal} if the default property
     * is found.
     * If supportsExpression is false, this will take the actual property value for
     * propName found in the given default properties and return that string.
     * If there is no sysprop of that name, this method returns an empty string.
     *
     * @param propName
     * @param defaultProperties
     * @param supportsExpression
     * @return the attribute expression value (or real value if supportsExpression is false).
     */
    private static String buildExpression(String propName, HashMap<String, String> defaultProperties,
            boolean supportsExpression, boolean vault, boolean restricted) {

        if (supportsExpression) {
            String expressionFormat = null;

            if (!vault) {
                if ((defaultProperties != null) && (defaultProperties.containsKey(propName))) {
                    expressionFormat = "${%s:%s}";
                } else {
                    expressionFormat = "${%s}";
                }
            } else {
                String attributeType = "restricted";
                if (!restricted) {
                    attributeType = "open";
                }
                if ((defaultProperties != null) && (defaultProperties.containsKey(propName))) {
                    expressionFormat = "${VAULT::" + attributeType + "::%s::%s}";
                } else {
                    expressionFormat = "${VAULT::" + attributeType + "::%s:: }";
                }
            }

            if ((defaultProperties != null) && (defaultProperties.containsKey(propName))) {
                String value = defaultProperties.get(propName);

                if (RestrictedFormat.isRestrictedFormat(value)) {
                    value = RestrictedFormat.retrieveValue(value);
                }

                return String.format(expressionFormat, propName, value);
            } else {
                return String.format(expressionFormat, propName);
            }
        } else {
            if ((defaultProperties != null) && (defaultProperties.containsKey(propName))) {
                return defaultProperties.get(propName);
            } else {
                LOG.warn("There is no known value for property [" + propName + "]");
                return "";
            }
        }
    }

    /**
     * Creates a keystore whose cert has a CN of this server's public endpoint address.
     *
     * @param serverDetails details of the server being installed - must not be null and endpoint must be included in it
     * @param configDirStr location of a configuration directory where the keystore is to be stored
     * @return where the keystore file should be created (if an error occurs, this file won't exist)
     */
    public static File createKeystore(ServerDetails serverDetails, String configDirStr) {
        File confDir = new File(configDirStr);
        File keystore = new File(confDir, "rhq.keystore");
        File keystoreBackup = new File(confDir, "rhq.keystore.backup");

        // if there is one out-of-box, we want to remove it and create one with our proper CN
        if (keystore.exists()) {
            keystoreBackup.delete();
            if (!keystore.renameTo(keystoreBackup)) {
                LOG.warn("Cannot backup existing keystore - cannot generate a new cert with a proper domain name. ["
                        + keystore + "] will be the keystore used by this server");
                return keystore;
            }
        }

        try {
            String keystorePath = keystore.getAbsolutePath();
            String keyAlias = "RHQ";
            String domainName = "CN=" + serverDetails.getEndpointAddress() + ", OU=RHQ, O=rhq-project.org, C=US";
            String keystorePassword = "RHQManagement";
            String keyPassword = keystorePassword;
            String keyAlgorithm = "rsa";
            int validity = 7300;
            SecurityUtil.createKeyStore(keystorePath, keyAlias, domainName, keystorePassword, keyPassword,
                    keyAlgorithm, validity);
            LOG.info("New keystore created [" + keystorePath + "] with cert domain name of [" + domainName + "]");
        } catch (Exception e) {
            LOG.warn("Could not generate a new cert with a proper domain name, will use the original keystore");
            keystore.delete();
            if (!keystoreBackup.renameTo(keystore)) {
                LOG.warn("Failed to restore the original keystore from backup - please rename [" + keystoreBackup
                        + "] to [" + keystore + "]");
            }
        }

        return keystore;
    }

    /**
     * Create an rhqadmin management user so when discovered, the AS7 plugin can use it to connect
     * to the RHQ Server.  The password is set in rhq-server.properties.  Because the plugin can't guess
     * the password, if not set to the default then the AS7 plugin will fail to connect, and the
     * RHQ Server resource connection properties will need to be updated after discovery and import.
     *
     * @param password the management password
     * @param serverDetails details of the server being installed
     * @param configDirStr location of a configuration directory where the mgmt-users.properties file lives
     */
    public static void createDefaultManagementUser(String password, ServerDetails serverDetails,
            String configDirStr) {
        File confDir = new File(configDirStr);
        File mgmtUsers = new File(confDir, "mgmt-users.properties");

        if (ServerInstallUtil.isEmpty(password)) {
            LOG.warn("Could not create default management user in file: [" + mgmtUsers + "] : invalid password ["
                    + password + "].");
            return;
        }

        // Add the default admin user, or if for some reason this file does not exist, just log the issue
        if (mgmtUsers.exists()) {
            try {
                PropertiesFileUpdate mgmtUsersPropFile = new PropertiesFileUpdate(mgmtUsers.getAbsolutePath());
                Properties existingUsers = mgmtUsersPropFile.loadExistingProperties();
                if (existingUsers.containsKey(RHQ_MGMT_USER)) {
                    LOG.info("There is already a mgmt user named [" + RHQ_MGMT_USER + "], will not create another");
                    return;
                }
            } catch (Exception e) {
                LOG.warn(
                        "Cannot determine if mgmt user exists in [" + mgmtUsers + "]; will try to create it anyway",
                        e);
            }

            FileOutputStream fos = null;

            try {
                String encodedPassword = new UsernamePasswordHashUtil().generateHashedHexURP(RHQ_MGMT_USER,
                        "ManagementRealm", password.toCharArray());

                fos = new FileOutputStream(mgmtUsers, true);
                fos.write(("\n" + RHQ_MGMT_USER + "=" + encodedPassword + "\n").getBytes());

            } catch (Exception e) {
                LOG.warn("Could not create default management user in file: [" + mgmtUsers + "] : ", e);

            } finally {
                StreamUtil.safeClose(fos);
            }
        } else {
            LOG.warn("Could not create default management user. Could not find file: [" + mgmtUsers + "]");
        }
    }

    public static void setSocketBindings(ModelControllerClient mcc, HashMap<String, String> serverProperties)
            throws Exception {

        final SocketBindingJBossASClient client = new SocketBindingJBossASClient(mcc);
        for (SocketBindingInfo binding : defaultSocketBindings) {
            // use the port defined by the server's properties if set, otherwise, just use our hardcoded default
            int newPort = binding.port;
            String overrideValue = serverProperties.get(binding.sysprop);
            if (overrideValue != null) {
                try {
                    newPort = Integer.parseInt(overrideValue);
                } catch (Exception e) {
                    LOG.warn("Invalid port in system property [" + binding.sysprop + "]: " + overrideValue);
                }
            }
            LOG.info(String.format("Setting socket binding [%s] to [${%s:%d}]", binding.name, binding.sysprop,
                    newPort));
            try {
                client.setStandardSocketBindingPortExpression(binding.name, binding.sysprop, newPort);
            } catch (Exception e) {
                // If the binding is required, we re-throw a possible exception. Otherwise just log
                if (binding.required) {
                    throw e;
                } else {
                    LOG.info(String.format(
                            "Setting socket binding port for [%s] resulted in [%s] - this is harmless ",
                            binding.name, e.getMessage())); // TODO log at debug level only?
                }
            }

            // if we need to switch the binding's interface, do it now
            if (binding.interfaceName != null) {
                LOG.info(String.format("Setting socket binding [%s] to use interface [%s]", binding.name,
                        binding.interfaceName));
                try {
                    client.setStandardSocketBindingInterface(binding.name, binding.interfaceName);
                } catch (Exception e) {
                    // If the binding is required, we re-throw a possible exception. Otherwise just log
                    if (binding.required) {
                        throw e;
                    } else {
                        LOG.info(String.format(
                                "Setting socket binding interface for [%s] resulted in [%s] - this is harmless ",
                                binding.name, e.getMessage())); // TODO log at debug level only?
                    }
                }
            }
        }

        return;
    }

    /**
     * In case some things should only be installed if the underlying app server is a specific version,
     * call this to get the release-version of the app server. Release versions are something like:
     * 7.4.0.Final-redhat-4 (which corresponds to a EAP product-version of "6.3.0.Alpha1")
     * 7.4.0.Final-redhat-19 (which corresponds to a EAP product-version of "6.3.0.GA")
     *
     * @return release version or null if not known/can't be determined
     */
    private static String getAppServerReleaseVersion(ModelControllerClient mcc) {
        try {
            return new CoreJBossASClient(mcc).getAppServerVersion();
        } catch (Exception e) {
            LOG.warn(
                    "Cannot determine what the underlying app server release version is - installation may not work");
            return null;
        }
    }
}