/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.java.sip.communicator.service.protocol;

import java.lang.reflect.*;
import java.util.*;

import net.java.sip.communicator.service.credentialsstorage.*;
import net.java.sip.communicator.util.*;

import org.jitsi.service.configuration.*;
import org.osgi.framework.*;

/**
 * The ProtocolProviderFactory is what actually creates instances of a
 * ProtocolProviderService implementation. A provider factory  would register,
 * persistently store, and remove when necessary, ProtocolProviders. The way
 * things are in the SIP Communicator, a user account is represented (in a 1:1
 * relationship) by  an AccountID and a ProtocolProvider. In other words - one
 * would have as many protocol providers installed in a given moment as they
 * would user account registered through the various services.
 *
 * @author Emil Ivov
 * @author Lubomir Marinov
 */
public abstract class ProtocolProviderFactory
{
    /**
     * The <tt>Logger</tt> used by the <tt>ProtocolProviderFactory</tt> class
     * and its instances for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(ProtocolProviderFactory.class);

    /**
     * Then name of a property which represents a password.
     */
    public static final String PASSWORD = "PASSWORD";

    /**
     * The name of a property representing the name of the protocol for an
     * ProtocolProviderFactory.
     */
    public static final String PROTOCOL = "PROTOCOL_NAME";

    /**
     * The name of a property representing the path to protocol icons.
     */
    public static final String PROTOCOL_ICON_PATH = "PROTOCOL_ICON_PATH";

    /**
     * The name of a property representing the path to the account icon to
     * be used in the user interface, when the protocol provider service is not
     * available.
     */
    public static final String ACCOUNT_ICON_PATH = "ACCOUNT_ICON_PATH";

    /**
     * The name of a property which represents the AccountID of a
     * ProtocolProvider and that, together with a password is used to login
     * on the protocol network..
     */
    public static final String USER_ID = "USER_ID";

    /**
     * The name that should be displayed to others when we are calling or
     * writing them.
     */
    public static final String DISPLAY_NAME = "DISPLAY_NAME";

    /**
     * The name that should be displayed to the user on call via and chat via
     * lists.
     */
    public static final String ACCOUNT_DISPLAY_NAME = "ACCOUNT_DISPLAY_NAME";

    /**
     * The name of the property under which we store protocol AccountID-s.
     */
    public static final String ACCOUNT_UID = "ACCOUNT_UID";

    /**
     * The name of the property under which we store protocol the address of
     * a protocol centric entity (any protocol server).
     */
    public static final String SERVER_ADDRESS = "SERVER_ADDRESS";

    /**
     * The name of the property under which we store the number of the port
     * where the server stored against the SERVER_ADDRESS property is expecting
     * connections to be made via this protocol.
     */
    public static final String SERVER_PORT = "SERVER_PORT";

    /**
     * The name of the property under which we store the name of the transport
     * protocol that needs to be used to access the server.
     */
    public static final String SERVER_TRANSPORT = "SERVER_TRANSPORT";

    /**
     * The name of the property under which we store protocol the address of
     * a protocol proxy.
     */
    public static final String PROXY_ADDRESS = "PROXY_ADDRESS";

    /**
     * The name of the property under which we store the number of the port
     * where the proxy stored against the PROXY_ADDRESS property is expecting
     * connections to be made via this protocol.
     */
    public static final String PROXY_PORT = "PROXY_PORT";

    /**
     * The name of the property which defines whether proxy is auto configured
     * by the protocol by using known methods such as specific DNS queries.
     */
    public static final String PROXY_AUTO_CONFIG = "PROXY_AUTO_CONFIG";

    /**
     * The property indicating the preferred UDP and TCP
     * port to bind to for clear communications.
     */
    public static final String PREFERRED_CLEAR_PORT_PROPERTY_NAME
        = "net.java.sip.communicator.SIP_PREFERRED_CLEAR_PORT";

    /**
     * The property indicating the preferred TLS (TCP)
     * port to bind to for secure communications.
     */
    public static final String PREFERRED_SECURE_PORT_PROPERTY_NAME
        = "net.java.sip.communicator.SIP_PREFERRED_SECURE_PORT";

    /**
     * The name of the property under which we store the the type of the proxy
     * stored against the PROXY_ADDRESS property. Exact type values depend on
     * protocols and among them are socks4, socks5, http and possibly others.
     */
    public static final String PROXY_TYPE = "PROXY_TYPE";

    /**
    * The name of the property under which we store the the username for the
    * proxy stored against the PROXY_ADDRESS property.
    */
   public static final String PROXY_USERNAME = "PROXY_USERNAME";

   /**
    * The name of the property under which we store the the authorization name
    * for the proxy stored against the PROXY_ADDRESS property.
    */
   public static final String AUTHORIZATION_NAME = "AUTHORIZATION_NAME";

   /**
    * The name of the property under which we store the password for the proxy
    * stored against the PROXY_ADDRESS property.
    */
   public static final String PROXY_PASSWORD = "PROXY_PASSWORD";

    /**
     * The name of the property under which we store the name of the transport
     * protocol that needs to be used to access the proxy.
     */
    public static final String PROXY_TRANSPORT = "PROXY_TRANSPORT";

    /**
     * The name of the property that indicates whether loose routing should be
     * forced for all traffic in an account, rather than routing through an
     * outbound proxy which is the default for Jitsi.
     */
    public static final String FORCE_PROXY_BYPASS = "FORCE_PROXY_BYPASS";

    /**
     * The name of the property that indicates whether the client must
     * be registered with a registrar when making outgoing calls.
     */
    public static final String MUST_REGISTER_TO_CALL = "MUST_REGISTER_TO_CALL";

    /**
     * The name of the property under which we store the user preference for a
     * transport protocol to use (i.e. tcp or udp).
     */
    public static final String PREFERRED_TRANSPORT = "PREFERRED_TRANSPORT";

    /**
     * The name of the property under which we store whether we generate
     * resource values or we just use the stored one.
     */
    public static final String AUTO_GENERATE_RESOURCE = "AUTO_GENERATE_RESOURCE";

    /**
     * The name of the property under which we store resources such as the
     * jabber resource property.
     */
    public static final String RESOURCE = "RESOURCE";

    /**
     * The name of the property under which we store resource priority.
     */
    public static final String RESOURCE_PRIORITY = "RESOURCE_PRIORITY";

    /**
     * The name of the property which defines that the call is encrypted by
     * default
     */
    public static final String DEFAULT_ENCRYPTION = "DEFAULT_ENCRYPTION";

    /**
     * The name of the property that indicates the encryption protocols for this
     * account.
     */
    public static final String ENCRYPTION_PROTOCOL = "ENCRYPTION_PROTOCOL";

    /**
     * The name of the property that indicates the status (enabed or disabled)
     * encryption protocols for this account.
     */
    public static final String ENCRYPTION_PROTOCOL_STATUS
        = "ENCRYPTION_PROTOCOL_STATUS";

    /**
     * The name of the property which defines if to include the ZRTP attribute
     * to SIP/SDP
     */
    public static final String DEFAULT_SIPZRTP_ATTRIBUTE =
        "DEFAULT_SIPZRTP_ATTRIBUTE";

    /**
     * The name of the property which defines the ID of the client TLS
     * certificate configuration entry.
     */
    public static final String CLIENT_TLS_CERTIFICATE =
        "CLIENT_TLS_CERTIFICATE";

    /**
     * The name of the property under which we store the boolean value
     * indicating if the user name should be automatically changed if the
     * specified name already exists. This property is meant to be used by IRC
     * implementations.
     */
    public static final String AUTO_CHANGE_USER_NAME = "AUTO_CHANGE_USER_NAME";

    /**
     * The name of the property under which we store the boolean value
     * indicating if a password is required. Initially this property is meant to
     * be used by IRC implementations.
     */
    public static final String NO_PASSWORD_REQUIRED = "NO_PASSWORD_REQUIRED";

    /**
     * The name of the property under which we store if the presence is enabled.
     */
    public static final String IS_PRESENCE_ENABLED = "IS_PRESENCE_ENABLED";

    /**
     * The name of the property under which we store if the p2p mode for SIMPLE
     * should be forced.
     */
    public static final String FORCE_P2P_MODE = "FORCE_P2P_MODE";

    /**
     * The name of the property under which we store the offline contact polling
     * period for SIMPLE.
     */
    public static final String POLLING_PERIOD = "POLLING_PERIOD";

    /**
     * The name of the property under which we store the chosen default
     * subscription expiration value for SIMPLE.
     */
    public static final String SUBSCRIPTION_EXPIRATION
                                                = "SUBSCRIPTION_EXPIRATION";

    /**
     * Indicates if the server address has been validated.
     */
    public static final String SERVER_ADDRESS_VALIDATED
                                                = "SERVER_ADDRESS_VALIDATED";

    /**
     * Indicates if the server settings are over
     */
    public static final String IS_SERVER_OVERRIDDEN
                                                = "IS_SERVER_OVERRIDDEN";
    /**
     * Indicates if the proxy address has been validated.
     */
    public static final String PROXY_ADDRESS_VALIDATED
                                                = "PROXY_ADDRESS_VALIDATED";

    /**
     * Indicates the search strategy chosen for the DICT protocole.
     */
    public static final String STRATEGY = "STRATEGY";

    /**
     * Indicates a protocol that would not be shown in the user interface as an
     * account.
     */
    public static final String IS_PROTOCOL_HIDDEN = "IS_PROTOCOL_HIDDEN";

    /**
     * Indicates if the given account is the preferred account.
     */
    public static final String IS_PREFERRED_PROTOCOL = "IS_PREFERRED_PROTOCOL";

    /**
     * The name of the property that would indicate if a given account is
     * currently enabled or disabled.
     */
    public static final String IS_ACCOUNT_DISABLED = "IS_ACCOUNT_DISABLED";

    /**
     * The name of the property that would indicate if a given account
     * configuration form is currently hidden.
     */
    public static final String IS_ACCOUNT_CONFIG_HIDDEN = "IS_CONFIG_HIDDEN";

    /**
     * The name of the property that would indicate if a given account
     * status menu is currently hidden.
     */
    public static final String IS_ACCOUNT_STATUS_MENU_HIDDEN =
        "IS_STATUS_MENU_HIDDEN";

    /**
     * The name of the property that would indicate if a given account
     * configuration is read only.
     */
    public static final String IS_ACCOUNT_READ_ONLY = "IS_READ_ONLY";

    /**
     * The name of the property that would indicate if a given account
     * groups are readonly, values can be all or a comma separated
     * group names including root.
     */
    public static final String ACCOUNT_READ_ONLY_GROUPS = "READ_ONLY_GROUPS";

    /**
     * Indicates if ICE should be used.
     */
    public static final String IS_USE_ICE = "ICE_ENABLED";

    /**
     * Indicates if STUN server should be automatically discovered.
     */
    public static final String AUTO_DISCOVER_STUN = "AUTO_DISCOVER_STUN";

    /**
     * Indicates if default STUN server would be used if no other STUN/TURN
     * server are available.
     */
    public static final String USE_DEFAULT_STUN_SERVER
        = "USE_DEFAULT_STUN_SERVER";

    /**
     * The name of the boolean account property which indicates whether Jitsi
     * Videobridge is to be used, if available and supported, for conference
     * calls.
     */
    public static final String USE_JITSI_VIDEO_BRIDGE
        = "USE_JITSI_VIDEO_BRIDGE";

    /**
     * The name of the boolean account property which indicates whether Jitsi
     * will use translator for media, instead of mixing, for conference
     * calls.
     * By default if supported mixing is used (audio mixed, video relayed).
     */
    public static final String USE_TRANSLATOR_IN_CONFERENCE
        = "USE_TRANSLATOR_IN_CONFERENCE";

    /**
     * The property name prefix for all stun server properties. We generally use
     * this prefix in conjunction with an index which is how we store multiple
     * servers.
     */
    public static final String STUN_PREFIX = "STUN";

    /**
     * The base property name for address of additional STUN servers specified.
     */
    public static final String STUN_ADDRESS = "ADDRESS";

    /**
     * The base property name for port of additional STUN servers specified.
     */
    public static final String STUN_PORT = "PORT";

    /**
     * The base property name for username of additional STUN servers specified.
     */
    public static final String STUN_USERNAME = "USERNAME";

    /**
     * The base property name for password of additional STUN servers specified.
     */
    public static final String STUN_PASSWORD = "PASSWORD";

    /**
     * The base property name for the turn supported property of additional
     * STUN servers specified.
     */
    public static final String STUN_IS_TURN_SUPPORTED = "IS_TURN_SUPPORTED";

    /**
     * Indicates if JingleNodes should be used with ICE.
     */
    public static final String IS_USE_JINGLE_NODES = "JINGLE_NODES_ENABLED";

    /**
     * Indicates if JingleNodes should be used with ICE.
     */
    public static final String AUTO_DISCOVER_JINGLE_NODES
        = "AUTO_DISCOVER_JINGLE_NODES";

    /**
     * Indicates if JingleNodes should use buddies to search for nodes.
     */
    public static final String JINGLE_NODES_SEARCH_BUDDIES
        = "JINGLE_NODES_SEARCH_BUDDIES";

    /**
     * Indicates if UPnP should be used with ICE.
     */
    public static final String IS_USE_UPNP = "UPNP_ENABLED";

    /**
     * Indicates if we allow non-TLS connection.
     */
    public static final String IS_ALLOW_NON_SECURE = "ALLOW_NON_SECURE";

    /**
     * Enable notifications for new voicemail messages.
     */
    public static final String VOICEMAIL_ENABLED = "VOICEMAIL_ENABLED";

    /**
     * Address used to reach voicemail box, by services able to
     * subscribe for voicemail new messages notifications.
     */
    public static final String VOICEMAIL_URI = "VOICEMAIL_URI";

    /**
     * Address used to call to hear your messages stored on the server
     * for your voicemail.
     */
    public static final String VOICEMAIL_CHECK_URI = "VOICEMAIL_CHECK_URI";

    /**
     * Indicates if calling is disabled for a certain account.
     */
    public static final String IS_CALLING_DISABLED_FOR_ACCOUNT
        = "CALLING_DISABLED";

    /**
     * Indicates if desktop streaming/sharing is disabled for a certain account.
     */
    public static final String IS_DESKTOP_STREAMING_DISABLED
        = "DESKTOP_STREAMING_DISABLED";

    /**
     * Indicates if desktop remote control is disabled for a certain account.
     */
    public static final String IS_DESKTOP_REMOTE_CONTROL_DISABLED
        = "DESKTOP_REMOTE_CONTROL_DISABLED";

    /**
     * The sms default server address.
     */
    public static final String SMS_SERVER_ADDRESS = "SMS_SERVER_ADDRESS";

    /**
     * Keep-alive method used by the protocol.
     */
    public static final String KEEP_ALIVE_METHOD = "KEEP_ALIVE_METHOD";

    /**
     * The interval for keep-alives if any.
     */
    public static final String KEEP_ALIVE_INTERVAL = "KEEP_ALIVE_INTERVAL";

    /**
     * The name of the property holding DTMF method.
     */
    public static final String DTMF_METHOD = "DTMF_METHOD";

    /**
     * The minimal DTMF tone duration.
     */
    public static final String DTMF_MINIMAL_TONE_DURATION
        = "DTMF_MINIMAL_TONE_DURATION";

    /**
     * Paranoia mode when turned on requires all calls to be secure and
     * indicated as such.
     */
    public static final String MODE_PARANOIA = "MODE_PARANOIA";

    /**
     * The name of the "override encodings" property
     */
    public static final String OVERRIDE_ENCODINGS = "OVERRIDE_ENCODINGS";

    /**
     * The prefix used to store account encoding properties
     */
    public static final String ENCODING_PROP_PREFIX = "Encodings";

    /**
     * An account property to provide a connected account to check for
     * its status. Used when the current provider need to reject calls
     * but is missing presence operation set and need to check other
     * provider for status.
     */
    public static final String CUSAX_PROVIDER_ACCOUNT_PROP
        = "cusax.XMPP_ACCOUNT_ID";

    /**
     * The <code>BundleContext</code> containing (or to contain) the service
     * registration of this factory.
     */
    private final BundleContext bundleContext;

    /**
     * The name of the protocol this factory registers its
     * <code>ProtocolProviderService</code>s with and to be placed in the
     * properties of the accounts created by this factory.
     */
    private final String protocolName;

    /**
     * The table that we store our accounts in.
     * <p>
     * TODO Synchronize the access to the field which may in turn be better
     * achieved by also hiding it from protected into private access.
     * </p>
     */
    protected final Map<AccountID, ServiceRegistration<ProtocolProviderService>>
        registeredAccounts
            = new HashMap<AccountID, ServiceRegistration<ProtocolProviderService>>();

    /**
     * The name of the property that indicates the AVP type.
     * <ul>
     * <li>{@link #SAVP_OFF}</li>
     * <li>{@link #SAVP_MANDATORY}</li>
     * <li>{@link #SAVP_OPTIONAL}</li>
     * </ul>
     */
    public static final String SAVP_OPTION = "SAVP_OPTION";

    /**
     * Always use RTP/AVP
     */
    public static final int SAVP_OFF = 0;

    /**
     * Always use RTP/SAVP
     */
    public static final int SAVP_MANDATORY = 1;

    /**
     * Sends two media description, with RTP/SAVP being first.
     */
    public static final int SAVP_OPTIONAL = 2;

    /**
     * The name of the property that defines the enabled SDES cipher suites.
     * Enabled suites are listed as CSV by their RFC name.
     */
    public static final String SDES_CIPHER_SUITES = "SDES_CIPHER_SUITES";

    /**
     * The name of the property that defines the enabled/disabled state of
     * message carbons.
     */
    public static final String IS_CARBON_DISABLED = "CARBON_DISABLED";

    /**
     * Creates a new <tt>ProtocolProviderFactory</tt>.
     *
     * @param bundleContext the bundle context reference of the service
     * @param protocolName the name of the protocol
     */
    protected ProtocolProviderFactory(BundleContext bundleContext,
        String protocolName)
    {
        this.bundleContext = bundleContext;
        this.protocolName = protocolName;
    }

    /**
     * Gets the <code>BundleContext</code> containing (or to contain) the
     * service registration of this factory.
     *
     * @return the <code>BundleContext</code> containing (or to contain) the
     *         service registration of this factory
     */
    public BundleContext getBundleContext()
    {
        return bundleContext;
    }

    /**
     * Initializes and creates an account corresponding to the specified
     * accountProperties and registers the resulting ProtocolProvider in the
     * <tt>context</tt> BundleContext parameter. Note that account
     * registration is persistent and accounts that are registered during
     * a particular sip-communicator session would be automatically reloaded
     * during all following sessions until they are removed through the
     * removeAccount method.
     *
     * @param userID the user identifier uniquely representing the newly
     * created account within the protocol namespace.
     * @param accountProperties a set of protocol (or implementation) specific
     * properties defining the new account.
     * @return the AccountID of the newly created account.
     * @throws java.lang.IllegalArgumentException if userID does not correspond
     * to an identifier in the context of the underlying protocol or if
     * accountProperties does not contain a complete set of account installation
     * properties.
     * @throws java.lang.IllegalStateException if the account has already been
     * installed.
     * @throws java.lang.NullPointerException if any of the arguments is null.
     */
    public abstract AccountID installAccount(String userID,
            Map<String, String> accountProperties)
        throws IllegalArgumentException,
               IllegalStateException,
               NullPointerException;


    /**
     * Modifies the account corresponding to the specified accountID. This
     * method is meant to be used to change properties of already existing
     * accounts. Note that if the given accountID doesn't correspond to any
     * registered account this method would do nothing.
     *
     * @param protocolProvider the protocol provider service corresponding to
     * the modified account.
     * @param accountProperties a set of protocol (or implementation) specific
     * properties defining the new account.
     *
     * @throws java.lang.NullPointerException if any of the arguments is null.
     */
    public abstract void modifyAccount(
                                ProtocolProviderService protocolProvider,
                                Map<String, String> accountProperties)
            throws NullPointerException;

    /**
     * Returns a copy of the list containing the <tt>AccountID</tt>s of all
     * accounts currently registered in this protocol provider.
     * @return a copy of the list containing the <tt>AccountID</tt>s of all
     * accounts currently registered in this protocol provider.
     */
    public ArrayList<AccountID> getRegisteredAccounts()
    {
        synchronized (registeredAccounts)
        {
            return new ArrayList<AccountID>(registeredAccounts.keySet());
        }
    }

    /**
     * Returns the ServiceReference for the protocol provider corresponding to
     * the specified accountID or null if the accountID is unknown.
     * @param accountID the accountID of the protocol provider we'd like to get
     * @return a ServiceReference object to the protocol provider with the
     * specified account id and null if the account id is unknown to the
     * provider factory.
     */
    public ServiceReference<ProtocolProviderService> getProviderForAccount(
            AccountID accountID)
    {
        ServiceRegistration<ProtocolProviderService> registration;

        synchronized (registeredAccounts)
        {
            registration = registeredAccounts.get(accountID);
        }

        try
        {
            if (registration != null)
                return registration.getReference();
        }
        catch (IllegalStateException ise)
        {
            synchronized (registeredAccounts)
            {
                registeredAccounts.remove(accountID);
            }
        }

        return null;
    }

    /**
     * Removes the specified account from the list of accounts that this
     * provider factory is handling. If the specified accountID is unknown to
     * the ProtocolProviderFactory, the call has no effect and false is
     * returned. This method is persistent in nature and once called the account
     * corresponding to the specified ID will not be loaded during future runs
     * of the project.
     *
     * @param accountID the ID of the account to remove.
     * @return true if an account with the specified ID existed and was removed
     * and false otherwise.
     */
    public boolean uninstallAccount(AccountID accountID)
    {
        // Unregister the protocol provider.
        ServiceReference<ProtocolProviderService> serRef
            = getProviderForAccount(accountID);

        boolean wasAccountExisting = false;

        // If the protocol provider service is registered, first unregister the
        // service.
        if (serRef != null)
        {
            BundleContext bundleContext = getBundleContext();
            ProtocolProviderService protocolProvider
                = bundleContext.getService(serRef);

            try
            {
                protocolProvider.unregister();
            }
            catch (OperationFailedException ex)
            {
                logger.error(
                        "Failed to unregister protocol provider for account: "
                            + accountID + " caused by: " + ex);
            }
        }

        ServiceRegistration<ProtocolProviderService> registration;

        synchronized (registeredAccounts)
        {
            registration = registeredAccounts.remove(accountID);
        }

        // first remove the stored account so when PP is unregistered we can
        // distinguish between deleted or just disabled account
        wasAccountExisting = removeStoredAccount(accountID);

        if (registration != null)
        {
            // Kill the service.
            registration.unregister();
        }

        return wasAccountExisting;
    }

    /**
     * The method stores the specified account in the configuration service
     * under the package name of the source factory. The restore and remove
     * account methods are to be used to obtain access to and control the stored
     * accounts.
     * <p>
     * In order to store all account properties, the method would create an
     * entry in the configuration service corresponding (beginning with) the
     * <tt>sourceFactory</tt>'s package name and add to it a unique identifier
     * (e.g. the current miliseconds.)
     * </p>
     *
     * @param accountID the AccountID corresponding to the account that we would
     * like to store.
     */
    protected void storeAccount(AccountID accountID)
    {
        this.storeAccount(accountID, true);
    }

    /**
     * The method stores the specified account in the configuration service
     * under the package name of the source factory. The restore and remove
     * account methods are to be used to obtain access to and control the stored
     * accounts.
     * <p>
     * In order to store all account properties, the method would create an
     * entry in the configuration service corresponding (beginning with) the
     * <tt>sourceFactory</tt>'s package name and add to it a unique identifier
     * (e.g. the current miliseconds.)
     * </p>
     *
     * @param accountID the AccountID corresponding to the account that we would
     * like to store.
     * @param  isModification if <tt>false</tt> there must be no such already
     * loaded account, it <tt>true</tt> ist modification of an existing account.
     * Usually we use this method with <tt>false</tt> in method installAccount
     * and with <tt>true</tt> or the overridden method in method
     * modifyAccount.
     */
    protected void storeAccount(AccountID accountID, boolean isModification)
    {
        if(!isModification
            && getAccountManager().getStoredAccounts().contains(accountID))
        {
            throw new IllegalStateException(
                "An account for id " + accountID.getUserID()
                    + " was already loaded!");
        }

        try
        {
            getAccountManager().storeAccount(this, accountID);
        }
        catch (OperationFailedException ofex)
        {
            throw new UndeclaredThrowableException(ofex);
        }
    }

    /**
     * Saves the password for the specified account after scrambling it a bit so
     * that it is not visible from first sight. (The method remains highly
     * insecure).
     *
     * @param accountID the AccountID for the account whose password we're
     *            storing
     * @param password the password itself
     *
     * @throws IllegalArgumentException if no account corresponding to
     *             <code>accountID</code> has been previously stored
     */
    public void storePassword(AccountID accountID, String password)
        throws IllegalArgumentException
    {
        try
        {
            storePassword(getBundleContext(), accountID, password);
        }
        catch (OperationFailedException ofex)
        {
            throw new UndeclaredThrowableException(ofex);
        }
    }

    /**
     * Saves the password for the specified account after scrambling it a bit
     * so that it is not visible from first sight (Method remains highly
     * insecure).
     * <p>
     * TODO Delegate the implementation to {@link AccountManager} because it
     * knows the format in which the password (among the other account
     * properties) is to be saved.
     * </p>
     *
     * @param bundleContext a currently valid bundle context.
     * @param accountID the <tt>AccountID</tt> of the account whose password is
     * to be stored
     * @param password the password to be stored
     *
     * @throws IllegalArgumentException if no account corresponding to
     * <tt>accountID</tt> has been previously stored.
     * @throws OperationFailedException if anything goes wrong while storing the
     * specified <tt>password</tt>
     */
    protected void storePassword(BundleContext bundleContext,
                                 AccountID    accountID,
                                 String       password)
        throws IllegalArgumentException,
               OperationFailedException
    {
        String accountPrefix
            = findAccountPrefix(
                bundleContext,
                accountID,
                getFactoryImplPackageName());

        if (accountPrefix == null)
        {
            throw
                new IllegalArgumentException(
                        "No previous records found for account ID: "
                            + accountID.getAccountUniqueID()
                            + " in package"
                            + getFactoryImplPackageName());
        }

        CredentialsStorageService credentialsStorage
            = ServiceUtils.getService(
                    bundleContext,
                    CredentialsStorageService.class);

        if (!credentialsStorage.storePassword(accountPrefix, password))
        {
            throw
                new OperationFailedException(
                        "CredentialsStorageService failed to storePassword",
                        OperationFailedException.GENERAL_ERROR);
        }

        // Update password property also in the AccountID
        // to prevent it from being removed during account reload
        // in some cases.
        accountID.setPassword(password);

    }

    /**
     * Returns the password last saved for the specified account.
     *
     * @param accountID the AccountID for the account whose password we're
     *            looking for
     *
     * @return a String containing the password for the specified accountID
     */
    public String loadPassword(AccountID accountID)
    {
        return loadPassword(getBundleContext(), accountID);
    }

    /**
     * Returns the password last saved for the specified account.
     * <p>
     * TODO Delegate the implementation to {@link AccountManager} because it
     * knows the format in which the password (among the other account
     * properties) was saved.
     * </p>
     *
     * @param bundleContext a currently valid bundle context.
     * @param accountID the AccountID for the account whose password we're
     *            looking for..
     *
     * @return a String containing the password for the specified accountID.
     */
    protected String loadPassword(BundleContext bundleContext,
                                  AccountID     accountID)
    {
        String accountPrefix = findAccountPrefix(
            bundleContext, accountID, getFactoryImplPackageName());

        if (accountPrefix == null)
            return null;

        CredentialsStorageService credentialsStorage
            = ServiceUtils.getService(
                    bundleContext,
                    CredentialsStorageService.class);

        return credentialsStorage.loadPassword(accountPrefix);
    }

    /**
     * Initializes and creates an account corresponding to the specified
     * accountProperties and registers the resulting ProtocolProvider in the
     * <tt>context</tt> BundleContext parameter. This method has a persistent
     * effect. Once created the resulting account will remain installed until
     * removed through the uninstallAccount method.
     *
     * @param accountProperties a set of protocol (or implementation) specific
     *            properties defining the new account.
     * @return the AccountID of the newly loaded account
     */
    public AccountID loadAccount(Map<String, String> accountProperties)
    {
        AccountID accountID = createAccount(accountProperties);

        loadAccount(accountID);

        return accountID;
    }

    /**
     * Creates a protocol provider for the given <tt>accountID</tt> and
     * registers it in the bundle context. This method has a persistent
     * effect. Once created the resulting account will remain installed until
     * removed through the uninstallAccount method.
     *
     * @param accountID the account identifier
     * @return <tt>true</tt> if the account with the given <tt>accountID</tt> is
     * successfully loaded, otherwise returns <tt>false</tt>
     */
    public boolean loadAccount(AccountID accountID)
    {
        // Need to obtain the original user id property, instead of calling
        // accountID.getUserID(), because this method could return a modified
        // version of the user id property.
        String userID
            = accountID.getAccountPropertyString(
                    ProtocolProviderFactory.USER_ID);

        ProtocolProviderService service = createService(userID, accountID);

        Dictionary<String, String> properties = new Hashtable<String, String>();
        properties.put(PROTOCOL, protocolName);
        properties.put(USER_ID, userID);

        ServiceRegistration<ProtocolProviderService> serviceRegistration
            = bundleContext.registerService(
                    ProtocolProviderService.class,
                    service,
                    properties);

        if (serviceRegistration == null)
        {
            return false;
        }
        else
        {
            synchronized (registeredAccounts)
            {
                registeredAccounts.put(accountID, serviceRegistration);
            }
            return true;
        }
    }

    /**
     * Unloads the account corresponding to the given <tt>accountID</tt>.
     * Unregisters the corresponding protocol provider, but keeps the account in
     * contrast to the uninstallAccount method.
     *
     * @param accountID the account identifier
     * @return true if an account with the specified ID existed and was unloaded
     * and false otherwise.
     */
    public boolean unloadAccount(AccountID accountID)
    {
        // Unregister the protocol provider.
        ServiceReference<ProtocolProviderService> serRef
            = getProviderForAccount(accountID);

        if (serRef == null)
        {
            return false;
        }

        BundleContext bundleContext = getBundleContext();
        ProtocolProviderService protocolProvider
            = bundleContext.getService(serRef);

        try
        {
            protocolProvider.unregister();
        }
        catch (OperationFailedException ex)
        {
            logger.error(
                    "Failed to unregister protocol provider for account: "
                        + accountID + " caused by: " + ex);
        }

        ServiceRegistration<ProtocolProviderService> registration;

        synchronized (registeredAccounts)
        {
            registration = registeredAccounts.remove(accountID);
        }
        if (registration == null)
        {
            return false;
        }

        // Kill the service.
        registration.unregister();

        return true;
    }

    /**
     * Initializes and creates an account corresponding to the specified
     * accountProperties.
     *
     * @param accountProperties a set of protocol (or implementation) specific
     * properties defining the new account.
     * @return the AccountID of the newly created account
     */
    public AccountID createAccount(Map<String, String> accountProperties)
    {
        BundleContext bundleContext = getBundleContext();
        if (bundleContext == null)
            throw new NullPointerException(
                "The specified BundleContext was null");

        if (accountProperties == null)
            throw new NullPointerException(
                "The specified property map was null");

        String userID = accountProperties.get(USER_ID);
        if (userID == null)
            throw new NullPointerException(
                "The account properties contained no user id.");

        String protocolName = getProtocolName();
        if (!accountProperties.containsKey(PROTOCOL))
            accountProperties.put(PROTOCOL, protocolName);

        return createAccountID(userID, accountProperties);
    }

    /**
     * Creates a new <code>AccountID</code> instance with a specific user ID to
     * represent a given set of account properties.
     * <p>
     * The method is a pure factory allowing implementers to specify the runtime
     * type of the created <code>AccountID</code> and customize the instance.
     * The returned <code>AccountID</code> will later be associated with a
     * <code>ProtocolProviderService</code> by the caller (e.g. using
     * {@link #createService(String, AccountID)}).
     * </p>
     *
     * @param userID the user ID of the new instance
     * @param accountProperties the set of properties to be represented by the
     *            new instance
     * @return a new <code>AccountID</code> instance with the specified user ID
     *         representing the given set of account properties
     */
    protected abstract AccountID createAccountID(
        String userID, Map<String, String> accountProperties);

    /**
     * Gets the name of the protocol this factory registers its
     * <code>ProtocolProviderService</code>s with and to be placed in the
     * properties of the accounts created by this factory.
     *
     * @return the name of the protocol this factory registers its
     *         <code>ProtocolProviderService</code>s with and to be placed in
     *         the properties of the accounts created by this factory
     */
    public String getProtocolName()
    {
        return protocolName;
    }

    /**
     * Initializes a new <code>ProtocolProviderService</code> instance with a
     * specific user ID to represent a specific <code>AccountID</code>.
     * <p>
     * The method is a pure factory allowing implementers to specify the runtime
     * type of the created <code>ProtocolProviderService</code> and customize
     * the instance. The caller will later register the returned service with
     * the <code>BundleContext</code> of this factory.
     * </p>
     *
     * @param userID the user ID to initialize the new instance with
     * @param accountID the <code>AccountID</code> to be represented by the new
     *            instance
     * @return a new <code>ProtocolProviderService</code> instance with the
     *         specific user ID representing the specified
     *         <code>AccountID</code>
     */
    protected abstract ProtocolProviderService createService(String userID,
        AccountID accountID);

    /**
     * Removes the account with <tt>accountID</tt> from the set of accounts
     * that are persistently stored inside the configuration service.
     *
     * @param accountID the AccountID of the account to remove.
     *
     * @return true if an account has been removed and false otherwise.
     */
    protected boolean removeStoredAccount(AccountID accountID)
    {
        return getAccountManager().removeStoredAccount(this, accountID);
    }

    /**
     * Returns the prefix for all persistently stored properties of the account
     * with the specified id.
     * @param bundleContext a currently valid bundle context.
     * @param accountID the AccountID of the account whose properties we're
     * looking for.
     * @param sourcePackageName a String containing the package name of the
     * concrete factory class that extends us.
     * @return a String indicating the ConfigurationService property name
     * prefix under which all account properties are stored or null if no
     * account corresponding to the specified id was found.
     */
    public static String findAccountPrefix(BundleContext bundleContext,
                                       AccountID     accountID,
                                       String sourcePackageName)
    {
        ServiceReference<ConfigurationService> confReference
            = bundleContext.getServiceReference(ConfigurationService.class);
        ConfigurationService configurationService
            = bundleContext.getService(confReference);

        //first retrieve all accounts that we've registered
        List<String> storedAccounts =
            configurationService.getPropertyNamesByPrefix(sourcePackageName,
                    true);

        //find an account with the corresponding id.
        for (String accountRootPropertyName : storedAccounts)
        {
            //unregister the account in the configuration service.
            //all the properties must have been registered in the following
            //hierarchy:
            //net.java.sip.communicator.impl.protocol.PROTO_NAME.ACC_ID.PROP_NAME
            String accountUID = configurationService.getString(
                accountRootPropertyName //node id
                + "." + ACCOUNT_UID); // propname

            if (accountID.getAccountUniqueID().equals(accountUID))
            {
                return accountRootPropertyName;
            }
        }
        return null;
    }

    /**
     * Returns the name of the package that we're currently running in (i.e.
     * the name of the package containing the proto factory that extends us).
     *
     * @return a String containing the package name of the concrete factory
     * class that extends us.
     */
    private String getFactoryImplPackageName()
    {
        String className = getClass().getName();

        return className.substring(0, className.lastIndexOf('.'));
    }

    /**
     * Prepares the factory for bundle shutdown.
     */
    public void stop()
    {
        if (logger.isTraceEnabled())
            logger.trace("Preparing to stop all protocol providers of" + this);

        synchronized (registeredAccounts)
        {
            for (ServiceRegistration<ProtocolProviderService> reg
                    : registeredAccounts.values())
            {
                stop(reg);
                reg.unregister();
            }

            registeredAccounts.clear();
        }
    }

    /**
     * Shuts down the <code>ProtocolProviderService</code> representing an
     * account registered with this factory.
     *
     * @param registeredAccount the <code>ServiceRegistration</code> of the
     *            <code>ProtocolProviderService</code> representing an account
     *            registered with this factory
     */
    protected void stop(
            ServiceRegistration<ProtocolProviderService> registeredAccount)
    {
        ProtocolProviderService protocolProviderService
            = getBundleContext().getService(registeredAccount.getReference());

        protocolProviderService.shutdown();
    }

    /**
     * Get the <tt>AccountManager</tt> of the protocol.
     *
     * @return <tt>AccountManager</tt> of the protocol
     */
    private AccountManager getAccountManager()
    {
        BundleContext bundleContext = getBundleContext();
        ServiceReference<AccountManager> serviceReference
            = bundleContext.getServiceReference(AccountManager.class);

        return bundleContext.getService(serviceReference);
    }


    /**
     * Finds registered <tt>ProtocolProviderFactory</tt> for given
     * <tt>protocolName</tt>.
     * @param bundleContext the OSGI bundle context that will be used.
     * @param protocolName the protocol name.
     * @return Registered <tt>ProtocolProviderFactory</tt> for given protocol
     * name or <tt>null</tt> if no provider was found.
     */
    static public ProtocolProviderFactory getProtocolProviderFactory(
            BundleContext bundleContext,
            String protocolName)
    {
        Collection<ServiceReference<ProtocolProviderFactory>> serRefs;
        String osgiFilter
            = "(" + ProtocolProviderFactory.PROTOCOL + "=" + protocolName + ")";

        try
        {
            serRefs
                = bundleContext.getServiceReferences(
                        ProtocolProviderFactory.class,
                        osgiFilter);
        }
        catch (InvalidSyntaxException ex)
        {
            serRefs = null;
            logger.error(ex);
        }
        if ((serRefs == null) || serRefs.isEmpty())
            return null;
        else
            return bundleContext.getService(serRefs.iterator().next());
    }
}
