/**
 *
 * Copyright 2003-2007 Jive Software.
 *
 * All rights reserved. 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 org.jivesoftware.smack;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Constructor;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.security.Provider;
import java.security.Security;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;

import org.jamppa.client.plugin.Plugin;
import org.jivesoftware.smack.compression.XMPPInputOutputStream;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.parsing.ParsingExceptionCallback;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.dns.HostAddress;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError;
import org.xmpp.packet.PacketError.Condition;
import org.xmpp.packet.PacketError.Type;
import org.xmpp.packet.Presence;

/**
 * Creates a socket connection to a XMPP server. This is the default connection
 * to a Jabber server and is specified in the XMPP Core (RFC 3920).
 *
 * @see Connection
 * @author Matt Tucker
 */
public class XMPPConnection extends Connection {

    /**
     * The socket which is used for this connection.
     */
    Socket socket;

    String connectionID = null;
    private String user = null;
    private boolean connected = false;
    // socketClosed is used concurrent
    // by XMPPConnection, PacketReader, PacketWriter
    private volatile boolean socketClosed = false;

    /**
     * Flag that indicates if the user is currently authenticated with the
     * server.
     */
    private boolean authenticated = false;
    /**
     * Flag that indicates if the user was authenticated with the server when
     * the connection to the server was closed (abruptly or not).
     */
    private boolean wasAuthenticated = false;
    private boolean anonymous = false;
    private boolean usingTLS = false;

    private ParsingExceptionCallback parsingExceptionCallback = SmackConfiguration
            .getDefaultParsingExceptionCallback();

    PacketWriter packetWriter;
    PacketReader packetReader;

    UserRoster roster = null;

    /**
     * Collection of available stream compression methods offered by the server.
     */
    private Collection<String> compressionMethods;

    /**
     * Set to true by packet writer if the server acknowledged the compression
     */
    private boolean serverAckdCompression = false;

    private Set<Plugin> plugins = new HashSet<Plugin>();

    /**
     * Creates a new connection to the specified XMPP server. A DNS SRV lookup
     * will be performed to determine the IP address and port corresponding to
     * the service name; if that lookup fails, it's assumed that server resides
     * at <tt>serviceName</tt> with the default port of 5222. Encrypted
     * connections (TLS) will be used if available, stream compression is
     * disabled, and standard SASL mechanisms will be used for authentication.
     *

     *

     * This is the simplest constructor for connecting to an XMPP server.
     * Alternatively, you can get fine-grained control over connection settings
     * using the {@link #XMPPConnection(ConnectionConfiguration)} constructor.
     *

     *

     * Note that XMPPConnection constructors do not establish a connection to
     * the server and you must call {@link #connect()}.
     *

     *

     * The CallbackHandler will only be used if the connection requires the
     * client provide an SSL certificate to the server. The CallbackHandler must
     * handle the PasswordCallback to prompt for a password to unlock the
     * keystore containing the SSL certificate.
     *
     * @param serviceName
     *            the name of the XMPP server to connect to; e.g.
     *            <tt>example.com</tt>.
     * @param callbackHandler
     *            the CallbackHandler used to prompt for the password to the
     *            keystore.
     */
    public XMPPConnection(String serviceName, CallbackHandler callbackHandler) {
        // Create the configuration for this new connection
        super(new ConnectionConfiguration(serviceName));
        config.setCompressionEnabled(false);
        config.setSASLAuthenticationEnabled(true);
        config.setDebuggerEnabled(DEBUG_ENABLED);
        config.setCallbackHandler(callbackHandler);
    }

    /**
     * Creates a new XMPP connection in the same way
     * {@link #XMPPConnection(String,CallbackHandler)} does, but with no
     * callback handler for password prompting of the keystore. This will work
     * in most cases, provided the client is not required to provide a
     * certificate to the server.
     *
     * @param serviceName
     *            the name of the XMPP server to connect to; e.g.
     *            <tt>example.com</tt>.
     */
    public XMPPConnection(String serviceName) {
        // Create the configuration for this new connection
        super(new ConnectionConfiguration(serviceName));
        config.setCompressionEnabled(false);
        config.setSASLAuthenticationEnabled(true);
        config.setDebuggerEnabled(DEBUG_ENABLED);
    }

    /**
     * Creates a new XMPP connection in the same way
     * {@link #XMPPConnection(ConnectionConfiguration,CallbackHandler)} does,
     * but with no callback handler for password prompting of the keystore. This
     * will work in most cases, provided the client is not required to provide a
     * certificate to the server.
     *
     *
     * @param config
     *            the connection configuration.
     */
    public XMPPConnection(ConnectionConfiguration config) {
        super(config);
    }

    /**
     * Creates a new XMPP connection using the specified connection
     * configuration.
     *

     *

     * Manually specifying connection configuration information is suitable for
     * advanced users of the API. In many cases, using the
     * {@link #XMPPConnection(String)} constructor is a better approach.
     *

     *

     * Note that XMPPConnection constructors do not establish a connection to
     * the server and you must call {@link #connect()}.
     *

     *

     *
     * The CallbackHandler will only be used if the connection requires the
     * client provide an SSL certificate to the server. The CallbackHandler must
     * handle the PasswordCallback to prompt for a password to unlock the
     * keystore containing the SSL certificate.
     *
     * @param config
     *            the connection configuration.
     * @param callbackHandler
     *            the CallbackHandler used to prompt for the password to the
     *            keystore.
     */
    public XMPPConnection(ConnectionConfiguration config,
            CallbackHandler callbackHandler) {
        super(config);
        config.setCallbackHandler(callbackHandler);
    }

    public String getConnectionID() {
        if (!isConnected()) {
            return null;
        }
        return connectionID;
    }

    public String getUser() {
        if (!isAuthenticated()) {
            return null;
        }
        return user;
    }

    /**
     * Install a parsing exception callback, which will be invoked once an
     * exception is encountered while parsing a stanza
     *
     * @param callback
     *            the callback to install
     */
    public void setParsingExceptionCallback(ParsingExceptionCallback callback) {
        parsingExceptionCallback = callback;
    }

    /**
     * Get the current active parsing exception callback.
     *
     * @return the active exception callback or null if there is none
     */
    public ParsingExceptionCallback getParsingExceptionCallback() {
        return parsingExceptionCallback;
    }

    @Override
    public synchronized void login(String username, String password,
            String resource) throws XMPPException {
        if (!isConnected()) {
            throw new IllegalStateException("Not connected to server.");
        }
        if (authenticated) {
            throw new IllegalStateException("Already logged in to server.");
        }
        // Do partial version of nameprep on the username.
        username = username.toLowerCase().trim();

        String response;
        if (config.isSASLAuthenticationEnabled()
                && saslAuthentication.hasNonAnonymousAuthentication()) {
            // Authenticate using SASL
            if (password != null) {
                response = saslAuthentication.authenticate(username, password,
                        resource);
            } else {
                response = saslAuthentication.authenticate(username, resource,
                        config.getCallbackHandler());
            }
        } else {
            // Authenticate using Non-SASL
            response = new NonSASLAuthentication(this).authenticate(username,
                    password, resource);
        }

        // Set the user.
        if (response != null) {
            this.user = response;
            // Update the serviceName with the one returned by the server
            config.setServiceName(StringUtils.parseServer(response));
        } else {
            this.user = username + "@" + getServiceName();
            if (resource != null) {
                this.user += "/" + resource;
            }
        }

        // If compression is enabled then request the server to use stream
        // compression
        if (config.isCompressionEnabled()) {
            useCompression();
        }

        // Indicate that we're now authenticated.
        authenticated = true;
        anonymous = false;

        // Create the roster if it is not a reconnection or roster already
        // created by getRoster()
        if (this.roster == null) {
            this.roster = new UserRoster(this);
        }
        if (config.isRosterLoadedAtLogin()) {
            this.roster.reload();
        }

        // Set presence to online.
        if (config.isSendPresence()) {
            packetWriter.sendPacket(new Presence());
        }

        // Stores the authentication for future reconnection
        config.setLoginInfo(username, password, resource);

        // If debugging is enabled, change the the debug window title to include
        // the
        // name we are now logged-in as.
        // If DEBUG_ENABLED was set to true AFTER the connection was created the
        // debugger
        // will be null
        if (config.isDebuggerEnabled() && debugger != null) {
            debugger.userHasLogged(user);
        }
    }

    @Override
    public synchronized void loginAnonymously() throws XMPPException {
        if (!isConnected()) {
            throw new IllegalStateException("Not connected to server.");
        }
        if (authenticated) {
            throw new IllegalStateException("Already logged in to server.");
        }

        String response;
        if (config.isSASLAuthenticationEnabled()
                && saslAuthentication.hasAnonymousAuthentication()) {
            response = saslAuthentication.authenticateAnonymously();
        } else {
            // Authenticate using Non-SASL
            response = new NonSASLAuthentication(this)
                    .authenticateAnonymously();
        }

        // Set the user value.
        this.user = response;
        // Update the serviceName with the one returned by the server
        config.setServiceName(StringUtils.parseServer(response));

        // If compression is enabled then request the server to use stream
        // compression
        if (config.isCompressionEnabled()) {
            useCompression();
        }

        // Set presence to online.
        packetWriter.sendPacket(new Presence());

        // Indicate that we're now authenticated.
        authenticated = true;
        anonymous = true;

        // If debugging is enabled, change the the debug window title to include
        // the
        // name we are now logged-in as.
        // If DEBUG_ENABLED was set to true AFTER the connection was created the
        // debugger
        // will be null
        if (config.isDebuggerEnabled() && debugger != null) {
            debugger.userHasLogged(user);
        }
    }

    public UserRoster getRoster() {
        // synchronize against login()
        synchronized (this) {
            // if connection is authenticated the roster is already set by
            // login()
            // or a previous call to getRoster()
            if (!isAuthenticated() || isAnonymous()) {
                if (roster == null) {
                    roster = new UserRoster(this);
                }
                return roster;
            }
        }

        if (!config.isRosterLoadedAtLogin()) {
            roster.reload();
        }
        // If this is the first time the user has asked for the roster after
        // calling
        // login, we want to wait for the server to send back the user's roster.
        // This
        // behavior shields API users from having to worry about the fact that
        // roster
        // operations are asynchronous, although they'll still have to listen
        // for
        // changes to the roster. Note: because of this waiting logic, internal
        // Smack code should be wary about calling the getRoster method, and may
        // need to
        // access the roster object directly.
        if (!roster.rosterInitialized) {
            try {
                synchronized (roster) {
                    long waitTime = SmackConfiguration.getPacketReplyTimeout();
                    long start = System.currentTimeMillis();
                    while (!roster.rosterInitialized) {
                        if (waitTime <= 0) {
                            break;
                        }
                        roster.wait(waitTime);
                        long now = System.currentTimeMillis();
                        waitTime -= now - start;
                        start = now;
                    }
                }
            } catch (InterruptedException ie) {
                // Ignore.
            }
        }
        return roster;
    }

    public boolean isConnected() {
        return connected;
    }

    public boolean isSecureConnection() {
        return isUsingTLS();
    }

    public boolean isSocketClosed() {
        return socketClosed;
    }

    public boolean isAuthenticated() {
        return authenticated;
    }

    public boolean isAnonymous() {
        return anonymous;
    }

    /**
     * Closes the connection by setting presence to unavailable then closing the
     * stream to the XMPP server. The shutdown logic will be used during a
     * planned disconnection or when dealing with an unexpected disconnection.
     * Unlike {@link #disconnect()} the connection's packet reader, packet
     * writer, and {@link UserRoster} will not be removed; thus connection's
     * state is kept.
     *
     * @param unavailablePresence
     *            the presence packet to send during shutdown.
     */
    protected void shutdown(Presence unavailablePresence) {
        // Set presence to offline.
        if (packetWriter != null) {
            packetWriter.sendPacket(unavailablePresence);
        }

        this.setWasAuthenticated(authenticated);
        authenticated = false;

        if (packetReader != null) {
            packetReader.shutdown();
        }
        if (packetWriter != null) {
            packetWriter.shutdown();
        }

        // Wait 150 ms for processes to clean-up, then shutdown.
        try {
            Thread.sleep(150);
        } catch (Exception e) {
            // Ignore.
        }

        // Set socketClosed to true. This will cause the PacketReader
        // and PacketWriter to ignore any Exceptions that are thrown
        // because of a read/write from/to a closed stream.
        // It is *important* that this is done before socket.close()!
        socketClosed = true;
        try {
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

        for (Plugin plugin : plugins) {
			plugin.shutdown();
		}

        // In most cases the close() should be successful, so set
        // connected to false here.
        connected = false;

        reader = null;
        writer = null;

        saslAuthentication.init();
    }

    public synchronized void disconnect(Presence unavailablePresence) {
        // If not connected, ignore this request.
        if (packetReader == null || packetWriter == null) {
            return;
        }

        if (!isConnected()) {
            return;
        }

        shutdown(unavailablePresence);

        wasAuthenticated = false;
    }

    public void sendPacket(Packet packet) {
        if (!isConnected()) {
            throw new IllegalStateException("Not connected to server.");
        }
        if (packet == null) {
            throw new NullPointerException("Packet is null.");
        }
        packetWriter.sendPacket(packet);
    }

    /**
     * Registers a packet interceptor with this connection. The interceptor will
     * be invoked every time a packet is about to be sent by this connection.
     * Interceptors may modify the packet to be sent. A packet filter determines
     * which packets will be delivered to the interceptor.
     *
     * @param packetInterceptor
     *            the packet interceptor to notify of packets about to be sent.
     * @param packetFilter
     *            the packet filter to use.
     * @deprecated replaced by
     *             {@link Connection#addPacketInterceptor(PacketInterceptor, PacketFilter)}
     *             .
     */
    public void addPacketWriterInterceptor(PacketInterceptor packetInterceptor,
            PacketFilter packetFilter) {
        addPacketInterceptor(packetInterceptor, packetFilter);
    }

    /**
     * Removes a packet interceptor.
     *
     * @param packetInterceptor
     *            the packet interceptor to remove.
     * @deprecated replaced by
     *             {@link Connection#removePacketInterceptor(PacketInterceptor)}
     *             .
     */
    public void removePacketWriterInterceptor(
            PacketInterceptor packetInterceptor) {
        removePacketInterceptor(packetInterceptor);
    }

    /**
     * Registers a packet listener with this connection. The listener will be
     * notified of every packet that this connection sends. A packet filter
     * determines which packets will be delivered to the listener. Note that the
     * thread that writes packets will be used to invoke the listeners.
     * Therefore, each packet listener should complete all operations quickly or
     * use a different thread for processing.
     *
     * @param packetListener
     *            the packet listener to notify of sent packets.
     * @param packetFilter
     *            the packet filter to use.
     * @deprecated replaced by
     *             {@link #addPacketSendingListener(PacketListener, PacketFilter)}
     *             .
     */
    public void addPacketWriterListener(PacketListener packetListener,
            PacketFilter packetFilter) {
        addPacketSendingListener(packetListener, packetFilter);
    }

    /**
     * Removes a packet listener for sending packets from this connection.
     *
     * @param packetListener
     *            the packet listener to remove.
     * @deprecated replaced by
     *             {@link #removePacketSendingListener(PacketListener)}.
     */
    public void removePacketWriterListener(PacketListener packetListener) {
        removePacketSendingListener(packetListener);
    }

    private void connectUsingConfiguration(ConnectionConfiguration config)
            throws XMPPException {
        XMPPException exception = null;
        Iterator<HostAddress> it = config.getHostAddresses().iterator();
        List<HostAddress> failedAddresses = new LinkedList<HostAddress>();
        boolean xmppIOError = false;
        while (it.hasNext()) {
            exception = null;
            HostAddress hostAddress = it.next();
            String host = hostAddress.getFQDN();
            int port = hostAddress.getPort();
            try {
                if (config.getSocketFactory() == null) {
                    this.socket = new Socket(host, port);
                } else {
                    this.socket = config.getSocketFactory().createSocket(host,
                            port);
                }
            } catch (UnknownHostException uhe) {
                String errorMessage = "Could not connect to " + host + ":"
                        + port + ".";
                exception = new XMPPException(errorMessage, new PacketError(
                        Condition.remote_server_timeout, Type.cancel,
                        errorMessage), uhe);
            } catch (IOException ioe) {
                String errorMessage = "XMPPError connecting to " + host + ":"
                        + port + ".";
                exception = new XMPPException(errorMessage, new PacketError(
                        Condition.internal_server_error, Type.cancel,
                        errorMessage), ioe);
                xmppIOError = true;
            }
            if (exception == null) {
                // We found a host to connect to, break here
                config.setUsedHostAddress(hostAddress);
                break;
            }
            hostAddress.setException(exception);
            failedAddresses.add(hostAddress);
            if (!it.hasNext()) {
                // There are no more host addresses to try
                // throw an exception and report all tried
                // HostAddresses in the exception
                StringBuilder sb = new StringBuilder();
                for (HostAddress fha : failedAddresses) {
                    sb.append(fha.getErrorMessage());
                    sb.append("; ");
                }
                PacketError xmppError;
                if (xmppIOError) {
                    xmppError = new PacketError(Condition.internal_server_error);
                } else {
                    xmppError = new PacketError(Condition.remote_server_timeout);
                }
                throw new XMPPException(sb.toString(), xmppError, exception);
            }
        }
        socketClosed = false;
        initConnection();
    }

    /**
     * Initializes the connection by creating a packet reader and writer and
     * opening a XMPP stream to the server.
     *
     * @throws XMPPException
     *             if establishing a connection to the server fails.
     */
    private void initConnection() throws XMPPException {
        boolean isFirstInitialization = packetReader == null
                || packetWriter == null;
        compressionHandler = null;
        serverAckdCompression = false;

        // Set the reader and writer instance variables
        initReaderAndWriter();

        try {
            if (isFirstInitialization) {
                packetWriter = new PacketWriter(this);
                packetReader = new PacketReader(this);

                // If debugging is enabled, we should start the thread that will
                // listen for
                // all packets and then log them.
                if (config.isDebuggerEnabled()) {
                    addPacketListener(debugger.getReaderListener(), null);
                    if (debugger.getWriterListener() != null) {
                        addPacketSendingListener(debugger.getWriterListener(),
                                null);
                    }
                }
            } else {
                packetWriter.init();
                packetReader.init();
            }

            // Start the packet writer. This will open a XMPP stream to the
            // server
            packetWriter.startup();
            // Start the packet reader. The startup() method will block until we
            // get an opening stream packet back from server.
            packetReader.startup();

            // Make note of the fact that we're now connected.
            connected = true;

            if (isFirstInitialization) {
                // Notify listeners that a new connection has been established
                for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
                    listener.connectionCreated(this);
                }
            }

        } catch (XMPPException ex) {
            // An exception occurred in setting up the connection. Make sure we
            // shut down the
            // readers and writers and close the socket.

            if (packetWriter != null) {
                try {
                    packetWriter.shutdown();
                } catch (Throwable ignore) { /* ignore */
                }
                packetWriter = null;
            }
            if (packetReader != null) {
                try {
                    packetReader.shutdown();
                } catch (Throwable ignore) { /* ignore */
                }
                packetReader = null;
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (Throwable ignore) { /* ignore */
                }
                reader = null;
            }
            if (writer != null) {
                try {
                    writer.close();
                } catch (Throwable ignore) { /* ignore */
                }
                writer = null;
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (Exception e) { /* ignore */
                }
                socket = null;
            }
            this.setWasAuthenticated(authenticated);
            authenticated = false;
            connected = false;

            throw ex; // Everything stoppped. Now throw the exception.
        }
    }

    private void initReaderAndWriter() throws XMPPException {
        try {
            if (compressionHandler == null) {
                reader = new BufferedReader(new InputStreamReader(
                        socket.getInputStream(), "UTF-8"));
                writer = new BufferedWriter(new OutputStreamWriter(
                        socket.getOutputStream(), "UTF-8"));
            } else {
                try {
                    OutputStream os = compressionHandler.getOutputStream(socket
                            .getOutputStream());
                    writer = new BufferedWriter(new OutputStreamWriter(os,
                            "UTF-8"));

                    InputStream is = compressionHandler.getInputStream(socket
                            .getInputStream());
                    reader = new BufferedReader(new InputStreamReader(is,
                            "UTF-8"));
                } catch (Exception e) {
                    e.printStackTrace();
                    compressionHandler = null;
                    reader = new BufferedReader(new InputStreamReader(
                            socket.getInputStream(), "UTF-8"));
                    writer = new BufferedWriter(new OutputStreamWriter(
                            socket.getOutputStream(), "UTF-8"));
                }
            }
        } catch (IOException ioe) {
            throw new XMPPException(
                    "XMPPError establishing connection with server.",
                    new PacketError(Condition.internal_server_error,
                            Type.cancel,
                            "XMPPError establishing connection with server."),
                    ioe);
        }

        // If debugging is enabled, we open a window and write out all network
        // traffic.
        initDebugger();
    }

    /***********************************************
     * TLS code below
     **********************************************/

    /**
     * Returns true if the connection to the server has successfully negotiated
     * TLS. Once TLS has been negotiatied the connection has been secured.
     *
     * @return true if the connection to the server has successfully negotiated
     *         TLS.
     */
    public boolean isUsingTLS() {
        return usingTLS;
    }

    /**
     * Notification message saying that the server supports TLS so confirm the
     * server that we want to secure the connection.
     *
     * @param required
     *            true when the server indicates that TLS is required.
     */
    void startTLSReceived(boolean required) {
        if (required
                && config.getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) {
            notifyConnectionError(new IllegalStateException(
                    "TLS required by server but not allowed by connection configuration"));
            return;
        }

        if (config.getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) {
            // Do not secure the connection using TLS since TLS was disabled
            return;
        }
        try {
            writer.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
            writer.flush();
        } catch (IOException e) {
            notifyConnectionError(e);
        }
    }

    /**
     * The server has indicated that TLS negotiation can start. We now need to
     * secure the existing plain connection and perform a handshake. This method
     * won't return until the connection has finished the handshake or an error
     * occured while securing the connection.
     *
     * @throws Exception
     *             if an exception occurs.
     */
    void proceedTLSReceived() throws Exception {
        SSLContext context = this.config.getCustomSSLContext();
        KeyStore ks = null;
        KeyManager[] kms = null;
        PasswordCallback pcb = null;

        if (config.getCallbackHandler() == null) {
            ks = null;
        } else if (context == null) {
            if (config.getKeystoreType().equals("NONE")) {
                ks = null;
                pcb = null;
            } else if (config.getKeystoreType().equals("PKCS11")) {
                try {
                    Constructor<?> c = Class.forName(
                            "sun.security.pkcs11.SunPKCS11").getConstructor(
                            InputStream.class);
                    String pkcs11Config = "name = SmartCard\nlibrary = "
                            + config.getPKCS11Library();
                    ByteArrayInputStream config = new ByteArrayInputStream(
                            pkcs11Config.getBytes());
                    Provider p = (Provider) c.newInstance(config);
                    Security.addProvider(p);
                    ks = KeyStore.getInstance("PKCS11", p);
                    pcb = new PasswordCallback("PKCS11 Password: ", false);
                    this.config.getCallbackHandler().handle(
                            new Callback[] { pcb });
                    ks.load(null, pcb.getPassword());
                } catch (Exception e) {
                    ks = null;
                    pcb = null;
                }
            } else if (config.getKeystoreType().equals("Apple")) {
                ks = KeyStore.getInstance("KeychainStore", "Apple");
                ks.load(null, null);
                // pcb = new PasswordCallback("Apple Keychain",false);
                // pcb.setPassword(null);
            } else {
                ks = KeyStore.getInstance(config.getKeystoreType());
                try {
                    pcb = new PasswordCallback("Keystore Password: ", false);
                    config.getCallbackHandler().handle(new Callback[] { pcb });
                    ks.load(new FileInputStream(config.getKeystorePath()),
                            pcb.getPassword());
                } catch (Exception e) {
                    ks = null;
                    pcb = null;
                }
            }
            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
            try {
                if (pcb == null) {
                    kmf.init(ks, null);
                } else {
                    kmf.init(ks, pcb.getPassword());
                    pcb.clearPassword();
                }
                kms = kmf.getKeyManagers();
            } catch (NullPointerException npe) {
                kms = null;
            }
        }

        // Verify certificate presented by the server
        if (context == null) {
            context = SSLContext.getInstance("TLS");
            context.init(kms, null, new java.security.SecureRandom());
        }
        Socket plain = socket;
        // Secure the plain connection
        socket = context.getSocketFactory().createSocket(plain,
                plain.getInetAddress().getHostAddress(), plain.getPort(), true);
        socket.setSoTimeout(0);
        socket.setKeepAlive(true);
        // Initialize the reader and writer with the new secured version
        initReaderAndWriter();
        // Proceed to do the handshake
        ((SSLSocket) socket).startHandshake();
        // if (((SSLSocket) socket).getWantClientAuth()) {
        // System.err.println("Connection wants client auth");
        // }
        // else if (((SSLSocket) socket).getNeedClientAuth()) {
        // System.err.println("Connection needs client auth");
        // }
        // else {
        // System.err.println("Connection does not require client auth");
        // }
        // Set that TLS was successful
        usingTLS = true;

        // Set the new writer to use
        packetWriter.setWriter(writer);
        // Send a new opening stream to the server
        packetWriter.openStream();
    }

    /**
     * Sets the available stream compression methods offered by the server.
     *
     * @param methods
     *            compression methods offered by the server.
     */
    void setAvailableCompressionMethods(Collection<String> methods) {
        compressionMethods = methods;
    }

    /**
     * Returns the compression handler that can be used for one compression
     * methods offered by the server.
     *
     * @return a instance of XMPPInputOutputStream or null if no suitable
     *         instance was found
     *
     */
    private XMPPInputOutputStream maybeGetCompressionHandler() {
        if (compressionMethods != null) {
            for (XMPPInputOutputStream handler : compressionHandlers) {
                if (!handler.isSupported())
                    continue;

                String method = handler.getCompressionMethod();
                if (compressionMethods.contains(method))
                    return handler;
            }
        }
        return null;
    }

    public boolean isUsingCompression() {
        return compressionHandler != null && serverAckdCompression;
    }

    /**
     * Starts using stream compression that will compress network traffic.
     * Traffic can be reduced up to 90%. Therefore, stream compression is ideal
     * when using a slow speed network connection. However, the server and the
     * client will need to use more CPU time in order to un/compress network
     * data so under high load the server performance might be affected.
     *

     *

     * Stream compression has to have been previously offered by the server.
     * Currently only the zlib method is supported by the client. Stream
     * compression negotiation has to be done before authentication took place.
     *

     *

     * Note: to use stream compression the smackx.jar file has to be present in
     * the classpath.
     *
     * @return true if stream compression negotiation was successful.
     */
    private boolean useCompression() {
        // If stream compression was offered by the server and we want to use
        // compression then send compression request to the server
        if (authenticated) {
            throw new IllegalStateException(
                    "Compression should be negotiated before authentication.");
        }

        if ((compressionHandler = maybeGetCompressionHandler()) != null) {
            requestStreamCompression(compressionHandler.getCompressionMethod());
            // Wait until compression is being used or a timeout happened
            synchronized (this) {
                try {
                    this.wait(SmackConfiguration.getPacketReplyTimeout() * 5);
                } catch (InterruptedException e) {
                    // Ignore.
                }
            }
            return isUsingCompression();
        }
        return false;
    }

    /**
     * Request the server that we want to start using stream compression. When
     * using TLS then negotiation of stream compression can only happen after
     * TLS was negotiated. If TLS compression is being used the stream
     * compression should not be used.
     */
    private void requestStreamCompression(String method) {
        try {
            writer.write("<compress xmlns='http://jabber.org/protocol/compress'>");
            writer.write("<method>" + method + "</method></compress>");
            writer.flush();
        } catch (IOException e) {
            notifyConnectionError(e);
        }
    }

    /**
     * Start using stream compression since the server has acknowledged stream
     * compression.
     *
     * @throws Exception
     *             if there is an exception starting stream compression.
     */
    void startStreamCompression() throws Exception {
        serverAckdCompression = true;
        // Initialize the reader and writer with the new secured version
        initReaderAndWriter();

        // Set the new writer to use
        packetWriter.setWriter(writer);
        // Send a new opening stream to the server
        packetWriter.openStream();
        // Notify that compression is being used
        synchronized (this) {
            this.notify();
        }
    }

    /**
     * Notifies the XMPP connection that stream compression was denied so that
     * the connection process can proceed.
     */
    void streamCompressionDenied() {
        synchronized (this) {
            this.notify();
        }
    }

    /**
     * Establishes a connection to the XMPP server and performs an automatic
     * login only if the previous connection state was logged (authenticated).
     * It basically creates and maintains a socket connection to the server.
     *

     *

     * Listeners will be preserved from a previous connection if the
     * reconnection occurs after an abrupt termination.
     *
     * @throws XMPPException
     *             if an error occurs while trying to establish the connection.
     *             Two possible errors can occur which will be wrapped by an
     *             XMPPException -- UnknownHostException (XMPP error code 504),
     *             and IOException (XMPP error code 502). The error codes and
     *             wrapped exceptions can be used to present more appropriate
     *             error messages to end-users.
     */
    public void connect() throws XMPPException {
        // Establishes the connection, readers and writers
        connectUsingConfiguration(config);
        // Automatically makes the login if the user was previously connected
        // successfully
        // to the server and the connection was terminated abruptly
        if (connected && wasAuthenticated) {
            // Make the login
            if (isAnonymous()) {
                // Make the anonymous login
                loginAnonymously();
            } else {
                login(config.getUsername(), config.getPassword(),
                        config.getResource());
            }
            notifyReconnection();
        }
    }

    /**
     * Sets whether the connection has already logged in the server.
     *
     * @param wasAuthenticated
     *            true if the connection has already been authenticated.
     */
    private void setWasAuthenticated(boolean wasAuthenticated) {
        if (!this.wasAuthenticated) {
            this.wasAuthenticated = wasAuthenticated;
        }
    }

    /**
     * Sends out a notification that there was an error with the connection and
     * closes the connection. Also prints the stack trace of the given exception
     *
     * @param e
     *            the exception that causes the connection close event.
     */
    synchronized void notifyConnectionError(Exception e) {
        // Listeners were already notified of the exception, return right here.
        if ((packetReader == null || packetReader.done)
                && (packetWriter == null || packetWriter.done))
            return;

        if (packetReader != null)
            packetReader.done = true;
        if (packetWriter != null)
            packetWriter.done = true;
        // Closes the connection temporary. A reconnection is possible
        shutdown(new Presence(Presence.Type.unavailable));
        // Notify connection listeners of the error.
        for (ConnectionListener listener : getConnectionListeners()) {
            try {
                listener.connectionClosedOnError(e);
            } catch (Exception e2) {
                // Catch and print any exception so we can recover
                // from a faulty listener
                e2.printStackTrace();
            }
        }
    }

    /**
     * Sends a notification indicating that the connection was reconnected
     * successfully.
     */
    protected void notifyReconnection() {
        // Notify connection listeners of the reconnection.
        for (ConnectionListener listener : getConnectionListeners()) {
            try {
                listener.reconnectionSuccessful();
            } catch (Exception e) {
                // Catch and print any exception so we can recover
                // from a faulty listener
                e.printStackTrace();
            }
        }
    }

    public void registerPlugin(Plugin plugin) {
        this.plugins.add(plugin);
    }

    public Set<Plugin> getPlugins() {
        return Collections.unmodifiableSet(plugins);
    }
}


/**
 *
 * Copyright 2003-2007 Jive Software.
 *
 * All rights reserved. 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 org.jivesoftware.smack;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.security.auth.callback.CallbackHandler;

import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.packet.Bind;
import org.jivesoftware.smack.packet.Session;
import org.jivesoftware.smack.sasl.SASLAnonymous;
import org.jivesoftware.smack.sasl.SASLCramMD5Mechanism;
import org.jivesoftware.smack.sasl.SASLDigestMD5Mechanism;
import org.jivesoftware.smack.sasl.SASLExternalMechanism;
import org.jivesoftware.smack.sasl.SASLGSSAPIMechanism;
import org.jivesoftware.smack.sasl.SASLMechanism;
import org.jivesoftware.smack.sasl.SASLPlainMechanism;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Packet;

/**
 *

 * This class is responsible authenticating the user using SASL, binding the
 * resource to the connection and establishing a session with the server.
 *

 *
 *

 * Once TLS has been negotiated (i.e. the connection has been secured) it is
 * possible to register with the server, authenticate using Non-SASL or
 * authenticate using SASL. If the server supports SASL then Smack will first
 * try to authenticate using SASL. But if that fails then Non-SASL will be
 * tried.
 *

 *
 *

 * The server may support many SASL mechanisms to use for authenticating. Out of
 * the box Smack provides several SASL mechanisms, but it is possible to
 * register new SASL Mechanisms. Use
 * {@link #registerSASLMechanism(String, Class)} to register a new mechanisms. A
 * registered mechanism wont be used until
 * {@link #supportSASLMechanism(String, int)} is called. By default, the list of
 * supported SASL mechanisms is determined from the {@link SmackConfiguration}.
 *

 *
 *

 * Once the user has been authenticated with SASL, it is necessary to bind a
 * resource for the connection. If no resource is passed in
 * {@link #authenticate(String, String, String)} then the server will assign a
 * resource for the connection. In case a resource is passed then the server
 * will receive the desired resource but may assign a modified resource for the
 * connection.
 *

 *
 *

 * Once a resource has been binded and if the server supports sessions then
 * Smack will establish a session so that instant messaging and presence
 * functionalities may be used.
 *

 *
 * @see org.jivesoftware.smack.sasl.SASLMechanism
 *
 * @author Gaston Dombiak
 * @author Jay Kline
 */
public class SASLAuthentication implements UserAuthentication {

    private static Map<String, Class<? extends SASLMechanism>> implementedMechanisms = new HashMap<String, Class<? extends SASLMechanism>>();
    private static List<String> mechanismsPreferences = new ArrayList<String>();

    private Connection connection;
    private Collection<String> serverMechanisms = new ArrayList<String>();
    private SASLMechanism currentMechanism = null;
    /**
     * Boolean indicating if SASL negotiation has finished and was successful.
     */
    private boolean saslNegotiated;
    /**
     * Boolean indication if SASL authentication has failed. When failed the
     * server may end the connection.
     */
    private boolean saslFailed;
    private boolean resourceBinded;
    private boolean sessionSupported;
    /**
     * The SASL related error condition if there was one provided by the server.
     */
    private String errorCondition;

    static {

        // Register SASL mechanisms supported by Smack
        registerSASLMechanism("EXTERNAL", SASLExternalMechanism.class);
        registerSASLMechanism("GSSAPI", SASLGSSAPIMechanism.class);
        registerSASLMechanism("DIGEST-MD5", SASLDigestMD5Mechanism.class);
        registerSASLMechanism("CRAM-MD5", SASLCramMD5Mechanism.class);
        registerSASLMechanism("PLAIN", SASLPlainMechanism.class);
        registerSASLMechanism("ANONYMOUS", SASLAnonymous.class);

        supportSASLMechanism("GSSAPI", 0);
        supportSASLMechanism("DIGEST-MD5", 1);
        supportSASLMechanism("CRAM-MD5", 2);
        supportSASLMechanism("PLAIN", 3);
        supportSASLMechanism("ANONYMOUS", 4);

    }

    /**
     * Registers a new SASL mechanism
     *
     * @param name
     *            common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or
     *            KERBEROS_V4.
     * @param mClass
     *            a SASLMechanism subclass.
     */
    public static void registerSASLMechanism(String name,
            Class<? extends SASLMechanism> mClass) {
        implementedMechanisms.put(name, mClass);
    }

    /**
     * Unregisters an existing SASL mechanism. Once the mechanism has been
     * unregistered it won't be possible to authenticate users using the removed
     * SASL mechanism. It also removes the mechanism from the supported list.
     *
     * @param name
     *            common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or
     *            KERBEROS_V4.
     */
    public static void unregisterSASLMechanism(String name) {
        implementedMechanisms.remove(name);
        mechanismsPreferences.remove(name);
    }

    /**
     * Registers a new SASL mechanism in the specified preference position. The
     * client will try to authenticate using the most prefered SASL mechanism
     * that is also supported by the server. The SASL mechanism must be
     * registered via {@link #registerSASLMechanism(String, Class)}
     *
     * @param name
     *            common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or
     *            KERBEROS_V4.
     */
    public static void supportSASLMechanism(String name) {
        mechanismsPreferences.add(0, name);
    }

    /**
     * Registers a new SASL mechanism in the specified preference position. The
     * client will try to authenticate using the most prefered SASL mechanism
     * that is also supported by the server. Use the <tt>index</tt> parameter to
     * set the level of preference of the new SASL mechanism. A value of 0 means
     * that the mechanism is the most prefered one. The SASL mechanism must be
     * registered via {@link #registerSASLMechanism(String, Class)}
     *
     * @param name
     *            common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or
     *            KERBEROS_V4.
     * @param index
     *            preference position amongst all the implemented SASL
     *            mechanism. Starts with 0.
     */
    public static void supportSASLMechanism(String name, int index) {
        mechanismsPreferences.add(index, name);
    }

    /**
     * Un-supports an existing SASL mechanism. Once the mechanism has been
     * unregistered it won't be possible to authenticate users using the removed
     * SASL mechanism. Note that the mechanism is still registered, but will
     * just not be used.
     *
     * @param name
     *            common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or
     *            KERBEROS_V4.
     */
    public static void unsupportSASLMechanism(String name) {
        mechanismsPreferences.remove(name);
    }

    /**
     * Returns the registerd SASLMechanism classes sorted by the level of
     * preference.
     *
     * @return the registerd SASLMechanism classes sorted by the level of
     *         preference.
     */
    public static List<Class<? extends SASLMechanism>> getRegisterSASLMechanisms() {
        List<Class<? extends SASLMechanism>> answer = new ArrayList<Class<? extends SASLMechanism>>();
        for (String mechanismsPreference : mechanismsPreferences) {
            answer.add(implementedMechanisms.get(mechanismsPreference));
        }
        return answer;
    }

    SASLAuthentication(Connection connection) {
        super();
        this.connection = connection;
        this.init();
    }

    /**
     * Returns true if the server offered ANONYMOUS SASL as a way to
     * authenticate users.
     *
     * @return true if the server offered ANONYMOUS SASL as a way to
     *         authenticate users.
     */
    public boolean hasAnonymousAuthentication() {
        return serverMechanisms.contains("ANONYMOUS");
    }

    /**
     * Returns true if the server offered SASL authentication besides ANONYMOUS
     * SASL.
     *
     * @return true if the server offered SASL authentication besides ANONYMOUS
     *         SASL.
     */
    public boolean hasNonAnonymousAuthentication() {
        return !serverMechanisms.isEmpty()
                && (serverMechanisms.size() != 1 || !hasAnonymousAuthentication());
    }

    /**
     * Performs SASL authentication of the specified user. If SASL
     * authentication was successful then resource binding and session
     * establishment will be performed. This method will return the full JID
     * provided by the server while binding a resource to the connection.
     *

     *
     * The server may assign a full JID with a username or resource different
     * than the requested by this method.
     *
     * @param username
     *            the username that is authenticating with the server.
     * @param resource
     *            the desired resource.
     * @param cbh
     *            the CallbackHandler used to get information from the user
     * @return the full JID provided by the server while binding a resource to
     *         the connection.
     * @throws XMPPException
     *             if an error occures while authenticating.
     */
    public String authenticate(String username, String resource,
            CallbackHandler cbh) throws XMPPException {
        // Locate the SASLMechanism to use
        String selectedMechanism = null;
        for (String mechanism : mechanismsPreferences) {
            if (implementedMechanisms.containsKey(mechanism)
                    && serverMechanisms.contains(mechanism)) {
                selectedMechanism = mechanism;
                break;
            }
        }
        if (selectedMechanism != null) {
            // A SASL mechanism was found. Authenticate using the selected
            // mechanism and then
            // proceed to bind a resource
            try {
                Class<? extends SASLMechanism> mechanismClass = implementedMechanisms
                        .get(selectedMechanism);
                Constructor<? extends SASLMechanism> constructor = mechanismClass
                        .getConstructor(SASLAuthentication.class);
                currentMechanism = constructor.newInstance(this);
                // Trigger SASL authentication with the selected mechanism. We
                // use
                // connection.getHost() since GSAPI requires the FQDN of the
                // server, which
                // may not match the XMPP domain.
                currentMechanism.authenticate(username, connection.getHost(),
                        cbh);

                // Wait until SASL negotiation finishes
                synchronized (this) {
                    if (!saslNegotiated && !saslFailed) {
                        try {
                            wait(30000);
                        } catch (InterruptedException e) {
                            // Ignore
                        }
                    }
                }

                if (saslFailed) {
                    // SASL authentication failed and the server may have closed
                    // the connection
                    // so throw an exception
                    if (errorCondition != null) {
                        throw new XMPPException("SASL authentication "
                                + selectedMechanism + " failed: "
                                + errorCondition);
                    } else {
                        throw new XMPPException(
                                "SASL authentication failed using mechanism "
                                        + selectedMechanism);
                    }
                }

                if (saslNegotiated) {
                    // Bind a resource for this connection and
                    return bindResourceAndEstablishSession(resource);
                } else {
                    // SASL authentication failed
                }
            } catch (XMPPException e) {
                throw e;
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            throw new XMPPException(
                    "SASL Authentication failed. No known authentication mechanisims.");
        }
        throw new XMPPException("SASL authentication failed");
    }

    /**
     * Performs SASL authentication of the specified user. If SASL
     * authentication was successful then resource binding and session
     * establishment will be performed. This method will return the full JID
     * provided by the server while binding a resource to the connection.
     *

     *
     * The server may assign a full JID with a username or resource different
     * than the requested by this method.
     *
     * @param username
     *            the username that is authenticating with the server.
     * @param password
     *            the password to send to the server.
     * @param resource
     *            the desired resource.
     * @return the full JID provided by the server while binding a resource to
     *         the connection.
     * @throws XMPPException
     *             if an error occures while authenticating.
     */
    public String authenticate(String username, String password, String resource)
            throws XMPPException {
        // Locate the SASLMechanism to use
        String selectedMechanism = null;
        for (String mechanism : mechanismsPreferences) {
            if (implementedMechanisms.containsKey(mechanism)
                    && serverMechanisms.contains(mechanism)) {
                selectedMechanism = mechanism;
                break;
            }
        }
        if (selectedMechanism != null) {
            // A SASL mechanism was found. Authenticate using the selected
            // mechanism and then
            // proceed to bind a resource
            try {
                Class<? extends SASLMechanism> mechanismClass = implementedMechanisms
                        .get(selectedMechanism);
                Constructor<? extends SASLMechanism> constructor = mechanismClass
                        .getConstructor(SASLAuthentication.class);
                currentMechanism = constructor.newInstance(this);
                // Trigger SASL authentication with the selected mechanism. We
                // use
                // connection.getHost() since GSAPI requires the FQDN of the
                // server, which
                // may not match the XMPP domain.

                // The serviceName is basically the value that XMPP server sends
                // to the client as being the location
                // of the XMPP service we are trying to connect to. This should
                // have the format: host [ "/" serv-name ]
                // as per RFC-2831 guidelines
                String serviceName = connection.getServiceName();
                currentMechanism.authenticate(username, connection.getHost(),
                        serviceName, password);

                // Wait until SASL negotiation finishes
                synchronized (this) {
                    if (!saslNegotiated && !saslFailed) {
                        try {
                            wait(30000);
                        } catch (InterruptedException e) {
                            // Ignore
                        }
                    }
                }

                if (saslFailed) {
                    // SASL authentication failed and the server may have closed
                    // the connection
                    // so throw an exception
                    if (errorCondition != null) {
                        throw new XMPPException("SASL authentication "
                                + selectedMechanism + " failed: "
                                + errorCondition);
                    } else {
                        throw new XMPPException(
                                "SASL authentication failed using mechanism "
                                        + selectedMechanism);
                    }
                }

                if (saslNegotiated) {
                    // Bind a resource for this connection and
                    return bindResourceAndEstablishSession(resource);
                } else {
                    // SASL authentication failed so try a Non-SASL
                    // authentication
                    return new NonSASLAuthentication(connection).authenticate(
                            username, password, resource);
                }
            } catch (XMPPException e) {
                throw e;
            } catch (Exception e) {
                e.printStackTrace();
                // SASL authentication failed so try a Non-SASL authentication
                return new NonSASLAuthentication(connection).authenticate(
                        username, password, resource);
            }
        } else {
            // No SASL method was found so try a Non-SASL authentication
            return new NonSASLAuthentication(connection).authenticate(username,
                    password, resource);
        }
    }

    /**
     * Performs ANONYMOUS SASL authentication. If SASL authentication was
     * successful then resource binding and session establishment will be
     * performed. This method will return the full JID provided by the server
     * while binding a resource to the connection.
     *

     *
     * The server will assign a full JID with a randomly generated resource and
     * possibly with no username.
     *
     * @return the full JID provided by the server while binding a resource to
     *         the connection.
     * @throws XMPPException
     *             if an error occures while authenticating.
     */
    public String authenticateAnonymously() throws XMPPException {
        try {
            currentMechanism = new SASLAnonymous(this);
            currentMechanism.authenticate(null, null, null, "");

            // Wait until SASL negotiation finishes
            synchronized (this) {
                if (!saslNegotiated && !saslFailed) {
                    try {
                        wait(5000);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }
            }

            if (saslFailed) {
                // SASL authentication failed and the server may have closed the
                // connection
                // so throw an exception
                if (errorCondition != null) {
                    throw new XMPPException("SASL authentication failed: "
                            + errorCondition);
                } else {
                    throw new XMPPException("SASL authentication failed");
                }
            }

            if (saslNegotiated) {
                // Bind a resource for this connection and
                return bindResourceAndEstablishSession(null);
            } else {
                return new NonSASLAuthentication(connection)
                        .authenticateAnonymously();
            }
        } catch (IOException e) {
            return new NonSASLAuthentication(connection)
                    .authenticateAnonymously();
        }
    }

    private String bindResourceAndEstablishSession(String resource)
            throws XMPPException {
        // Wait until server sends response containing the <bind> element
        synchronized (this) {
            if (!resourceBinded) {
                try {
                    wait(30000);
                } catch (InterruptedException e) {
                    // Ignore
                }
            }
        }

        if (!resourceBinded) {
            // Server never offered resource binding
            throw new XMPPException("Resource binding not offered by server");
        }

        Bind bindResource = new Bind();
        bindResource.setResource(resource);

        PacketCollector collector = connection
                .createPacketCollector(new PacketIDFilter(bindResource.getID()));
        // Send the packet
        connection.sendPacket(bindResource);
        // Wait up to a certain number of seconds for a response from the
        // server.
        Bind response = (Bind) collector.nextResult(SmackConfiguration
                .getPacketReplyTimeout());
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from the server.");
        }
        // If the server replied with an error, throw an exception.
        else if (response.getType() == IQ.Type.error) {
            throw new XMPPException(response.getError());
        }
        String userJID = response.getJid();

        if (sessionSupported) {
            Session session = new Session();
            collector = connection.createPacketCollector(new PacketIDFilter(
                    session.getID()));
            // Send the packet
            connection.sendPacket(session);
            // Wait up to a certain number of seconds for a response from the
            // server.
            IQ ack = (IQ) collector.nextResult(SmackConfiguration
                    .getPacketReplyTimeout());
            collector.cancel();
            if (ack == null) {
                throw new XMPPException("No response from the server.");
            }
            // If the server replied with an error, throw an exception.
            else if (ack.getType() == IQ.Type.error) {
                throw new XMPPException(ack.getError());
            }
        }
        return userJID;
    }

    /**
     * Sets the available SASL mechanism reported by the server. The server will
     * report the available SASL mechanism once the TLS negotiation was
     * successful. This information is stored and will be used when doing the
     * authentication for logging in the user.
     *
     * @param mechanisms
     *            collection of strings with the available SASL mechanism
     *            reported by the server.
     */
    void setAvailableSASLMethods(Collection<String> mechanisms) {
        this.serverMechanisms = mechanisms;
    }

    /**
     * Returns true if the user was able to authenticate with the server usins
     * SASL.
     *
     * @return true if the user was able to authenticate with the server usins
     *         SASL.
     */
    public boolean isAuthenticated() {
        return saslNegotiated;
    }

    /**
     * The server is challenging the SASL authentication we just sent. Forward
     * the challenge to the current SASLMechanism we are using. The
     * SASLMechanism will send a response to the server. The length of the
     * challenge-response sequence varies according to the SASLMechanism in use.
     *
     * @param challenge
     *            a base64 encoded string representing the challenge.
     * @throws IOException
     *             If a network error occures while authenticating.
     */
    void challengeReceived(String challenge) throws IOException {
        currentMechanism.challengeReceived(challenge);
    }

    /**
     * Notification message saying that SASL authentication was successful. The
     * next step would be to bind the resource.
     */
    void authenticated() {
        synchronized (this) {
            saslNegotiated = true;
            // Wake up the thread that is waiting in the #authenticate method
            notify();
        }
    }

    /**
     * Notification message saying that SASL authentication has failed. The
     * server may have closed the connection depending on the number of possible
     * retries.
     *
     * @deprecated replaced by {@see #authenticationFailed(String)}.
     */
    void authenticationFailed() {
        authenticationFailed(null);
    }

    /**
     * Notification message saying that SASL authentication has failed. The
     * server may have closed the connection depending on the number of possible
     * retries.
     *
     * @param condition
     *            the error condition provided by the server.
     */
    void authenticationFailed(String condition) {
        synchronized (this) {
            saslFailed = true;
            errorCondition = condition;
            // Wake up the thread that is waiting in the #authenticate method
            notify();
        }
    }

    /**
     * Notification message saying that the server requires the client to bind a
     * resource to the stream.
     */
    void bindingRequired() {
        synchronized (this) {
            resourceBinded = true;
            // Wake up the thread that is waiting in the #authenticate method
            notify();
        }
    }

    public void send(Packet stanza) {
        connection.sendPacket(stanza);
    }

    /**
     * Notification message saying that the server supports sessions. When a
     * server supports sessions the client needs to send a Session packet after
     * successfully binding a resource for the session.
     */
    void sessionsSupported() {
        sessionSupported = true;
    }

    /**
     * Initializes the internal state in order to be able to be reused. The
     * authentication is used by the connection at the first login and then
     * reused after the connection is disconnected and then reconnected.
     */
    protected void init() {
        saslNegotiated = false;
        saslFailed = false;
        resourceBinded = false;
        sessionSupported = false;
    }
}


package uk.co.tfd.sm.milton.spase;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.xml.namespace.QName;

import org.apache.commons.lang.StringUtils;
import org.sakaiproject.nakamura.api.lite.ClientPoolException;
import org.sakaiproject.nakamura.api.lite.CommitHandler;
import org.sakaiproject.nakamura.api.lite.Repository;
import org.sakaiproject.nakamura.api.lite.Session;
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.StorageClientUtils;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.api.lite.accesscontrol.Permission;
import org.sakaiproject.nakamura.api.lite.accesscontrol.Permissions;
import org.sakaiproject.nakamura.api.lite.accesscontrol.Security;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
import org.sakaiproject.nakamura.api.lite.content.Content;
import org.sakaiproject.nakamura.api.lite.content.ContentManager;
import org.sakaiproject.nakamura.api.lite.lock.AlreadyLockedException;
import org.sakaiproject.nakamura.api.lite.lock.LockState;
import org.sakaiproject.nakamura.api.lite.util.PreemptiveIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.bradmcevoy.common.ContentTypeUtils;
import com.bradmcevoy.http.Auth;
import com.bradmcevoy.http.CollectionResource;
import com.bradmcevoy.http.FileItem;
import com.bradmcevoy.http.FileResource;
import com.bradmcevoy.http.FolderResource;
import com.bradmcevoy.http.LockInfo;
import com.bradmcevoy.http.LockInfo.LockDepth;
import com.bradmcevoy.http.LockInfo.LockScope;
import com.bradmcevoy.http.LockInfo.LockType;
import com.bradmcevoy.http.LockResult;
import com.bradmcevoy.http.LockResult.FailureReason;
import com.bradmcevoy.http.LockTimeout;
import com.bradmcevoy.http.LockTimeout.DateAndSeconds;
import com.bradmcevoy.http.LockToken;
import com.bradmcevoy.http.LockableResource;
import com.bradmcevoy.http.LockingCollectionResource;
import com.bradmcevoy.http.Range;
import com.bradmcevoy.http.Request;
import com.bradmcevoy.http.Request.Method;
import com.bradmcevoy.http.Resource;
import com.bradmcevoy.http.exceptions.BadRequestException;
import com.bradmcevoy.http.exceptions.ConflictException;
import com.bradmcevoy.http.exceptions.LockedException;
import com.bradmcevoy.http.exceptions.NotAuthorizedException;
import com.bradmcevoy.http.exceptions.PreConditionFailedException;
import com.bradmcevoy.property.MultiNamespaceCustomPropertyResource;
import com.bradmcevoy.property.PropertySource.PropertyAccessibility;
import com.bradmcevoy.property.PropertySource.PropertyMetaData;
import com.bradmcevoy.property.PropertySource.PropertySetException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;

public class SparseMiltonContentResource implements FileResource, FolderResource,
		MultiNamespaceCustomPropertyResource, LockableResource,
		LockingCollectionResource {

	public static class LockHolder {

		private LockInfo lockInfo;
		private long timeoutSeconds;
		private long expiryTime;
		private String asString;
		private LockTimeout lockTimeout;

		public LockHolder(LockInfo lockInfo, LockTimeout timeout) {
			Long timeoutSeconds = timeout.getSeconds();
			if (timeoutSeconds == null) {
				timeoutSeconds = 3600L;
			}
			DateAndSeconds t = timeout.getLockedUntil(3600L, timeoutSeconds);
			this.lockInfo = lockInfo;
			this.lockTimeout = timeout;
			this.timeoutSeconds = timeoutSeconds;
			this.expiryTime = t.date.getTime();
			asString = lockInfo.lockedByUser + ":" + lockInfo.depth + ":"
					+ lockInfo.scope + ":" + lockInfo.type + ":" + expiryTime
					+ ":" + timeoutSeconds;

		}

		public LockHolder(String lock, boolean adjustTimeout) {
			String[] parts = StringUtils.split(lock, ':');
			this.lockInfo = new LockInfo(LockScope.valueOf(parts[2]),
					LockType.valueOf(parts[3]), parts[0],
					LockDepth.valueOf(parts[1]));
			this.expiryTime = Long.parseLong(parts[4]);
			if (adjustTimeout) {
				timeoutSeconds = ((System.currentTimeMillis() - expiryTime) / 1000);
			} else {
				timeoutSeconds = Long.parseLong(parts[5]);
			}
			this.lockTimeout = new LockTimeout(timeoutSeconds);
			asString = lockInfo.lockedByUser + ":" + lockInfo.depth + ":"
					+ lockInfo.scope + ":" + lockInfo.type + ":" + expiryTime
					+ ":" + timeoutSeconds;

		}

		@Override
		public String toString() {
			return asString;
		}

		public long getTimeoutInSeconds() {
			return timeoutSeconds;
		}

		public LockInfo getLockInfo() {
			return lockInfo;
		}

		public LockTimeout getLockTimeout() {
			return lockTimeout;
		}

	}

	private static final Logger LOGGER = LoggerFactory
			.getLogger(SparseMiltonContentResource.class);
	private static final Map<Method, Permission> METHOD_PERMISSIONS = getMethodPermissionMap();
	private static final Set<Method> PROPERTY_WRITE_METHODS = ImmutableSet
			.of(Method.PROPPATCH);
	private static final Set<Method> REDIRECT_METHODS = ImmutableSet.of(
			Method.ACL, Method.COPY, Method.DELETE, Method.MKCALENDAR,
			Method.MKCOL, Method.MOVE, Method.POST, Method.PROPPATCH,
			Method.PUT);
	@SuppressWarnings("unused")
	private static final Set<Method> WRITE_METHODS = ImmutableSet.of(
			Method.COPY, Method.ACL, Method.DELETE, Method.MKCALENDAR,
			Method.MKCOL, Method.MOVE, Method.POST, Method.PROPPATCH,
			Method.PUT);
	private static final long LONG_MAX_AGE = 3600L * 24L * 45L;

	private String name;
	private Repository repository;
	private String path;
	private Content content;
	private Session session;
	private boolean authorizedToWriteProperties;

	public SparseMiltonContentResource(String name, String path, Session session, Content content) {
		this.name = name;
		this.path = path;
		this.content = content;
		this.session = session;
		LOGGER.debug("Created content with content object of {} {} ", this,
				this.content);
	}

	public SparseMiltonContentResource(String path, Session session,
			Content newContent) {
		this(StorageClientUtils.getObjectName(path), path, session, newContent);
	}

	private static Map<Method, Permission> getMethodPermissionMap() {
		Builder<Method, Permission> b = ImmutableMap.builder();
		b.put(Method.ACL,
				Permissions.CAN_ANYTHING_ACL.combine(Permissions.CAN_READ));
		b.put(Method.CONNECT, Permissions.CAN_READ);
		b.put(Method.COPY, Permissions.CAN_READ); // need to be able to write to
													// the destination
		b.put(Method.DELETE,
				Permissions.CAN_READ.combine(Permissions.CAN_WRITE));
		b.put(Method.GET, Permissions.CAN_READ);
		b.put(Method.HEAD, Permissions.CAN_READ);
		b.put(Method.LOCK, Permissions.CAN_READ.combine(Permissions.CAN_WRITE));
		b.put(Method.MKCALENDAR,
				Permissions.CAN_READ.combine(Permissions.CAN_WRITE));
		b.put(Method.MKCOL, Permissions.CAN_READ.combine(Permissions.CAN_WRITE));
		b.put(Method.MOVE, Permissions.CAN_READ.combine(Permissions.CAN_WRITE)); // need
																					// to
																					// check
																					// destination
		b.put(Method.OPTIONS, Permissions.CAN_READ);
		b.put(Method.POST, Permissions.CAN_READ.combine(Permissions.CAN_WRITE)); // might
																					// need
																					// more
																					// here
		b.put(Method.PROPFIND, Permissions.CAN_READ);
		b.put(Method.PROPPATCH,
				Permissions.CAN_READ.combine(Permissions.CAN_WRITE));
		b.put(Method.PUT, Permissions.CAN_READ.combine(Permissions.CAN_WRITE));
		b.put(Method.REPORT, Permissions.CAN_READ);
		b.put(Method.TRACE, Permissions.CAN_READ);
		b.put(Method.UNLOCK,
				Permissions.CAN_READ.combine(Permissions.CAN_WRITE));
		return b.build();
	}

	public void copyTo(CollectionResource toCollection, String name)
			throws NotAuthorizedException, BadRequestException,
			ConflictException {
		try {
			String sourcePath = path;
			String destPath = StorageClientUtils.newPath(
					((SparseMiltonContentResource) toCollection).getPath(), name);
			StorageClientUtils.copyTree(session.getContentManager(),
					sourcePath, destPath, true);
		} catch (StorageClientException e) {
			LOGGER.error(e.getMessage(), e);
			throw new ConflictException(this, e.getMessage());
		} catch (AccessDeniedException e) {
			LOGGER.error(e.getMessage(), e);
			throw new NotAuthorizedException(toCollection);
		} catch (IOException e) {
			LOGGER.error(e.getMessage(), e);
			throw new ConflictException(this, e.getMessage());
		}
	}

	public String getUniqueId() {
		LOGGER.debug("Getting Unique ID from {} ", content);
		return content.getId();
	}

	public String getName() {
		LOGGER.debug("Getting name from {} ", content);
		return name;
	}

	private String getPath() {
		return path;
	}

	public Object authenticate(String user, String password) {
		LOGGER.debug("Authenticating agains the resource ");
		try {
			if (user == null || User.ANON_USER.equals(user)) {
				return repository.login();
			}
			return repository.login(user, password);
		} catch (ClientPoolException e) {
			LOGGER.error(e.getMessage(), e);
		} catch (StorageClientException e) {
			LOGGER.error(e.getMessage(), e);
		} catch (AccessDeniedException e) {
			LOGGER.error(e.getMessage(), e);
		}
		return null;
	}

	public boolean authorise(Request request, Method method, Auth auth) {
		Session session = (Session) auth.getTag();
		if (session == null) {
			LOGGER.debug("Not Authorized, session == null ");
			return false;
		}
		Permission permission = METHOD_PERMISSIONS.get(method);
		if (permission == null) {
			LOGGER.debug("Not Authorized, permissions == null ");
			return false;
		}
		try {
			session.getAccessControlManager().check(Security.ZONE_CONTENT,
					path, permission);
			LOGGER.debug("Authorized {} ", permission);
			authorizedToWriteProperties = PROPERTY_WRITE_METHODS
					.contains(method);
			return true;
		} catch (AccessDeniedException e) {
			LOGGER.error(e.getMessage(), e);
		} catch (StorageClientException e) {
			LOGGER.error(e.getMessage(), e);
		}
		LOGGER.debug("Authorize Failed ");
		return false;
	}

	public String getRealm() {
		LOGGER.debug("Get Realm ");
		return null;
	}

	public Date getModifiedDate() {
		LOGGER.debug("Get Modifled ");
		if (content != null) {
			if (content.hasProperty(Content.LASTMODIFIED_FIELD)) {
				return new Date(
						(Long) content.getProperty(Content.LASTMODIFIED_FIELD));
			} else {
				return new Date();
			}
		}
		return new Date(0L);
	}

	public String checkRedirect(Request request) {
		LOGGER.debug("Check Redirect ");
		if (REDIRECT_METHODS.contains(request.getMethod())) {
			return path;
		}
		return null;
	}

	public void delete() throws NotAuthorizedException, ConflictException,
			BadRequestException {
		LOGGER.debug("Delete On {} ", path);
		try {
			Iterable<String> i = content.listChildPaths();
			if (i.iterator().hasNext()) {
				throw new ConflictException(this,
						"Cant delete if there are child resources");
			}
			session.getContentManager().delete(content.getPath());
		} catch (AccessDeniedException e) {
			throw new NotAuthorizedException(this);
		} catch (StorageClientException e) {
			throw new BadRequestException(this, e.getMessage());
		}
	}

	public void sendContent(OutputStream out, Range range,
			Map<String, String> params, String contentType) throws IOException,
			NotAuthorizedException, BadRequestException {
		LOGGER.debug("Send Content ");
		try {
			InputStream in;
			try {
				// FIXME: This wont obey the server protection.
				// We need to reedirect and we need to be able to extract the identity when we do
				// WebDav should only be used in trusted situations where streaming bodies presents no risk.
				in = session.getContentManager().getInputStream(path);
			} catch (IOException e) {
				throw new BadRequestException(this, e.getMessage());
			}
			if (in == null) {
				return;
			}
			byte[] buffer = new byte[10240];
			if (range != null) {
				try {
					in.skip(range.getStart());
				} catch (IOException e) {
					throw new BadRequestException(this, e.getMessage());
				}
			}
			for (;;) {
				int nr;
				try {
					nr = in.read(buffer);
				} catch (IOException e) {
					throw new BadRequestException(this, e.getMessage());
				}
				if (nr == -1) {
					break;
				}
				out.write(buffer, 0, nr);
			}
		} catch (StorageClientException e) {
			throw new BadRequestException(this, e.getMessage());
		} catch (AccessDeniedException e) {
			throw new NotAuthorizedException(this);
		}
	}

	public Long getMaxAgeSeconds(Auth auth) {
		LOGGER.debug("Get Max Age for {} ", auth);
		Session session = (Session) auth.getTag();
		if (session == null || User.ANON_USER.equals(session.getUserId())) {
			return LONG_MAX_AGE;
		}
		try {
			User anonUser = (User) session.getAuthorizableManager()
					.findAuthorizable(User.ANON_USER);
			if (session.getAccessControlManager().can(anonUser,
					Security.ZONE_CONTENT, path, Permissions.CAN_READ)) {
				return LONG_MAX_AGE;
			}
		} catch (StorageClientException e) {
			LOGGER.error(e.getMessage(), e);
		} catch (AccessDeniedException e) {
			LOGGER.error(e.getMessage(), e);
		}
		return null;
	}

	public String getContentType(String accepts) {
		LOGGER.debug("Get Content type for {} ", content);
		if (content == null) {
			return null;
		}
		String contentType = (String) content
				.getProperty(Content.MIMETYPE_FIELD);
		if (contentType == null) {
			return null;
		}
		return ContentTypeUtils.findAcceptableContentType(contentType, accepts);
	}

	public Long getContentLength() {
		LOGGER.debug("Get Content length {} ", content);
		if (content == null) {
			return null;
		}
		return (Long) content.getProperty(Content.LENGTH_FIELD);
	}

	public void moveTo(CollectionResource rDest, String name)
			throws ConflictException, NotAuthorizedException,
			BadRequestException {
		try {
			String sourcePath = path;
			String destPath = StorageClientUtils.newPath(
					((SparseMiltonContentResource) rDest).getPath(), name);
			LOGGER.debug(
					"====================================== Moving from {} to {} ",
					sourcePath, destPath);
			session.getContentManager().moveWithChildren(sourcePath, destPath);
		} catch (StorageClientException e) {
			LOGGER.error(e.getMessage(), e);
			throw new ConflictException(this, e.getMessage());
		} catch (AccessDeniedException e) {
			LOGGER.error(e.getMessage(), e);
			throw new NotAuthorizedException(rDest);
		}
	}

	public String processForm(Map<String, String> parameters,
			Map<String, FileItem> files) throws BadRequestException,
			NotAuthorizedException, ConflictException {
		LOGGER.info("Process form {} {} ", parameters, files);
		// TODO Auto-generated method stub
		return null;
	}

	public Date getCreateDate() {
		if (content == null) {
			return null;
		}
		Long created = (Long) content.getProperty(Content.CREATED_FIELD);
		if (created != null) {
			return new Date(created);
		}
		return new Date();
	}

	public CollectionResource createCollection(String newName)
			throws NotAuthorizedException, ConflictException,
			BadRequestException {
		LOGGER.debug("Create Collection ", newName);
		try {
			String newPath = StorageClientUtils.newPath(path, newName);
			ContentManager contentManager = session.getContentManager();
			Content newContent = contentManager.get(newPath);
			if (newContent != null) {
				throw new ConflictException(this, "Collection already exists");
			}
			newContent = new Content(newPath, ImmutableMap.of("collection",
					(Object) true));
			contentManager.update(newContent);
			newContent = contentManager.get(newPath);
			return new SparseMiltonContentResource(newPath, session, newContent);
		} catch (StorageClientException e) {
			throw new BadRequestException(this, e.getMessage());
		} catch (AccessDeniedException e) {
			throw new NotAuthorizedException(this);
		}
	}

	public Resource child(String childName) {
		LOGGER.debug("Get Child ", childName);
		try {
			String newPath = StorageClientUtils.newPath(path, childName);
			Content c = session.getContentManager().get(newPath);
			if (c != null) {
				return new SparseMiltonContentResource(newPath, session, c);
			}
		} catch (StorageClientException e) {
			LOGGER.error(e.getMessage(), e);
		} catch (AccessDeniedException e) {
			LOGGER.error(e.getMessage(), e);
		}
		return null;
	}

	public List<? extends Resource> getChildren() {
		// this needs to be disposed by the system.
		LOGGER.debug("Get Children ");
		final Iterator<Content> children = content.listChildren().iterator();
		return ImmutableList.copyOf(new PreemptiveIterator<SparseMiltonContentResource>() {

					private SparseMiltonContentResource resource;

					@Override
					protected boolean internalHasNext() {
						while (children.hasNext()) {
							Content n = children.next();
							if (n != null) {
								resource = new SparseMiltonContentResource(n
										.getPath(), session, n);
								return true;
							}
						}
						resource = null;
						return false;
					}

					@Override
					protected SparseMiltonContentResource internalNext() {
						return resource;
					}

				});
	}

	public Resource createNew(String newName, InputStream inputStream,
			Long length, String contentType) throws IOException,
			ConflictException, NotAuthorizedException, BadRequestException {
		LOGGER.debug("Create new {} ", newName);
		try {
			String newPath = StorageClientUtils.newPath(path, newName);
			ContentManager contentManager = session.getContentManager();
			Content newContent = contentManager.get(newPath);
			if (newContent == null) {
				newContent = new Content(newPath, ImmutableMap.of(
						Content.MIMETYPE_FIELD, (Object) contentType));
			} else {
				newContent.setProperty(Content.MIMETYPE_FIELD,
						(Object) contentType);
			}
			contentManager.update(newContent);
			contentManager.writeBody(newPath, inputStream);
			newContent = contentManager.get(newPath);
			return new SparseMiltonContentResource(newPath, session, newContent);
		} catch (StorageClientException e) {
			throw new BadRequestException(this, e.getMessage());
		} catch (AccessDeniedException e) {
			throw new NotAuthorizedException(this);
		}
	}

	// ========= PROPFIND PROPPATCH Support ===============================

	public Object getProperty(QName name) {
		String n = getFullName(name);
		Object o = content.getProperty(n);
		LOGGER.debug("-------------- GETTING {} as {} --------------", n, o);
		return o;
	}

	public void setProperty(QName name, Object value)
			throws PropertySetException, NotAuthorizedException {
		String n = getFullName(name);
		if (value == null) {
			LOGGER.debug("-------------- REMOVING {} --------------", n);
			content.removeProperty(n);
		} else {
			LOGGER.debug("-------------- SETTING {} as {} --------------", n,
					value);
			content.setProperty(n, value);
		}
		session.addCommitHandler(content.getId(), new CommitHandler() {

			public void commit() {
				try {
					session.getContentManager().update(content);
				} catch (AccessDeniedException e) {
					LOGGER.error(e.getMessage());
				} catch (StorageClientException e) {
					LOGGER.error(e.getMessage(), e);
				}
			}
		});
	}

	public PropertyMetaData getPropertyMetaData(QName name) {
		if (authorizedToWriteProperties) {
			return new PropertyMetaData(PropertyAccessibility.WRITABLE,
					Object.class);
		} else if (content.hasProperty(getFullName(name))) {
			return new PropertyMetaData(PropertyAccessibility.WRITABLE,
					Object.class);
		}
		return null;
	}

	private String getFullName(QName name) {
		String nameSpace = name.getNamespaceURI();
		if (nameSpace == null || nameSpace.length() == 0) {
			return "{None}" + name.getLocalPart();
		}
		return name.toString();
	}

	public List<QName> getAllPropertyNames() {
		List<QName> l = Lists.newArrayList();
		for (Entry<String, Object> p : content.getProperties().entrySet()) {
			String name = p.getKey();
			if (name.startsWith("{")) {
				int i = name.indexOf('}');
				l.add(new QName(name.substring(1, i - 1), name.substring(i + 1)));
			} else {
				l.add(new QName(name));
			}
		}
		return l;
	}

	// ========= LOCK Support ===============================

	public LockToken createAndLock(String name, LockTimeout timeout,
			LockInfo lockInfo) throws NotAuthorizedException {
		LOGGER.debug("Create And Lock {} {} ", timeout, lockInfo);

		try {
			String newPath = StorageClientUtils.newPath(path, name);
			LockHolder lockHolder = new LockHolder(lockInfo, timeout);
			String token = session.getLockManager().lock(newPath,
					lockHolder.getTimeoutInSeconds(), lockHolder.toString());
			return new LockToken(token, lockInfo, timeout);
		} catch (StorageClientException e) {
			LOGGER.error(e.getMessage(), e);
			throw new NotAuthorizedException(this);
		} catch (AlreadyLockedException e) {
			LOGGER.error(e.getMessage(), e);
			throw new NotAuthorizedException(this);
		}
	}

	public LockResult lock(LockTimeout timeout, LockInfo lockInfo)
			throws NotAuthorizedException, PreConditionFailedException,
			LockedException {
		try {
			LockHolder lockHolder = new LockHolder(lockInfo, timeout);
			String token = session.getLockManager().lock(path,
					lockHolder.getTimeoutInSeconds(), lockHolder.toString());
			return LockResult.success(new LockToken(token, lockInfo, timeout));
		} catch (AlreadyLockedException e) {
			return LockResult.failed(FailureReason.ALREADY_LOCKED);
		} catch (StorageClientException e) {
			LOGGER.error(e.getMessage(), e);
			throw new NotAuthorizedException(this);
		}
	}

	public LockResult refreshLock(String token) throws NotAuthorizedException,
			PreConditionFailedException {
		try {
			LockState lockState = session.getLockManager().getLockState(path,
					token);
			LOGGER.debug("Refreshing lock with {} gave {} ", token, lockState);
			if (lockState.isOwner() && lockState.hasMatchedToken()
					&& path.equals(lockState.getLockPath())) {
				LOGGER.debug("Refreshing lock ");
				LockHolder lock = new LockHolder(lockState.getExtra(), false);
				session.getLockManager().refreshLock(path,
						lock.getTimeoutInSeconds(), lock.toString(), token);
				LockInfo lockInfo = lock.getLockInfo();
				LockTimeout timeout = lock.getLockTimeout();
				return LockResult.success(new LockToken(token, lockInfo,
						timeout));
			}
			LOGGER.debug("Not Refreshing Lock");
		} catch (StorageClientException e) {
			LOGGER.error(e.getMessage(), e);
			throw new NotAuthorizedException(this);
		}
		throw new PreConditionFailedException(this);
	}

	public void unlock(String tokenId) throws NotAuthorizedException,
			PreConditionFailedException {
		try {
			LockState lockState = session.getLockManager().getLockState(path,
					tokenId);
			if (lockState.isOwner() && lockState.hasMatchedToken()
					&& path.equals(lockState.getLockPath())) {
				session.getLockManager().unlock(path, tokenId);
				return;
			}
		} catch (StorageClientException e) {
			throw new NotAuthorizedException(this);
		}
		throw new PreConditionFailedException(this);
	}

	public LockToken getCurrentLock() {
		try {
			// get the lock regardless, the handlers should deal with locking.
			LockState lockState = session.getLockManager().getLockState(
					path, "unknown");
			if (lockState.isLocked()) {
				LOGGER.debug(" getCurrentLock() Found Lock {} {} ", path,
						lockState);
				String extra = lockState.getExtra();
				String token = lockState.getToken();
				LockHolder lockHolder = new LockHolder(extra, true);
				LockToken lockToken = new LockToken(token, lockHolder.getLockInfo(),
						lockHolder.getLockTimeout());
				return lockToken;
			}
			LOGGER.debug("No Lock Present at {} ", path);
		} catch (StorageClientException e) {
			LOGGER.error(e.getMessage(), e);
		}
		return null;
	}

}


package org.intellimate.izou.sdk.events;

import org.intellimate.izou.events.EventBehaviourControllerModel;
import org.intellimate.izou.identification.Identification;

import java.util.HashMap;
import java.util.List;
import java.util.function.Function;

/**
 * This class can control the Behaviour of the the Event, like which Output-Plugin should get the Event first.
 */
public class EventBehaviourController implements EventBehaviourControllerModel {

    private Function<List<Identification>, HashMap<Integer, List<Identification>>> outputPluginBehaviour;

    /**
     * this method sets the controls for the Output-Plugin Behaviour.
     * <p>
     * Supply a Function, which controls the OutputPlugin-Behaviour. You can set Priorities.
     * The output-plugin with the highest POSITIVE priority (in int) will be processed first. Negative priorities
     * are processed last (so outputPlugins with no priorities will be processed in between positive and negative
     * priorities)
     *
     * This function returns an HashMap, where the keys represent the associated Behaviour
     * and the values the Identification;
     * </p>
     * @param outputPluginBehaviour all the registered outputPlugins
     */
    public void controlOutputPluginBehaviour(Function<List<Identification>,
            HashMap<Integer, List<Identification>>> outputPluginBehaviour) {
        this.outputPluginBehaviour = outputPluginBehaviour;
    }

    /**
     * generates the data to control the Event
     * @param identifications the Identifications of the OutputPlugins
     * @return a HashMap, where the keys represent the associated Behaviour and the values the Identification;
     */
    @Override
    public HashMap<Integer, List<Identification>> getOutputPluginBehaviour(List<Identification> identifications) {
        if(outputPluginBehaviour == null) return new HashMap<>();
        return outputPluginBehaviour.apply(identifications);
    }
}