/**
 * $RCSfile$
 * $Revision: $
 * $Date: $
 *
 * Copyright (C) 2006 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
 */

package org.jivesoftware.multiplexer;

import org.dom4j.Element;
import org.jivesoftware.multiplexer.net.SocketConnection;
import org.jivesoftware.util.Log;

/**
 * A ServerPacketHandler is responsible for handling stanzas sent from the server. For each
 * server connection there is going to be an instance of this class.<p>
 *
 * Route stanzas are forwarded to clients. IQ stanzas are used when the server wants to
 * close a client connection or wants to update the clients connections configurations.
 * Stream errors with condition <tt>system-shutdown</tt> indicate that the server is
 * shutting down. The connection manager will close existing client connections but
 * will keep running.
 *
 * @author Gaston Dombiak
 */
class ServerPacketHandler {

    private ConnectionManager connectionManager = ConnectionManager.getInstance();

    /**
     * Connection to the server.
     */
    private SocketConnection connection;
    /**
     * JID that identifies this connection to the server. The address is composed by
     * the connection manager name and the name of the thread. e.g.: connManager1/thread1
     */
    private String jidAddress;

    public ServerPacketHandler(SocketConnection connection, String jidAddress) {
        this.connection = connection;
        this.jidAddress = jidAddress;
    }

    /**
     * Handles stanza sent from the server. Route stanzas are forwarded to clients. IQ
     * stanzas are used when the server wants to close a client connection or wants to
     * update the clients connections configurations. Stream errors with condition
     * <tt>system-shutdown</tt> indicate that the server is shutting down. The connection
     * manager will close existing client connections but will keep running.
     *
     * @param stanza stanza sent from the server.
     */
    public void handle(Element stanza) {
        String tag = stanza.getName();
        if ("route".equals(tag)) {
            // Process wrapped packets
            processRoute(stanza);
        }
        else if ("iq".equals(tag)) {
            String type = stanza.attributeValue("type");
            if ("set".equals(type)) {
                Element wrapper = stanza.element("session");
                if (wrapper != null) {
                    String streamID = wrapper.attributeValue("id");
                    // Check if the server is informing us that we need to close a session
                    if (wrapper.element("close") != null) {
                        // Get the session that matches the requested stream ID
                        Session session = Session.getSession(streamID);
                        if (session != null) {
                            session.close();
                        }
                    } else {
                        Log.warn("Invalid IQ stanza of type SET was received: " + stanza.asXML());
                    }
                } else {
                    Element configuration = stanza.element("configuration");
                    if (configuration != null) {
                        obtainClientOptions(stanza, configuration);
                    } else {
                        Log.warn("Invalid IQ stanza of type SET was received: " + stanza.asXML());
                    }
                }
            } else if ("result".equals(type)) {
                if (Log.isDebugEnabled()) {
                    Log.debug("IQ stanza of type RESULT was discarded: " + stanza.asXML());
                }
            } else {
                Log.warn("IQ stanza with invalid type was discarded: " + stanza.asXML());
            }
        } else if ("error".equals(tag) && "stream".equals(stanza.getNamespacePrefix())) {
            if (stanza.element("system-shutdown") != null) {
                // Close connections to the server and client connections. The connection
                // manager will still be running and accepting client connections. New
                // connections to the server will be created on demand.
                connectionManager.getServerSurrogate().closeAll();
            } else {
                // Some stream error was sent from the server
                Log.warn("Server sent unexpected stream error: " + stanza.asXML());
            }
        } else {
            Log.warn("Unknown stanza type sent to Connection Manager: " + stanza.asXML());
        }
    }

    /**
     * Forwards wrapped stanza contained in the <tt>route</tt> element to the specified
     * client. The target client connection is specified in the <tt>route</tt> element by
     * the <tt>streamid</tt> attribute.<p>
     *
     * Wrapped stanzas that failed to be delivered to the target client are returned to
     * the server.
     *
     * @param route the route element containing the wrapped stanza to send to the target
     *        client.
     */
    private void processRoute(Element route) {
        String streamID = route.attributeValue("streamid");
        // Get the wrapped stanza
        Element stanza = (Element) route.elementIterator().next();
        // Get the session that matches the requested stream ID
        Session session = Session.getSession(streamID);
        if (session != null && !session.isClosed()) {
            // Deliver the wrapped stanza to the client
            session.deliver(stanza);
        }
        else {
            // Inform the server that the wrapped stanza was not delivered
            String tag = stanza.getName();
            if ("message".equals(tag)) {
                connectionManager.getServerSurrogate().deliveryFailed(stanza, streamID);
            }
            else if ("iq".equals(tag)) {
                String type = stanza.attributeValue("type", "get");
                if ("get".equals(type) || "set".equals(type)) {
                    // Build IQ of type ERROR
                    Element reply = stanza.createCopy();
                    reply.addAttribute("type", "error");
                    reply.addAttribute("from", stanza.attributeValue("to"));
                    reply.addAttribute("to", stanza.attributeValue("from"));
                    Element error = reply.addElement("error");
                    error.addAttribute("type", "wait");
                    error.addElement("unexpected-request")
                            .addAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-stanzas");
                    // Bounce the failed IQ packet
                    connectionManager.getServerSurrogate().send(reply.asXML(), streamID);
                }
            }
        }
    }

    /**
     * Processes server configuration to use for client connections and store the
     * configuration in {@link ServerSurrogate}.
     *
     * @param stanza stanza sent from the server containing the configuration.
     * @param configuration the configuration element contained in the stanza.
     */
    private void obtainClientOptions(Element stanza, Element configuration) {
        ServerSurrogate serverSurrogate = connectionManager.getServerSurrogate();
        // Check if TLS is avaiable (and if it is required)
        Element startTLS = configuration.element("starttls");
        if (startTLS != null) {
            if (startTLS.element("required") != null) {
                serverSurrogate.setTlsPolicy(Connection.TLSPolicy.required);
            } else {
                serverSurrogate.setTlsPolicy(Connection.TLSPolicy.optional);
            }
        } else {
            serverSurrogate.setTlsPolicy(Connection.TLSPolicy.disabled);
        }
        // Check if compression is available
        Element compression = configuration.element("compression");
        if (compression != null) {
            serverSurrogate.setCompressionPolicy(Connection.CompressionPolicy.optional);
        } else {
            serverSurrogate.setCompressionPolicy(Connection.CompressionPolicy.disabled);
        }
        // Cache supported SASL mechanisms for client authentication
        Element mechanisms = configuration.element("mechanisms");
        if (mechanisms != null) {
            serverSurrogate.setSASLMechanisms(mechanisms);
        }
        // Check if anonymous login is supported
        serverSurrogate.setNonSASLAuthEnabled(configuration.element("auth") != null);
        // Check if in-band registration is supported
        serverSurrogate.setInbandRegEnabled(configuration.element("register") != null);

        // Send ACK to the server
        Element reply = stanza.createCopy();
        reply.addAttribute("type", "result");
        reply.addAttribute("to", connectionManager.getServerName());
        reply.addAttribute("from", jidAddress);
        connection.deliver(reply.asXML());
    }
}

