/*
 * 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.communications;

import java.io.File;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;

import mazz.i18n.Logger;

import org.jboss.remoting.security.SSLSocketBuilder;

import org.rhq.enterprise.communications.command.client.RemoteInputStream;
import org.rhq.enterprise.communications.command.client.RemoteOutputStream;
import org.rhq.enterprise.communications.command.impl.echo.server.EchoCommandService;
import org.rhq.enterprise.communications.command.impl.identify.server.IdentifyCommandService;
import org.rhq.enterprise.communications.command.impl.stream.server.RemoteInputStreamCommandService;
import org.rhq.enterprise.communications.command.impl.stream.server.RemoteOutputStreamCommandService;
import org.rhq.enterprise.communications.command.server.CommandAuthenticator;
import org.rhq.enterprise.communications.i18n.CommI18NFactory;
import org.rhq.enterprise.communications.i18n.CommI18NResourceKeys;
import org.rhq.enterprise.communications.util.ConcurrencyManager;

/**
 * Just provides some convienence methods to extract the service container configuration properties. These are remoting
 * server configuration properties that are common on both the RHQ Server and RHQ Agent used to configure the
 * communications services.
 *
 * @author John Mazzitelli
 */
public class ServiceContainerConfiguration {
    /**
     * Logger
     */
    private static final Logger LOG = CommI18NFactory.getLogger(ServiceContainerConfiguration.class);

    /**
     * The configuration properties this object wraps. This must be either the RHQ Server or RHQ Agent preferences node.
     */
    private final Preferences m_preferences;

    /**
     * Wraps a preferences object in this instance.
     *
     * @param  prefs the service container configuration preferences
     *
     * @throws IllegalArgumentException if <code>prefs</code> is <code>null</code>
     */
    public ServiceContainerConfiguration(Preferences prefs) {
        if (prefs == null) {
            throw new IllegalArgumentException("prefs=null");
        }

        m_preferences = prefs;
    }

    /**
     * Returns the raw preferences containing the service container configuration.
     *
     * @return the configuration preferences
     */
    public Preferences getPreferences() {
        return m_preferences;
    }

    /**
     * Returns the version of the configuration schema.
     *
     * @return configuration version; if the configuration isn't versioned, 0 is returned
     */
    public int getServiceContainerConfigurationVersion() {
        int value = m_preferences.getInt(ServiceContainerConfigurationConstants.CONFIG_SCHEMA_VERSION, 0);

        return value;
    }

    /**
     * This tags the existing preferences by setting the configuration schema version preference appropriately.
     */
    public void tagWithServiceContainerConfigurationVersion() {
        m_preferences.putInt(ServiceContainerConfigurationConstants.CONFIG_SCHEMA_VERSION,
            ServiceContainerConfigurationConstants.CURRENT_CONFIG_SCHEMA_VERSION);
        flush(ServiceContainerConfigurationConstants.CONFIG_SCHEMA_VERSION);
    }

    /**
     * Returns the global concurrency limit on the number of commands that are allowed to be received concurrently (i.e.
     * the counting semaphore name as known to the {@link ConcurrencyManager}).
     *
     * <p>If this returns a value of 0 or less, there will be no global limit on the number of messages that can be
     * received and processed concurrently.</p>
     *
     * @return the global concurrency limit value
     */
    public int getGlobalConcurrencyLimit() {
        int value = m_preferences.getInt(ServiceContainerConfigurationConstants.GLOBAL_CONCURRENCY_LIMIT, 0);
        return value;
    }

    /**
     * Returns the name of the MBeanServer that will be created to house all the services. This is actually the default
     * domain name of the MBeanServer. If an MBeanServer already exists and is registered with this default domain name,
     * it will be used to house the communications services. If the MBeanServer name does not exist in the
     * configuration, <code>null</code> will be returned, which indicates that the
     * {@link ManagementFactory#getPlatformMBeanServer() JVM's platform MBeanServer} should be used.
     *
     * @return the MBeanServer name
     */
    public String getMBeanServerName() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.MBEANSERVER_NAME, null);
        return value;
    }

    /**
     * Returns <code>true</code> if the communication services should be disabled, <code>false</code> if they should be
     * created normally. It is rare that you would want to configure this setting to be <code>true</code>.
     *
     * @return the flag to indicate if communications should be disabled
     */
    public boolean isCommunicationsDisabled() {
        boolean value = m_preferences.getBoolean(ServiceContainerConfigurationConstants.DISABLE_COMMUNICATIONS,
            ServiceContainerConfigurationConstants.DEFAULT_DISABLE_COMMUNICATIONS);
        return value;
    }

    /**
     * Returns the data directory where all internally persisted data can be written to. If the data directory does not
     * exist, it will be created.
     *
     * <p>Because this does alittle extra work, it may not be suitable to call if you just want to get the value of the
     * data directory or if you just want to know if its defined or not. In those instances, use
     * {@link #getDataDirectoryIfDefined()} instead.</p>
     *
     * @return the data directory
     */
    public File getDataDirectory() {
        String dirStr = m_preferences.get(ServiceContainerConfigurationConstants.DATA_DIRECTORY,
            ServiceContainerConfigurationConstants.DEFAULT_DATA_DIRECTORY);
        File dir = new File(dirStr);

        if (!dir.exists()) {
            dir.mkdirs();
        }

        return dir;
    }

    /**
     * This will return the data directory string as found in the preferences. If the data directory is not defined in
     * the preferences, <code>null</code> is returned.
     *
     * @return the data directory string as defined in the preferences or <code>null</code> if it is not defined
     *
     * @see    #getDataDirectory()
     */
    public String getDataDirectoryIfDefined() {
        String dir_str = m_preferences.get(ServiceContainerConfigurationConstants.DATA_DIRECTORY, null);

        return dir_str;
    }

    /**
     * Returns the flag to indicate if the multicast detector should be enabled or not.
     *
     * @return the multicast detector enable flag (<code>true</code> means the detector should be enabled)
     */
    public boolean isMulticastDetectorEnabled() {
        boolean value = m_preferences.getBoolean(ServiceContainerConfigurationConstants.MULTICASTDETECTOR_ENABLED,
            ServiceContainerConfigurationConstants.DEFAULT_MULTICASTDETECTOR_ENABLED);
        return value;
    }

    /**
     * Returns the multicast detector's multicast address used for network registry messages (servers coming and going).
     * This is the IP of the multicast group that the detector will join.
     *
     * @return the multicast detector multicast address (<code>null</code> if not set in the preferences)
     */
    public String getMulticastDetectorMulticastAddress() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.MULTICASTDETECTOR_ADDRESS, null);
        return value;
    }

    /**
     * Returns the multicast detector's bind address which is what the network interface binds to.
     *
     * @return the multicast detector bind address (<code>null</code> if not set in the preferences)
     */
    public String getMulticastDetectorBindAddress() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.MULTICASTDETECTOR_BINDADDRESS, null);
        return value;
    }

    /**
     * Returns the multicast detector's default time delay. This is the amount of time, in milliseconds, which can
     * elapse without receiving a detection event before suspecting that a server is dead and performing an explicit
     * invocation on it to verify it is alive. If this invocation, or ping, fails, the server will be removed from the
     * network registry. This should be greater than {@link #getMulticastDetectorHeartbeatTimeDelay()}.
     *
     * @return the multicast detector default time delay (<code>Long.MIN_VALUE</code> if not set in the preferences)
     */
    public long getMulticastDetectorDefaultTimeDelay() {
        long value = m_preferences.getLong(ServiceContainerConfigurationConstants.MULTICASTDETECTOR_DEFAULT_TIMEDELAY,
            Long.MIN_VALUE);
        return value;
    }

    /**
     * Returns the multicast detector's heartbeat time delay. This is the amount of time to wait between sending (and
     * sometimes receiving) detection messages. This should be less than {@link #getMulticastDetectorDefaultTimeDelay()}
     * .
     *
     * @return the multicast detector heartbeat time delay (<code>Long.MIN_VALUE</code> if not set in the preferences)
     */
    public long getMulticastDetectorHeartbeatTimeDelay() {
        long value = m_preferences.getLong(
            ServiceContainerConfigurationConstants.MULTICASTDETECTOR_HEARTBEAT_TIMEDELAY, Long.MIN_VALUE);
        return value;
    }

    /**
     * Returns the multicast detector's port.
     *
     * @return the multicast detector port (<code>Integer.MIN_VALUE</code> if not set in the preferences)
     */
    public int getMulticastDetectorPort() {
        int value = m_preferences.getInt(ServiceContainerConfigurationConstants.MULTICASTDETECTOR_PORT,
            Integer.MIN_VALUE);
        return value;
    }

    /**
     * Returns the connector's RHQ type (indicating if the remoting server resides in the RHQ Server or RHQ Agent).
     *
     * @return the RHQ type
     */
    public String getConnectorRHQType() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_RHQTYPE,
            ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_RHQTYPE);
        return value;
    }

    /**
     * Returns the connector's transport type (socket, http, etc).
     *
     * @return the connector transport type
     */
    public String getConnectorTransport() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_TRANSPORT,
            ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_TRANSPORT);
        return value;
    }

    /**
     * Returns the connector's bind address.
     *
     * @return the connector bind address
     */
    public String getConnectorBindAddress() {
        String defaultBindAddress;

        try {
            defaultBindAddress = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            defaultBindAddress = "127.0.0.1";
        }

        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_BIND_ADDRESS,
            defaultBindAddress);
        return value;
    }

    /**
     * Returns the connector's bind port.
     *
     * @return the connector's bind port
     */
    public int getConnectorBindPort() {
        int value = m_preferences.getInt(ServiceContainerConfigurationConstants.CONNECTOR_BIND_PORT,
            ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_BIND_PORT);
        return value;
    }

    /**
     * Returns the connector's transport parameters which are used to further customize the connector and its transport
     * subsystem.
     *
     * @return the connector transport parameters
     */
    public String getConnectorTransportParams() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_TRANSPORT_PARAMS,
            ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_TRANSPORT_PARAMS);
        return value;
    }

    /**
     * This is a convienence method that builds a full remote endpoint string that combines
     * {@link #getConnectorTransport()}, {@link #getConnectorBindAddress()}, {@link #getConnectorBindPort()},
     * {@link #getConnectorTransportParams()} and {@link #getConnectorRHQType()}.
     *
     * @return a remote endpoint string that can be used by remote clients to connect to the connector.
     */
    public String getConnectorRemoteEndpoint() {
        String rhqtype = getConnectorRHQType();

        if (!rhqtype.equals(ServiceContainerConfigurationConstants.RHQTYPE_AGENT)
            && !rhqtype.equals(ServiceContainerConfigurationConstants.RHQTYPE_SERVER)) {
            LOG.warn(CommI18NResourceKeys.SERVICE_CONTAINER_INVALID_RHQTYPE, rhqtype,
                ServiceContainerConfigurationConstants.RHQTYPE_SERVER,
                ServiceContainerConfigurationConstants.RHQTYPE_AGENT);

            rhqtype = ServiceContainerConfigurationConstants.RHQTYPE_AGENT;
        }

        String rhqtype_param = ServiceContainerConfigurationConstants.CONNECTOR_RHQTYPE + "=" + rhqtype;
        String transport = getConnectorTransport();
        String bind_address = getConnectorBindAddress();
        int bind_port = getConnectorBindPort();
        String transport_params = getConnectorTransportParams().trim();

        // build the locator URI from the configuration properties
        String locator_uri = transport + "://" + bind_address + ":" + bind_port;

        if (transport_params.length() == 0) {
            transport_params = "/?" + rhqtype_param;
        } else {
            // for some transports (e.g. servlet) the params will actually be the rest of the URL with options query string
            // to denote that we don't want to start the query string immediately after host:port, the params can start with /
            if (transport_params.startsWith("/")) {
                if (transport_params.indexOf('?') == -1) {
                    transport_params += "?" + rhqtype_param;
                } else {
                    transport_params += "&" + rhqtype_param;
                }
            } else {
                transport_params = "/?" + rhqtype_param + "&" + transport_params;
            }
        }

        locator_uri += transport_params;

        return locator_uri;
    }

    /**
     * Returns the connector's lease period, in milliseconds. When this period is greater than 0, it enables the
     * capability for the connector to detect when a client is no longer available. This is done by estabilishing a
     * lease with the remoting clients.
     *
     * @return the connector's lease period (<code>Long.MIN_VALUE</code> if not set in the preferences)
     */
    public long getConnectorLeasePeriod() {
        long value = m_preferences.getLong(ServiceContainerConfigurationConstants.CONNECTOR_LEASE_PERIOD,
            Long.MIN_VALUE);
        return value;
    }

    /**
     * Returns the protocol used over the secure socket.
     *
     * @return protocol name
     */
    public String getConnectorSecuritySocketProtocol() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_SOCKET_PROTOCOL,
            ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_SECURITY_SOCKET_PROTOCOL);
        return value;
    }

    /**
     * Returns the client authentication mode. It is either:}
     *
     * <ul>
     *   <li>{@link SSLSocketBuilder#CLIENT_AUTH_MODE_NONE} - clients are not authenticated</li>
     *   <li>{@link SSLSocketBuilder#CLIENT_AUTH_MODE_WANT} - clients are authenticated if they provide a cert; if they
     *     do not, anonymous connections are allowed</li>
     *   <li>{@link SSLSocketBuilder#CLIENT_AUTH_MODE_NEED} - clients must supply a cert and they must be
     *     authenticated</li>
     * </ul>
     *
     * The default is {@link SSLSocketBuilder#CLIENT_AUTH_MODE_NONE} - that will be returned if the client auth mode was
     * not found in the configuration.
     * 
     * <i>Note:</i> To support Tomcat syntax, the client authentication mode value is allowed to be "true" or "false".
     * "true" maps to {@link SSLSocketBuilder#CLIENT_AUTH_MODE_NEED},
     * "false" maps to {@link SSLSocketBuilder#CLIENT_AUTH_MODE_NONE}.
     *
     * @return the client authentication mode
     */
    public String getConnectorSecurityClientAuthMode() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_CLIENT_AUTH_MODE,
            ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_SECURITY_CLIENT_AUTH_MODE);

        if (value.equalsIgnoreCase("true")) {
            value = SSLSocketBuilder.CLIENT_AUTH_MODE_NEED;
        } else if (value.equalsIgnoreCase("false")) {
            value = SSLSocketBuilder.CLIENT_AUTH_MODE_NONE;
        }

        if (!value.equals(SSLSocketBuilder.CLIENT_AUTH_MODE_NONE)
            && !value.equals(SSLSocketBuilder.CLIENT_AUTH_MODE_WANT)
            && !value.equals(SSLSocketBuilder.CLIENT_AUTH_MODE_NEED)) {
            LOG.warn(CommI18NResourceKeys.SERVICE_CONTAINER_CONFIGURATION_INVALID_CLIENT_AUTH,
                ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_CLIENT_AUTH_MODE, value,
                SSLSocketBuilder.CLIENT_AUTH_MODE_NONE, SSLSocketBuilder.CLIENT_AUTH_MODE_WANT,
                SSLSocketBuilder.CLIENT_AUTH_MODE_NEED, SSLSocketBuilder.CLIENT_AUTH_MODE_NEED);

            value = SSLSocketBuilder.CLIENT_AUTH_MODE_NEED;
        }

        return value;
    }

    /**
     * Returns the path to the keystore file. This returns a <code>String</code> as opposed to <code>File</code> since
     * some underlying remoting code may allow for this filepath to be relative to a jar inside the classloader.
     *
     * @return keystore file path
     */
    public String getConnectorSecurityKeystoreFile() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_KEYSTORE_FILE, null);

        if (value == null) {
            value = new File(getDataDirectory(),
                ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_SECURITY_KEYSTORE_FILE_NAME).getAbsolutePath();
        }

        return value;
    }

    /**
     * Returns the algorithm used to manage the keys in the keystore.
     *
     * @return key manager algorithm
     */
    public String getConnectorSecurityKeystoreAlgorithm() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_KEYSTORE_ALGORITHM,
            ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_SECURITY_KEYSTORE_ALGORITHM);
        return value;
    }

    /**
     * Returns the type of keystore.
     *
     * @return keystore type
     */
    public String getConnectorSecurityKeystoreType() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_KEYSTORE_TYPE,
            ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_SECURITY_KEYSTORE_TYPE);
        return value;
    }

    /**
     * Returns the password to the keystore file itself.
     *
     * @return keystore password
     */
    public String getConnectorSecurityKeystorePassword() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_KEYSTORE_PASSWORD,
            "rhqpwd");
        return value;
    }

    /**
     * Returns the password to gain access to the key in the keystore. If no key password is configured, this returns
     * the {@link #getConnectorSecurityKeystorePassword() keystore password}.
     *
     * @return password to the key
     */
    public String getConnectorSecurityKeystoreKeyPassword() {
        String value = m_preferences.get(
            ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_KEYSTORE_KEY_PASSWORD, null);
        if (value == null) {
            value = getConnectorSecurityKeystorePassword();
        }

        return value;
    }

    /**
     * Returns the key alias of the key in the keystore.
     *
     * @return key alias name
     */
    public String getConnectorSecurityKeystoreAlias() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_KEYSTORE_ALIAS,
            ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_SECURITY_KEYSTORE_ALIAS);
        return value;
    }

    /**
     * Returns the path to the truststore file. This returns a <code>String</code> as opposed to <code>File</code> since
     * some underlying remoting code may allow for this filepath to be relative to a jar inside the classloader.
     *
     * @return truststore file path
     */
    public String getConnectorSecurityTruststoreFile() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_TRUSTSTORE_FILE,
            null);

        if (value == null) {
            value = new File(getDataDirectory(),
                ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_SECURITY_TRUSTSTORE_FILE_NAME)
                .getAbsolutePath();
        }

        return value;
    }

    /**
     * Returns the algorithm used to manage the keys in the truststore.
     *
     * @return algorithm name
     */
    public String getConnectorSecurityTruststoreAlgorithm() {
        String value = m_preferences.get(
            ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_TRUSTSTORE_ALGORITHM,
            ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_SECURITY_TRUSTSTORE_ALGORITHM);
        return value;
    }

    /**
     * Returns the type of the truststore file.
     *
     * @return truststore file type
     */
    public String getConnectorSecurityTruststoreType() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_TRUSTSTORE_TYPE,
            ServiceContainerConfigurationConstants.DEFAULT_CONNECTOR_SECURITY_TRUSTSTORE_TYPE);
        return value;
    }

    /**
     * Returns the password of the truststore file itself.
     *
     * @return truststore file password
     */
    public String getConnectorSecurityTruststorePassword() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CONNECTOR_SECURITY_TRUSTSTORE_PASSWORD,
            null);
        return value;
    }

    /**
     * Returns the command services that are to be automatically deployed when the services are started up.
     *
     * @return the set of command services to be automatically deployed at startup
     */
    public String getStartupCommandServices() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.CMDSERVICES, EchoCommandService.class
            .getName()
            + "," + IdentifyCommandService.class.getName());
        return value;
    }

    /**
     * Returns the definitions of the POJOs that are to be automatically deployed as remote services at start up.
     *
     * @return the set of POJOs to remote at startup
     */
    public String getStartupRemotePojos() {
        String value = m_preferences.get(ServiceContainerConfigurationConstants.REMOTE_POJOS, PingImpl.class.getName()
            + ":" + Ping.class.getName());
        return value;
    }

    /**
     * Returns the maximum amount of milliseconds a remote {@link RemoteOutputStream output} or
     * {@link RemoteInputStream input} stream server-side component hosted in a {@link RemoteOutputStreamCommandService}
     * or {@link RemoteInputStreamCommandService} is allowed to be idle before it is automatically closed and removed.
     * This means that a client must attempt to access the remote stream every X milliseconds (where X is the value this
     * method returns) or that stream will no longer be available. Note that this does not mean a client must read/write
     * the entire stream in this amount of time, it only means a client must make a request on the stream every X
     * milliseconds (be it to read or write a byte, mark it, see how many bytes are available, etc).
     *
     * @return the max idle time in milliseconds
     */
    public long getRemoteStreamMaxIdleTime() {
        long value = m_preferences.getLong(ServiceContainerConfigurationConstants.REMOTE_STREAM_MAX_IDLE_TIME,
            ServiceContainerConfigurationConstants.DEFAULT_REMOTE_STREAM_MAX_IDLE_TIME);

        return value;
    }

    /**
     * Returns the flag to indicate if the command service directory has been enabled to perform dynamic discovery of
     * new command services that are added to (or old services removed from) the system after the initial startup
     * discovery.
     *
     * @return <code>true</code> if dynamic discovery of command services is enabled; <code>false</code> if the initial
     *         set of {@link #getStartupCommandServices() } are the only ones that will be enabled during the lifetime
     *         of the service container
     */
    public boolean isCommandServiceDirectoryDynamicDiscoveryEnabled() {
        boolean value = m_preferences.getBoolean(
            ServiceContainerConfigurationConstants.CMDSERVICE_DIRECTORY_DYNAMIC_DISCOVERY,
            ServiceContainerConfigurationConstants.DEFAULT_CMDSERVICE_DIRECTORY_DYNAMIC_DISCOVERY);
        return value;
    }

    /**
     * Returns an implementation of the configured {@link CommandAuthenticator} that should be used to authenticate
     * incoming commands.
     *
     * @return command authenticator object instance or <code>null</code> if no authenticator is configured
     *
     * @throws Exception if failed to instantiate the configured class name
     */
    public CommandAuthenticator getCommandAuthenticator() throws Exception {
        CommandAuthenticator obj = null;
        String value = m_preferences.get(ServiceContainerConfigurationConstants.COMMAND_AUTHENTICATOR, "");

        if ((value != null) && (value.length() > 0)) {
            obj = (CommandAuthenticator) Class.forName(value).newInstance();
        }

        return obj;
    }

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        StringBuffer buf = new StringBuffer(m_preferences.absolutePath());

        buf.append('[');

        try {
            String[] keys = m_preferences.keys();

            for (int i = 0; i < keys.length; i++) {
                String key = keys[i];

                buf.append(key);
                buf.append('=');
                buf.append(m_preferences.get(key, LOG
                    .getMsgString(CommI18NResourceKeys.SERVICE_CONTAINER_CONFIGURATION_UNKNOWN)));

                if ((i + 1) < keys.length) {
                    buf.append(',');
                }
            }
        } catch (BackingStoreException e) {
            buf.append('<');
            buf.append(LOG.getMsgString(CommI18NResourceKeys.SERVICE_CONTAINER_CONFIGURATION_CANNOT_GET_PREFS, e));
            buf.append('>');
        }

        buf.append(']');

        return buf.toString();
    }

    /**
     * Forces the preferences to flush so they get written to the backing store.
     * 
     * @param changedPreference the name of the preference that was changed to
     *                          cause flush to be called (used for error log message)
     */
    private void flush(String changedPreference) {
        try {
            m_preferences.flush();
        } catch (Exception e) {
            LOG.warn(LOG.getMsgString(CommI18NResourceKeys.CANNOT_STORE_PREFERENCES), changedPreference, e);
        }
    }
}