package org.jacorb.orb.giop;

/*
 *        JacORB - a free Java ORB
 *
 *   Copyright (C) 1997-2014 Gerald Brose / The JacORB Team.
 *
 *   This library is free software; you can redistribute it and/or
 *   modify it under the terms of the GNU Library General Public
 *   License as published by the Free Software Foundation; either
 *   version 2 of the License, or (at your option) any later version.
 *
 *   This library is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *   Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this library; if not, write to the Free
 *   Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.jacorb.config.Configurable;
import org.jacorb.config.Configuration;
import org.jacorb.config.ConfigurationException;
import org.jacorb.orb.DefaultProfileSelector;
import org.jacorb.orb.ProfileSelector;
import org.jacorb.orb.diop.DIOPFactories;
import org.jacorb.orb.etf.ListenEndpoint;
import org.jacorb.orb.etf.ListenEndpoint.Protocol;
import org.jacorb.orb.etf.ProtocolAddressBase;
import org.jacorb.orb.factory.SocketFactoryManager;
import org.jacorb.orb.giop.TransportListener.Event;
import org.jacorb.orb.iiop.IIOPAddress;
import org.jacorb.util.ObjectUtil;
import org.omg.CORBA.BAD_PARAM;
import org.omg.ETF.Factories;
import org.slf4j.Logger;

/**
 * This class manages Transports. On the one hand it creates them, and
 * on the other it enforces an upper limit on the open transports.
 *
 * The class also receives notifications from threads that are about do use a
 * Transport and notifies any interested listeners. "Use" is defined as
 * sending (or handling) a request.
 *
 * @author Nicolas Noffke
 * */

public class TransportManager
    implements Configurable
{
    /** the configuration object  */
    private org.jacorb.config.Configuration configuration = null;

    /** configuration properties */
    private Logger logger = null;
    private List<String> factoryClassNames = null;
    private ProfileSelector profileSelector = null;
    private final SocketFactoryManager socketFactoryManager;

    /**
     * Denotes whether to use NIO for IIOP transport.
     */
    private boolean useNonBlockingIIOPTransport = false;

    /**
     * Maps ETF Profile tags (Integer) to ETF Factories objects.
     */
    private Map<Integer,Factories>  factoriesMap  = null;

    /**
     * List of all installed ETF Factories.  This list contains an
     * instance of each Factories class, ordered in the same way as
     * they were specified in the jacorb.transport.factories property.
     */
    private List<Factories> factoriesList = null;

    /**
     * List of all IIOP/SLIOP endpoint address-pairs
     */
    private HashMap<Protocol,ArrayList<ListenEndpoint>> listenEndpointList = null;


    /**
     * The first listener (in a chain of instances), representing
     * parties with interest in Transport events.
     */
    private TransportListener listener = null;

    public TransportManager( )
    {
        socketFactoryManager = new SocketFactoryManager();
    }

    @Override
    public void configure(Configuration myConfiguration)
    		throws ConfigurationException
    {
        configuration = myConfiguration;
        logger = configuration.getLogger("org.jacorb.orb.giop");
        useNonBlockingIIOPTransport = configuration.getAttributeAsBoolean
            ("jacorb.connection.nonblocking", false);

        socketFactoryManager.configure(configuration);

        // get factory class names
        factoryClassNames = configuration.getAttributeList("jacorb.transport.factories");

        if (factoryClassNames.isEmpty())
        {
           factoryClassNames.add("org.jacorb.orb.iiop.IIOPFactories");
        }

        // pickup listen endpoints specified by arguments -ORBListenEndpoints
        // and populate the array list listenEndpointList
        updateListenEndpointAddresses();

        // pickup the default OAAdress/OASSLAddress from prop file
        // and add them to the end of the array list listenEndpointList as well.
        updateDefaultEndpointAddresses();

        // get profile selector info
        profileSelector =
            (ProfileSelector)configuration.getAttributeAsObject("jacorb.transport.client.selector");

        if (profileSelector == null)
        {
            profileSelector = new DefaultProfileSelector();
        }
    }

    public ProfileSelector getProfileSelector()
    {
        return profileSelector;
    }

    public SocketFactoryManager getSocketFactoryManager()
    {
        return socketFactoryManager;
    }

    /**
     * Returns an ETF Factories object for the given tag, or null
     * if no Factories class has been defined for this tag.
     */
    public synchronized org.omg.ETF.Factories getFactories(int tag)
    {
        // This isn't ideal. If DIOPFactories was a full implementation then
        // this class should be added to the
        // TransportManager::loadFactories. This shortcut block (which is used
        // by ParsedIOR) and the static caching in DIOPFactories wouldn't be
        // needed.
        if (tag == DIOPFactories.TAG_DIOP_UDP)
        {
            return DIOPFactories.getDIOPFactory();
        }

        if (factoriesMap == null)
        {
            loadFactories();
        }
        return factoriesMap.get (tag);
    }

    /**
     * Returns a list of Factories for all configured transport plugins,
     * in the same order as they were specified in the
     * jacorb.transport.factories property.
     */
    public synchronized List<Factories> getFactoriesList()
    {
        if (factoriesList == null)
        {
            loadFactories();
        }
        return Collections.unmodifiableList(factoriesList);
    }


    public ArrayList<ListenEndpoint> getListenEndpoints (Protocol p)
    {
        ArrayList<ListenEndpoint> endpoints = listenEndpointList.get (p);

        if (endpoints == null)
        {
            if (logger.isDebugEnabled())
            {
                logger.debug ("Unable to find endpoints for " + p);
            }
            ListenEndpoint l = new ListenEndpoint();
            l.setProtocol(p);
            endpoints = new ArrayList<ListenEndpoint>();
            endpoints.add(l);
            listenEndpointList.put(p, endpoints);
        }

        return endpoints;
    }

    /**
     * Build the factoriesMap and factoriesList.
     */
    private void loadFactories()
    {
        if (configuration == null )
        {
            throw new org.omg.CORBA.BAD_INV_ORDER("TransportManager not configured!");
        }

        if (factoryClassNames == null )
        {
            throw new org.omg.CORBA.INTERNAL("factoryClassNames may not be null");
        }

        factoriesMap  = new HashMap<Integer,Factories>();
        factoriesList = new ArrayList<Factories>();

        for (Iterator<String> i = factoryClassNames.iterator(); i.hasNext();)
        {
            String className = i.next();

            Factories factories = instantiateFactories(className);
            factoriesMap.put(factories.profile_tag(), factories); // NOPMD
            factoriesList.add (factories);
        }
    }

    /**
     * Instantiates the given Factories class.
     */
    private org.omg.ETF.Factories instantiateFactories (String className)
    {
        try
        {
           if (useNonBlockingIIOPTransport &&
               "org.jacorb.orb.iiop.IIOPFactories".equals (className))
           {
              className = "org.jacorb.orb.nio.NIOFactories";
           }

            // ObjectUtil.classForName() uses the context class loader.
            // This is important here because JacORB might be on the
            // bootclasspath, and the external transport on the normal
            // classpath.
            Class<?> clazz = ObjectUtil.classForName(className);
            Object instance = clazz.newInstance();

            if (instance instanceof Configurable)
            {
                Configurable configurable = (Configurable)instance;
                configurable.configure(configuration);
            }

            logger.debug("created org.omg.ETF.Factories: " + className);

            return (Factories)instance;
        }
        catch (Exception e)
        {
            throw new BAD_PARAM
                ("could not instantiate Factories class " + className
                 + ", exception: " + e);
        }
    }

    public void notifyTransportListeners(GIOPConnection giopc)
    {
        if (listener != null)
        {
            listener.transportSelected (new Event (giopc));
        }
    }

    public void addTransportListener(TransportListener tl)
    {

        if (logger.isInfoEnabled ())
        {
            logger.info ("Transport listener to add: " + tl);
        }

        if (tl != null)
        {
            addTransportListenerImpl (tl);
        }
    }

    private synchronized void addTransportListenerImpl(final TransportListener tl)
    {
        if (listener == null)
        {
            listener = tl;
        }
        else
        {
            listener = new TransportListener ()
            {

                private final TransportListener next_ = listener;

                @Override
                public void transportSelected(Event event)
                {
                    try
                    {
                        tl.transportSelected (event);
                    }
                    finally
                    {
                        next_.transportSelected (event);
                    }
                }
            };
        }
    }

    private ProtocolAddressBase createProtocolAddress(String address_str)
    {
        final IIOPAddress address = new IIOPAddress();

        int proto_delim = address_str.indexOf (':');
        Protocol proto;
        try
        {
            proto = Protocol.valueOf(address_str.substring (0,proto_delim).toUpperCase(Locale.ENGLISH));
        }
        catch (IllegalArgumentException e)
        {
            throw new BAD_PARAM("Invalid protocol " + address_str);
        }
        address.setProtocol(proto);
        final int addresss_start_ofs = proto_delim + 3;

        if (!address.fromString(address_str.substring(addresss_start_ofs)))
        {
            throw new org.omg.CORBA.INTERNAL("Invalid protocol address string: " + address_str);
        }

        // set protocol string
        return address;
    }

    /**
     * Pick default OAAdress/OASSLAddress pair from property file
     * and add them to the list of endpoint address list.
     */
    private void updateDefaultEndpointAddresses() throws ConfigurationException
    {
        IIOPAddress address = null;
        IIOPAddress ssl_address = null;


        // If we already have an endpoint list defined there is no point creating a default.
        if (listenEndpointList != null && listenEndpointList.size() > 0)
        {
            return;
        }

        ListenEndpoint defaultEndpoint = new ListenEndpoint();

        String address_str = configuration.getAttribute("OAAddress",null);
        if (address_str != null)
        {
            // build an iiop/ssliop protocol address.
            // create_protocol_address will allow iiop and ssliop only
            ProtocolAddressBase addr = createProtocolAddress(address_str);
            address = (IIOPAddress)addr;
            address.configure(configuration);
        }
        else
        {
            int oaPort = configuration.getAttributeAsInteger("OAPort",0);
            String oaHost = configuration.getAttribute("OAIAddr","");
            address = new IIOPAddress(oaHost,oaPort);
            address.configure(configuration);
        }

        String ssl_address_str = configuration.getAttribute("OASSLAddress",null);
        if (ssl_address_str != null)
        {
            // build a protocol address
            ProtocolAddressBase ssl_addr = createProtocolAddress(ssl_address_str);
            ssl_address = (IIOPAddress)ssl_addr;
            ssl_address.configure(configuration);

        }
        else
        {
            int ssl_oaPort = configuration.getAttributeAsInteger("OASSLPort",0);
            String ssl_oaHost = configuration.getAttribute("OAIAddr","");
            ssl_address = new IIOPAddress(ssl_oaHost,ssl_oaPort);
            ssl_address.configure(configuration);
        }

        if (address.getProtocol() == null)
        {
            address.setProtocol(Protocol.IIOP);
        }
        if (ssl_address.getProtocol() == null || ssl_address.getProtocol() == Protocol.SSLIOP)
        {
            ssl_address.setProtocol(Protocol.IIOP);
        }
        defaultEndpoint.setAddress(address);
        defaultEndpoint.setSSLAddress(ssl_address);
        defaultEndpoint.setProtocol(address.getProtocol());

        ArrayList<ListenEndpoint> s = listenEndpointList.get(address.getProtocol());
        s = new ArrayList<ListenEndpoint>();
        s.add (defaultEndpoint);
        listenEndpointList.put(address.getProtocol(), s);
    }

    /**
     * Pickup endpoint addresses from command-line arguments -ORBListenEndPoints
     */
    private void updateListenEndpointAddresses() throws ConfigurationException
    {
        listenEndpointList = new HashMap <Protocol, ArrayList<ListenEndpoint>>();

        // get original argument list from ORB
        String[] args = configuration.getORB().getArgs();

        if (args != null)
        {
            for (int i = 0; i < args.length; i++)
            {
                if (args[i] == null) {
                    continue;
                }

                if (!args[i].equalsIgnoreCase("-ORBListenEndpoints"))
                {
                    continue;
                }

                if (i+1 >= args.length || args[i+1] == null)
                {
                    throw new BAD_PARAM("Invalid ORBListenEndpoint <value> format: -ORBListenEndpoints argument without value" );
                }

                String ep_args = args[i+1];
                String ep_args_trim = ep_args.trim();

                //check and remove single quotes if needed
                if (ep_args_trim.charAt(0) == '\'' &&
                        ep_args_trim.charAt(ep_args_trim.length()-1) == '\'')
                {
                    ep_args_trim = ep_args.trim().substring(1,ep_args.trim().length()-1);
                }

                // split up argument into segments using the semi-clone as delimiters
                String[] seg_addr_list = ep_args_trim.split(";");

                for (int xx = 0; xx < seg_addr_list.length; xx++)
                {
                    String seg_args = seg_addr_list[xx].trim();

                    if (seg_args.equals(""))
                    {
                        continue;
                    }

                    // split up group of args into individual arg segments
                    // using the coma as delimiters
                    String[] indiv_list = seg_args.trim().split(",");
                    for (int xxx = 0; xxx < indiv_list.length; xxx++)
                    {
                        String address_str = null;
                        String ssl_port = null;
                        String host_str = "";
                        String[] options_args = null;

                        String addr_arg = indiv_list[xxx].trim();
                        if (addr_arg.equals(""))
                        {
                            continue;
                        }

                        // locate the first colon delimiter and
                        // pickup the protocol identifier string
                        int delim = addr_arg.indexOf(":");
                        Protocol protocol;
                        try
                        {
                            protocol = Protocol.valueOf (addr_arg.substring (0,delim).toUpperCase(Locale.ENGLISH));
                        }
                        catch (IllegalArgumentException e)
                        {
                            throw new BAD_PARAM("Invalid ORBListenEndPoints protocol " + addr_arg);
                        }

                        // locate the double slash delimiter
                        int db_slash = addr_arg.indexOf("//", delim+1);
                        if (db_slash == -1)
                        {
                            throw new BAD_PARAM("Invalid ORBListenEndPoints <value;value;...> format: listen endpoint \'" + addr_arg + "\' is malformed!" );
                        }

                        // check if additional option delimiter is present
                        // and pick up the protocol address
                        String dbs = "/";
                        if (protocol == Protocol.UIOP)
                        {
                            dbs = "|";
                        }
                        int opt_slash = addr_arg.indexOf(dbs, db_slash + 2);
                        if (opt_slash == -1)
                        {
                            address_str = addr_arg.substring(0);

                        }
                        else
                        {
                            address_str = addr_arg.substring(0, opt_slash);
                        }

                        // pick up optional arguments if present
                        if (opt_slash != -1)
                        {
                            options_args = addr_arg.substring(opt_slash+1).split("&");
                            for (int y = 0; y < options_args.length; y++)
                            {
                                String options_args_trim = options_args[xxx].trim();

                                int opt_delim = options_args_trim.indexOf('=');
                                if(opt_delim == -1)
                                {
                                    throw new BAD_PARAM("error: listen endpoint options \'" + options_args[y] + "\' is malformed!");
                                }
                                else
                                {
                                    String opt_str = options_args_trim.substring(0, opt_delim);
                                    String opt_value = options_args_trim.substring(opt_delim+1);

                                    if(opt_str.equalsIgnoreCase("ssl_port"))
                                    {
                                        ssl_port = opt_value;
                                    }
                                    else
                                    {
                                        throw new BAD_PARAM("error: listen endpoint options \'" + options_args[y] + "\' is not supported!");
                                    }
                                }

                            }
                        }

                        if(address_str != null)
                        {
                            String address_trim = address_str.trim();
                            // build an iiop/ssliop protocol address.
                            // create_protocol_address will allow iiop and ssliop only
                            IIOPAddress address = null;
                            IIOPAddress ssl_address = null;
                            if (protocol == Protocol.IIOP)
                            {
                                ProtocolAddressBase addr1 = createProtocolAddress(address_trim);
                                if (addr1 instanceof IIOPAddress)
                                {
                                    address = (IIOPAddress)addr1;
                                    address.configure(configuration);
                                }

                                if (ssl_port != null)
                                {
                                    int colon_delim = address_trim.indexOf(":");
                                    int port_delim = address_trim.indexOf(":", colon_delim+2);
                                    if (port_delim > 0)
                                    {
                                        host_str = address_trim.substring(colon_delim+3, port_delim);
                                    }
                                    else
                                    {
                                        host_str = "";
                                    }

                                    ssl_address = new IIOPAddress(host_str,Integer.parseInt(ssl_port));
                                    ssl_address.configure(configuration);
                                }

                            }
                            else if(protocol == Protocol.SSLIOP)
                            {
                                ProtocolAddressBase addr2 = createProtocolAddress(address_trim);
                                if (addr2 instanceof IIOPAddress)
                                {
                                    ssl_address = (IIOPAddress)addr2;
                                    ssl_address.configure(configuration);
                                }

                                //  Set the protocol to IIOP for using IIOP Protocol Factory
                                protocol = Protocol.IIOP;
                            }
                            else
                            {
                                ProtocolAddressBase addr1 = createProtocolAddress(address_trim);
                                if (addr1 instanceof IIOPAddress)
                                {
                                    address = (IIOPAddress)addr1;
                                    address.configure(configuration);
                                }
                            }

                            if (configuration.getAttributeAsBoolean("jacorb.security.support_ssl", false) &&
                                ssl_address == null)
                            {
                                throw new org.omg.CORBA.BAD_PARAM
                                    ("Error: an SSL port (ssl_port) is required when property jacorb.security.support_ssl is enabled");
                            }

                            ListenEndpoint listen_ep = new ListenEndpoint();
                            listen_ep.setAddress(address);
                            listen_ep.setSSLAddress(ssl_address);
                            listen_ep.setProtocol(protocol);

                            ArrayList<ListenEndpoint> s = listenEndpointList.get(protocol);
                            if ( s == null)
                            {
                                s = new ArrayList<ListenEndpoint>();
                                listenEndpointList.put(protocol, s);
                            }
                            s.add(listen_ep);
                        }
                    } // end for - inner
                } //end for - outter
            } //end for
        } // end if
    }
}
