/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 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.system;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.DateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;

import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ejb.Timeout;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.management.MBeanServerConnection;
import javax.management.MBeanServerInvocationHandler;
import javax.management.ObjectName;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.sql.DataSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.jboss.annotation.IgnoreDependency;
import org.jboss.deployment.MainDeployerMBean;
import org.jboss.mx.util.MBeanServerLocator;

import org.rhq.core.db.DatabaseType;
import org.rhq.core.db.DatabaseTypeFactory;
import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.authz.Permission;
import org.rhq.core.domain.common.ProductInfo;
import org.rhq.core.domain.common.ServerDetails;
import org.rhq.core.domain.common.SystemConfiguration;
import org.rhq.core.domain.common.ServerDetails.Detail;
import org.rhq.core.domain.common.composite.SystemSetting;
import org.rhq.core.domain.common.composite.SystemSettings;
import org.rhq.core.domain.configuration.definition.PropertySimpleType;
import org.rhq.core.server.PersistenceUtility;
import org.rhq.core.util.ObjectNameFactory;
import org.rhq.core.util.StopWatch;
import org.rhq.enterprise.server.RHQConstants;
import org.rhq.enterprise.server.auth.SubjectManagerLocal;
import org.rhq.enterprise.server.authz.RequiredPermission;
import org.rhq.enterprise.server.core.CoreServerMBean;
import org.rhq.enterprise.server.core.CustomJaasDeploymentServiceMBean;
import org.rhq.enterprise.server.plugin.pc.MasterServerPluginContainer;
import org.rhq.enterprise.server.plugin.pc.ServerPluginEnvironment;
import org.rhq.enterprise.server.plugin.pc.drift.DriftServerPluginContainer;
import org.rhq.enterprise.server.plugin.pc.drift.DriftServerPluginManager;
import org.rhq.enterprise.server.util.LookupUtil;
import org.rhq.enterprise.server.util.SystemDatabaseInformation;

@Stateless
public class SystemManagerBean implements SystemManagerLocal, SystemManagerRemote {
    private final String SQL_VACUUM = "VACUUM ANALYZE {0}";

    private final String SQL_ANALYZE = "ANALYZE";

    private final String SQL_REINDEX = "REINDEX TABLE {0}";

    private final String SQL_REBUILD = "ALTER INDEX {0} REBUILD UNRECOVERABLE";

    private final String[] TABLES_TO_VACUUM = { "RHQ_RESOURCE", "RHQ_CONFIG", "RHQ_CONFIG_PROPERTY", "RHQ_AGENT" };

    private final String[] TABLES_TO_REINDEX = { "RHQ_MEASUREMENT_DATA_NUM_1D", "RHQ_MEASUREMENT_DATA_NUM_6H",
        "RHQ_MEASUREMENT_DATA_NUM_1H", "RHQ_MEASUREMENT_DATA_TRAIT", "RHQ_CALLTIME_DATA_KEY",
        "RHQ_CALLTIME_DATA_VALUE", "RHQ_AVAILABILITY" };

    private final String[] ORA_INDEXES_TO_REBUILD = { "RHQ_MEAS_DATA_1H_ID_TIME_PK", "RHQ_MEAS_DATA_6H_ID_TIME_PK",
        "RHQ_MEAS_DATA_1D_ID_TIME_PK", "RHQ_MEAS_BASELINE_CTIME_IDX", "RHQ_MEAS_DATA_TRAIT_ID_TIME_PK" };

    private Log log = LogFactory.getLog(SystemManagerBean.class);

    @PersistenceContext(unitName = RHQConstants.PERSISTENCE_UNIT_NAME)
    private EntityManager entityManager;

    @javax.annotation.Resource(name = "RHQ_DS", mappedName = RHQConstants.DATASOURCE_JNDI_NAME)
    private DataSource dataSource;

    @javax.annotation.Resource
    private TimerService timerService;

    @EJB
    private SystemManagerLocal systemManager;

    @EJB
    @IgnoreDependency
    private SubjectManagerLocal subjectManager;

    private static SystemSettings cachedSystemSettings = null;
    private final String TIMER_DATA = "SystemManagerBean.reloadConfigCache";

    @SuppressWarnings("unchecked")
    public void scheduleConfigCacheReloader() {
        // each time the webapp is reloaded, we don't want to create duplicate jobs
        Collection<Timer> timers = timerService.getTimers();
        for (Timer existingTimer : timers) {
            log.debug("Found timer - attempting to cancel: " + existingTimer.toString());
            try {
                existingTimer.cancel();
            } catch (Exception e) {
                log.warn("Failed in attempting to cancel timer: " + existingTimer.toString());
            }
        }

        // single-action timer that will trigger in 60 seconds
        timerService.createTimer(60000L, TIMER_DATA);
    }

    @Timeout
    public void reloadConfigCache(Timer timer) {
        try {
            // note: I could have added a timestamp column, looked at it and see if its newer than our
            //       currently cached config and if so, load in the config then. But that's still
            //       1 roundtrip to the database, and maybe 2 (if the config is different). So we
            //       still have the same amount of roundtrips, and to make the code easier, without
            //       a need for the timestamp column/checking, we just load in the full config now.
            //       We never need 2 round trips, and this table is small enough that selecting the
            //       all its rows is really not going to effect performance much.
            systemManager.loadSystemConfigurationCacheInNewTx();
        } catch (Throwable t) {
            log.error("Failed to reload the system config cache - will try again later. Cause: " + t);
        } finally {
            // reschedule ourself to trigger in another 60 seconds
            try {
                timerService.createTimer(60000L, TIMER_DATA);
            } catch (Throwable t) {
                log
                    .error("Failed to reschedule system config cache reload timer! System config cache reload handling will not work from this point. A server restart may be needed after issue is resolved:"
                        + t);
            }
        }
    }

    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public DatabaseType getDatabaseType() {
        Connection conn = null;
        DatabaseType dbtype = null;
        try {
            conn = dataSource.getConnection();
            dbtype = DatabaseTypeFactory.getDatabaseType(conn);
            return dbtype;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    log.warn("Failed to close temporary connection", e);
                }
            }
        }
    }

    @RequiredPermission(Permission.MANAGE_SETTINGS)
    @Deprecated
    public Properties getSystemConfiguration(Subject subject) {
        Properties copy = new Properties();

        SystemSettings settings = getSystemSettings(subject);
        for (Map.Entry<SystemSetting, String> e : settings.entrySet()) {
            //transform the value back to the database format, because that's
            //what this method always returned
            String value = transformSystemConfigurationProperty(e.getKey(), e.getValue(), false);

            copy.put(e.getKey().getInternalName(), value);
        }

        return copy;
    }

    @RequiredPermission(Permission.MANAGE_SETTINGS)
    public SystemSettings getSystemSettings(Subject subject) {
        if (cachedSystemSettings == null) {
            loadSystemConfigurationCache();
        }

        return new SystemSettings(cachedSystemSettings);
    }

    @RequiredPermission(Permission.MANAGE_SETTINGS)
    public void setSystemSettings(Subject subject, SystemSettings settings) {
        setSystemSettings(settings, false);
    }

    private void setSystemSettings(SystemSettings settings, boolean skipValidation) {
        // first, we need to get the current settings so we'll know if we need to persist or merge the new ones
        @SuppressWarnings("unchecked")
        List<SystemConfiguration> configs = entityManager.createNamedQuery(SystemConfiguration.QUERY_FIND_ALL)
            .getResultList();

        Map<String, SystemConfiguration> existingConfigMap = new HashMap<String, SystemConfiguration>();

        for (SystemConfiguration config : configs) {
            existingConfigMap.put(config.getPropertyKey(), config);
        }

        // verify each new setting and persist them to the database
        // note that if a new setting is the same as the old one, we do nothing - leave the old entity as is        
        for (Map.Entry<SystemSetting, String> e : settings.entrySet()) {
            SystemSetting prop = e.getKey();
            String value = e.getValue();

            if (skipValidation == false) {
                verifyNewSystemConfigurationProperty(prop, value, settings);
            }

            SystemConfiguration existingConfig = existingConfigMap.get(prop.getInternalName());
            if (existingConfig == null) {
                value = transformSystemConfigurationProperty(prop, value, false);
                existingConfig = new SystemConfiguration(prop.getInternalName(), value);
                entityManager.persist(existingConfig);
            } else {
                //make sure we compare the new value with a database-agnostic value
                //it is important to compare in the database-agnostic format instead
                //of database specific because the conversion isn't reflective.   
                //Some legacy code somewhere is (or just used to) store booleans as "0"s and "1"s
                //even though they are stored as strings and thus don't suffer from Oracle's
                //lack of support for boolean data type. If we encounter such values, we convert
                //them to "false"/"true" and store that value to database. This is a one way operation
                //and therefore we need to compare the database-agnostic (i.e. "true"/"false") and
                //not database specific.
                String existingValue = transformSystemConfigurationProperty(prop, existingConfig.getPropertyValue(),
                    true);

                if ((existingValue == null && value != null) || !existingValue.equals(value)) {
                    //SystemSetting#isReadOnly should be a superset of the "fReadOnly" field in the database
                    //but let's just be super paranoid here...
                    if (prop.isReadOnly()
                        || (existingConfig.getFreadOnly() != null && existingConfig.getFreadOnly().booleanValue())) {
                        throw new IllegalArgumentException("The setting [" + prop.getInternalName()
                            + "] is read-only - you cannot change its current value! Current value is ["
                            + existingConfig.getPropertyValue() + "] while the new value was [" + value + "].");
                    }

                    //transform to the database-specific format
                    value = transformSystemConfigurationProperty(prop, value, false);

                    existingConfig.setPropertyValue(value);
                    entityManager.merge(existingConfig);
                }
            }
        }

        //let the cache be reloaded once it's needed.
        //we can't assume that the caller provided the full set of properties we
        //have in the database, so let's just make sure we reinit the cache
        //from there...
        cachedSystemSettings = null;
    }

    private Map<String, String> toMap(Properties props) {
        HashMap<String, String> map = new HashMap<String, String>(props.size());
        for (Map.Entry<Object, Object> entry : props.entrySet()) {
            map.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue()));
        }
        return map;
    }

    private void transformToSystemSettingsFormat(Map<String, String> map) {
        for (Map.Entry<String, String> e : map.entrySet()) {
            SystemSetting prop = SystemSetting.getByInternalName(e.getKey());
            if (prop != null) {
                //this is a legacy method that supplies values in the DB-specific format.
                //we therefore have to transform the values as if they came from the database.
                String value = transformSystemConfigurationProperty(prop, e.getValue(), true);
                e.setValue(value);
            }
        }
    }

    private Map<String, String> getDriftServerPlugins() {
        DriftServerPluginManager pluginMgr = getDriftServerPluginManager();
        Map<String, String> plugins = new HashMap<String, String>();

        for (ServerPluginEnvironment env : pluginMgr.getPluginEnvironments()) {
            plugins.put(env.getPluginKey().getPluginName(), env.getPluginDescriptor().getDisplayName());
        }

        return plugins;
    }

    private DriftServerPluginManager getDriftServerPluginManager() {
        MasterServerPluginContainer masterPC = LookupUtil.getServerPluginService().getMasterPluginContainer();
        if (masterPC == null) {
            log.warn(MasterServerPluginContainer.class.getSimpleName() + " is not started yet");
            return null;
        }

        DriftServerPluginContainer pc = masterPC.getPluginContainerByClass(DriftServerPluginContainer.class);
        if (pc == null) {
            log.warn(DriftServerPluginContainer.class + " has not been loaded by the " + masterPC.getClass() + " yet");
            return null;
        }

        return (DriftServerPluginManager) pc.getPluginManager();
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void loadSystemConfigurationCacheInNewTx() {
        // this is used by our timer, so any exceptions coming out here doesn't throw away our timer
        loadSystemConfigurationCache();
    }

    @SuppressWarnings("unchecked")
    public void loadSystemConfigurationCache() {
        // After this is done, the cachedSystemSettings contains the latest config.
        List<SystemConfiguration> configs = entityManager.createNamedQuery(SystemConfiguration.QUERY_FIND_ALL)
            .getResultList();

        SystemSettings settings = new SystemSettings();

        for (SystemConfiguration config : configs) {
            SystemSetting prop = SystemSetting.getByInternalName(config.getPropertyKey());
            if (prop == null) {
                log.warn("The database contains unknown system configuration setting [" + config.getPropertyKey()
                    + "].");
                continue;
            }

            if (config.getPropertyValue() == null) {
                // for some reason, the configuration is not found in the DB, so fallback to the persisted default.
                // if there isn't even a persisted default, just use an empty string.
                String defaultValue = config.getDefaultPropertyValue();
                defaultValue = transformSystemConfigurationProperty(prop, defaultValue, true);
                settings.put(prop, (defaultValue != null) ? defaultValue : "");
            } else {
                String value = config.getPropertyValue();
                value = transformSystemConfigurationProperty(prop, value, true);
                settings.put(prop, value);
            }
        }

        settings.setDriftPlugins(getDriftServerPlugins());

        cachedSystemSettings = settings;
    }

    @RequiredPermission(Permission.MANAGE_SETTINGS)
    @Deprecated
    public void setSystemConfiguration(Subject subject, Properties properties, boolean skipValidation) throws Exception {
        Map<String, String> map = toMap(properties);

        transformToSystemSettingsFormat(map);

        SystemSettings settings = SystemSettings.fromMap(map);

        setSystemSettings(settings, skipValidation);
    }

    @RequiredPermission(Permission.MANAGE_SETTINGS)
    public void validateSystemConfiguration(Subject subject, Properties properties)
        throws InvalidSystemConfigurationException {
        Map<String, String> map = toMap(properties);

        transformToSystemSettingsFormat(map);

        SystemSettings settings = SystemSettings.fromMap(map);
        for (Map.Entry<SystemSetting, String> e : settings.entrySet()) {
            SystemSetting prop = e.getKey();
            String value = e.getValue();

            verifyNewSystemConfigurationProperty(prop, value, settings);
        }
    }

    /**
     * Call this to transform a system setting to a more appropriate value.
     */
    private String transformSystemConfigurationProperty(SystemSetting prop, String value, boolean fromDb) {
        if (fromDb) {
            // to support Oracle (whose booleans may be 1 or 0) transform the boolean settings properly
            switch (prop) {
            case LDAP_BASED_JAAS_PROVIDER:
                if (RHQConstants.JDBCJAASProvider.equals(value)) {
                    return Boolean.toString(false);
                } else if (RHQConstants.LDAPJAASProvider.equals(value)) {
                    return Boolean.toString(true);
                }
                break;
            case USE_SSL_FOR_LDAP:
                if (RHQConstants.LDAP_PROTOCOL_SECURED.equals(value)) {
                    return Boolean.toString(true);
                } else {
                    return Boolean.toString(false);
                }
            default:
                if (prop.getType() == PropertySimpleType.BOOLEAN) {
                    if ("0".equals(value)) {
                        return Boolean.FALSE.toString();
                    } else if ("1".equals(value)) {
                        return Boolean.TRUE.toString();
                    }
                }
            }
        } else {
            //toDB
            //the 0,1 -> true false scenario is no problem here, because the values are stored
            //as string anyway (so no conversion is done). I assume the above is a historical
            //code that could be safely eliminated.
            switch (prop) {
            case LDAP_BASED_JAAS_PROVIDER:
                if (Boolean.parseBoolean(value)) {
                    return RHQConstants.LDAPJAASProvider;
                } else {
                    return RHQConstants.JDBCJAASProvider;
                }
            case USE_SSL_FOR_LDAP:
                if (Boolean.parseBoolean(value)) {
                    return RHQConstants.LDAP_PROTOCOL_SECURED;
                } else {
                    return RHQConstants.LDAP_PROTOCOL_UNSECURED;
                }
            }
        }

        return value;
    }

    /**
     * This will make sure the given configuration has a valid value. An exception will be thrown if the given system
     * configuration value is invalid. This method returns if everything is OK.
     *
     * @param property   name for the property
     * @param value      value of the property
     * @param settings   the full set of system configurations, in case the changed value needs to be compared against
     *                   other values
     */
    private void verifyNewSystemConfigurationProperty(SystemSetting property, String value, SystemSettings settings) {
        if (property == SystemSetting.BASE_LINE_DATASET) {
            // 1h table holds at most 14 days worth of data, make sure we don't set a dataset more than that
            long baselineDataSet = Long.parseLong(value);
            if (baselineDataSet > (1000L * 60 * 60 * 24 * 14)) {
                throw new InvalidSystemConfigurationException("Baseline dataset must be less than 14 days");
            }
        } else if (property == SystemSetting.BASE_LINE_FREQUENCY) {
            long baselineFrequency = Long.parseLong(value);
            long baselineDataSet = Long.parseLong(settings.get(SystemSetting.BASE_LINE_DATASET));
            if (baselineFrequency > baselineDataSet) {
                throw new InvalidSystemConfigurationException(
                    "baseline computation frequency must not be larger than baseline data set");
            }
        } else if (property == SystemSetting.AGENT_MAX_QUIET_TIME_ALLOWED) {
            long time = Long.parseLong(value);
            if (time < 1000L * 60 * 2) {
                throw new InvalidSystemConfigurationException("Agent Max Quiet Time Allowed must be at least 2 minutes");
            }
        }

        return;
    }

    public void enableHibernateStatistics() {
        PersistenceUtility.enableHibernateStatistics(this.entityManager, ManagementFactory.getPlatformMBeanServer());
    }

    @RequiredPermission(Permission.MANAGE_SETTINGS)
    public void reconfigureSystem(Subject whoami) {
        try {
            Object mbean;

            mbean = MBeanServerInvocationHandler.newProxyInstance(MBeanServerLocator.locateJBoss(),
                CustomJaasDeploymentServiceMBean.OBJECT_NAME, CustomJaasDeploymentServiceMBean.class, false);
            ((CustomJaasDeploymentServiceMBean) mbean).installJaasModules();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @RequiredPermission(Permission.MANAGE_SETTINGS)
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public long analyze(Subject whoami) {
        Connection conn = null;
        DatabaseType dbtype = null;
        try {
            conn = dataSource.getConnection();
            dbtype = DatabaseTypeFactory.getDatabaseType(conn);
            if (!DatabaseTypeFactory.isPostgres(dbtype)) {
                return -1;
            }

            long duration = doCommand(dbtype, conn, SQL_ANALYZE, null);
            return duration;
        } catch (Exception e) {
            log.error("Error analyzing database", e);
            throw new RuntimeException("Error analyzing database", e);
        } finally {
            if (dbtype != null) {
                dbtype.closeConnection(conn);
            }
        }
    }

    @RequiredPermission(Permission.MANAGE_SETTINGS)
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public long reindex(Subject whoami) {
        Connection conn = null;
        DatabaseType dbtype = null;
        try {
            conn = dataSource.getConnection();
            dbtype = DatabaseTypeFactory.getDatabaseType(conn);
            long duration = 0;
            if (DatabaseTypeFactory.isPostgres(dbtype)) {
                for (String table : TABLES_TO_REINDEX) {
                    duration += doCommand(dbtype, conn, SQL_REINDEX, table);
                }
            } else if (DatabaseTypeFactory.isOracle(dbtype)) {
                for (String index : ORA_INDEXES_TO_REBUILD) {
                    duration += doCommand(dbtype, conn, SQL_REBUILD, index);
                }
            } else {
                return -1;
            }

            return duration;
        } catch (Exception e) {
            log.error("Error reindexing database", e);
            throw new RuntimeException("Error reindexing database", e);
        } finally {
            if (dbtype != null) {
                dbtype.closeConnection(conn);
            }
        }
    }

    @RequiredPermission(Permission.MANAGE_SETTINGS)
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public long vacuum(Subject whoami) {
        return vacuum(whoami, null);
    }

    @RequiredPermission(Permission.MANAGE_SETTINGS)
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public long vacuum(Subject whoami, String[] tableNames) {
        long duration = 0;
        Connection conn = null;
        DatabaseType dbtype = null;
        try {
            conn = dataSource.getConnection();
            dbtype = DatabaseTypeFactory.getDatabaseType(conn);
            if (!DatabaseTypeFactory.isPostgres(dbtype)) {
                return -1;
            }

            if (tableNames == null) // no names given -> operate on all tables.
            {
                tableNames = new String[1];
                tableNames[0] = null;
            }

            for (String tableName : tableNames) {
                duration += doCommand(dbtype, conn, SQL_VACUUM, tableName);
            }

            return duration;
        } catch (Exception e) {
            log.error("Error vacuuming database: " + e.getMessage(), e);
            return duration;
        } finally {
            if (dbtype != null) {
                dbtype.closeConnection(conn);
            }
        }
    }

    @RequiredPermission(Permission.MANAGE_SETTINGS)
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public long vacuumAppdef(Subject whoami) {
        return vacuum(whoami, TABLES_TO_VACUUM);
    }

    private long doCommand(DatabaseType dbtype, Connection conn, String command, String table) {
        Statement stmt = null;
        StopWatch watch = new StopWatch();

        if (table == null) {
            table = "";
        }

        command = command.replace("{0}", table);

        if (log.isDebugEnabled()) {
            log.debug("Execute command: " + command);
        }

        try {
            stmt = conn.createStatement();
            stmt.execute(command);
            return watch.getElapsed();
        } catch (SQLException e) {
            log.error("Error in command: " + command + ": " + e, e);
            return watch.getElapsed();
        } finally {
            dbtype.closeStatement(stmt);
        }
    }

    /**
     * Ensures the installer is no longer deployed.
     */
    public void undeployInstaller() {
        try {
            File serverHomeDir = LookupUtil.getCoreServer().getJBossServerHomeDir();

            File deployDirectory = new File(serverHomeDir, "deploy");

            if (deployDirectory.exists()) {
                File deployedInstallWar = new File(deployDirectory.getAbsolutePath(), "rhq-installer.war");
                File undeployedInstallWar = new File(deployDirectory.getAbsolutePath(), "rhq-installer.war.rej");

                if (deployedInstallWar.exists()) {
                    // we need to undeploy it first - on windows the files are locked and can't be renamed until undeployed
                    ObjectName name = ObjectNameFactory.create("jboss.system:service=MainDeployer");
                    MBeanServerConnection mbs = MBeanServerLocator.locateJBoss();
                    MainDeployerMBean mbean = (MainDeployerMBean) MBeanServerInvocationHandler.newProxyInstance(mbs,
                        name, MainDeployerMBean.class, false);
                    URL url = deployedInstallWar.toURI().toURL();
                    String urlString = url.toString().replace("%20", " "); // bug in undeployer doesn't like %20 - it wants a real space
                    ((MainDeployerMBean) mbean).undeploy(urlString);
                    if (((MainDeployerMBean) mbean).isDeployed(urlString) == false) {
                        log.info("Installer war has been hot-undeployed from memory");
                    } else {
                        log.warn("Installer hot-undeploy failed - full installer undeploy may not work...");
                    }

                    if (!deployedInstallWar.renameTo(undeployedInstallWar)) {
                        throw new RuntimeException("Cannot undeploy the installer war: " + deployedInstallWar);
                    }

                    // I don't trust it - make sure we removed it
                    if (deployedInstallWar.exists()) {
                        throw new RuntimeException("Failed to undeploy the installer war: " + deployedInstallWar);
                    }

                    // now that the installer is removed, put something in its place to avoid
                    // getting tomcat errors and to at least point the user back to the GUI
                    File deployedPostInstallWar = new File(deployDirectory.getAbsolutePath(), "rhq-postinstaller.war");
                    File undeployedPostInstallWar = new File(deployDirectory.getAbsolutePath(),
                        "rhq-postinstaller.war.rej");
                    if (!deployedPostInstallWar.exists()) {
                        if (undeployedPostInstallWar.exists()) {
                            if (undeployedPostInstallWar.renameTo(deployedPostInstallWar)) {
                                log.debug("Post-install notification war has been deployed");
                            } else {
                                log.info("Post-install notification war failed to deploy - this can be ignored");
                            }
                        } else {
                            log.info("Post-install notification war not found and not deployed - this can be ignored");
                        }
                    } else {
                        log.info("Post-install notification war already deployed");
                    }
                } else if (undeployedInstallWar.exists()) {
                    log.debug("Installer looks to be undeployed already, this is good: " + undeployedInstallWar);
                } else {
                    log.debug("Installer can't be found - assume it has been completely purged: " + deployedInstallWar);
                }
            } else {
                throw new RuntimeException("Your deployment seems corrupted - missing deploy dir: " + deployDirectory);
            }
        } catch (Exception e) {
            log.warn("Please manually remove installer war to secure your deployment: " + e);
            return;
        }

        // we only get here if we are SURE we removed it!
        log.info("Confirmed that the installer has been undeployed");
        return;
    }

    public boolean isDebugModeEnabled() {
        try {
            Subject su = this.subjectManager.getOverlord();
            String setting = getSystemSettings(su).get(SystemSetting.DEBUG_MODE_ENABLED);
            if (setting == null) {
                setting = "false";
            }
            return Boolean.valueOf(setting);
        } catch (Throwable t) {
            return false; // paranoid catch-all
        }
    }

    public boolean isExperimentalFeaturesEnabled() {
        try {
            Subject su = this.subjectManager.getOverlord();
            String setting = getSystemSettings(su).get(SystemSetting.EXPERIMENTAL_FEATURES_ENABLED);
            if (setting == null) {
                setting = "false";
            }
            return Boolean.valueOf(setting);
        } catch (Throwable t) {
            return false; // paranoid catch-all
        }
    }

    public boolean isLdapAuthorizationEnabled() {
        Subject su = this.subjectManager.getOverlord();
        SystemSettings settings = getSystemSettings(su);

        String ldapAuthValue = settings.get(SystemSetting.LDAP_BASED_JAAS_PROVIDER);

        boolean ldapAuth = ldapAuthValue == null ? false : Boolean.valueOf(ldapAuthValue);

        String groupFilter = settings.get(SystemSetting.LDAP_GROUP_FILTER);
        String groupMember = settings.get(SystemSetting.LDAP_GROUP_MEMBER);

        return ldapAuth
            && (((groupFilter != null) && groupFilter.trim().length() > 0) || ((groupMember != null) && groupMember
                .trim().length() > 0));
    }

    @RequiredPermission(Permission.MANAGE_SETTINGS)
    public ServerDetails getServerDetails(Subject subject) {
        CoreServerMBean coreServerMBean = LookupUtil.getCoreServer();

        ServerDetails serverDetails = new ServerDetails();

        serverDetails.setProductInfo(getProductInfo(subject));

        HashMap<Detail, String> details = serverDetails.getDetails();

        DateFormat localTimeFormatter = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.FULL);
        details.put(ServerDetails.Detail.SERVER_LOCAL_TIME, localTimeFormatter.format(new Date()));
        details.put(ServerDetails.Detail.SERVER_TIMEZONE, TimeZone.getDefault().getDisplayName());
        details.put(ServerDetails.Detail.SERVER_HOME_DIR, coreServerMBean.getJBossServerHomeDir().getAbsolutePath());
        details.put(ServerDetails.Detail.SERVER_INSTALL_DIR, coreServerMBean.getInstallDir().getAbsolutePath());

        SystemDatabaseInformation dbInfo = SystemDatabaseInformation.getInstance();
        details.put(ServerDetails.Detail.DATABASE_CONNECTION_URL, dbInfo.getDatabaseConnectionURL());
        details.put(ServerDetails.Detail.DATABASE_DRIVER_NAME, dbInfo.getDatabaseDriverName());
        details.put(ServerDetails.Detail.DATABASE_DRIVER_VERSION, dbInfo.getDatabaseDriverVersion());
        details.put(ServerDetails.Detail.DATABASE_PRODUCT_NAME, dbInfo.getDatabaseProductName());
        details.put(ServerDetails.Detail.DATABASE_PRODUCT_VERSION, dbInfo.getDatabaseProductVersion());
        details.put(ServerDetails.Detail.CURRENT_MEASUREMENT_TABLE, dbInfo.getCurrentMeasurementTable());
        details.put(ServerDetails.Detail.NEXT_MEASUREMENT_TABLE_ROTATION, dbInfo.getNextMeasurementTableRotation());

        return serverDetails;
    }

    public ProductInfo getProductInfo(Subject subject) {
        CoreServerMBean coreServer = LookupUtil.getCoreServer();
        ProductInfo productInfo = coreServer.getProductInfo();
        return productInfo;
    }
}