Package org.jboss.mx.remote.server

Source Code of org.jboss.mx.remote.server.CascadingAgent$Connector

package org.jboss.mx.remote.server;

import java.lang.reflect.Proxy;
import java.net.InetAddress;
import java.util.*;

import javax.management.AttributeChangeNotification;
import javax.management.DynamicMBean;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanServer;
import javax.management.MBeanServerNotification;
import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.Query;
import javax.management.QueryExp;

import EDU.oswego.cs.dl.util.concurrent.SynchronizedLong;
import org.jboss.mx.remote.*;
import org.jboss.mx.remote.connector.ConnectorFactory;
import org.jboss.mx.remote.discovery.DetectionNotification;
import org.jboss.mx.remote.event.ClassEventFilter;
import org.jboss.mx.remote.event.CompositeEventFilter;
import org.jboss.mx.remote.event.LocalMBeanEventFilter;
import org.jboss.mx.remote.event.CompositeQueryExp;
import org.jboss.mx.util.ProxyContext;


/**
* CascadingAgent is responsible for exposing Remote MBeanServers and MBeans to the
* local MBeanServer as if they were local MBeans.
*
* @author <a href="mailto:telrod@e2technologies.net">Tom Elrod</a>
* @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
* @version $Revision: 1.13 $
*/
public class CascadingAgent extends RemoteServiceMBeanSupport
        implements CascadingAgentMBean, NotificationListener
{
    private String transports[];
    private String transportslist;
    private Map partitions = new HashMap(1);
    private String partitionslist = DetectionNotification.DEFAULT_PARTITION;
    private final Map connectors = Collections.synchronizedMap(new HashMap());
    private final Map objectmap = Collections.synchronizedMap(new HashMap());
    private final List querylist = Collections.synchronizedList(new ArrayList(2));
    private QueryExp query;
    private final Object LOCK = new Object();
    protected final SynchronizedLong attributeSequence = new SynchronizedLong(0);

    public CascadingAgent()
    {
        partitions.put(DetectionNotification.DEFAULT_PARTITION, DetectionNotification.DEFAULT_PARTITION);
        addQuery(Query.not(DomainValueExp.eq("JMImplementation")));
    }

    public void setPartitions(String list)
    {
        String old = this.partitionslist;
        partitions.clear();
        partitions.put(DetectionNotification.DEFAULT_PARTITION, DetectionNotification.DEFAULT_PARTITION);
        StringBuffer buf = new StringBuffer(DetectionNotification.DEFAULT_PARTITION);
        if (list != null)
        {
            StringTokenizer tok = new StringTokenizer(list, ",");
            buf.append(",");
            while (tok.hasMoreTokens())
            {
                String token = tok.nextToken();
                partitions.put(token, token);
                buf.append(token);
                if (tok.hasMoreTokens())
                {
                    buf.append(",");
                }
            }
        }
        partitionslist = buf.toString();
        fireAttributeChange("cascadingagent.partition.changed", "Partitions", String.class, old, this.partitionslist);
    }

    /**
     * return a comma-separated list of partitions visible by this cascading agent
     *
     * @return partition list string
     */
    public String getPartitions()
    {
        return partitionslist;
    }

    /**
     * set one or more comma-separated QueryExp by classname
     *
     * @param classname
     */
    public void setQuery(String classname)
        throws Exception
    {
        StringTokenizer tok=new StringTokenizer(classname,",");
        while(tok.hasMoreTokens())
        {
           String cl = tok.nextToken();
           QueryExp query=(QueryExp)Thread.currentThread().getContextClassLoader().loadClass(cl).newInstance();
           addQuery(query);
        }
    }

    /**
     * return a comma-separated list of QueryExp class names
     *
     * @return list of class names
     */
    public String getQuery()
    {
        Iterator i = querylist.iterator();
        StringBuffer buf=new StringBuffer();
        while(i.hasNext())
        {
            QueryExp query=(QueryExp)i.next();
            buf.append(query.getClass().getName());
            if (i.hasNext())
            {
                buf.append(",");
            }
        }
        return buf.toString();
    }

    private void rebuildQuery ()
    {
        if (querylist==null || querylist.isEmpty())
        {
            query=null;
            return;
        }
        QueryExp exp[]=new QueryExp[querylist.size()];
        Iterator i = querylist.iterator();
        int c=0;
        while(i.hasNext())
        {
           exp[c]=(QueryExp)i.next();
           if (log.isDebugEnabled())
           {
               log.debug("    Query ["+c+"] = "+exp.getClass().getName());
           }
           c++;
        }
        // reset filter
        query=new CompositeQueryExp(exp);
    }

    /**
     * add a filter to the cascading agent that will be used when querying remote mbeans
     *
     * @param query
     */
    public void addQuery (QueryExp query)
    {
        querylist.add(query);
        rebuildQuery();
    }

    /**
     * remove a previously added QueryExp
     *
     * @param query
     */
    public void removeQuery (QueryExp query)
    {
        querylist.remove(query);
        rebuildQuery();
    }

    /**
     * get all the QueryExp that are used when querying remote mbeans
     *
     * @return query expressions
     */
    public Collection getAllQueries()
    {
        return querylist;
    }

    protected void startService() throws Exception
    {
        super.startService();

        // add a notification listener for all events related to mbeanserver
        getServer().addNotificationListener(JMXUtil.getMBeanServerObjectName(), this, null, null);

        // add listeners
        Set beans = JMXUtil.queryLocalMBeans(getServer(), new ObjectName("jmx.remoting:type=Detector,*"), null);
        Iterator iter = beans.iterator();
        while (iter.hasNext())
        {
            ObjectInstance instance = (ObjectInstance) iter.next();
            if (log.isDebugEnabled())
            {
                log.debug("registering Detector: " + instance.getObjectName());
            }
            getServer().addNotificationListener(instance.getObjectName(), this, new LocalMBeanEventFilter(), null);
        }
    }

    protected void stopService() throws Exception
    {
        super.stopService();

        // remove a notification listener for all events related to mbeanserver
        getServer().removeNotificationListener(JMXUtil.getMBeanServerObjectName(), this);

        // remove listeners
        Set beans = JMXUtil.queryLocalMBeans(getServer(), new ObjectName("jmx.remoting:type=Detector,*"), null);
        Iterator iter = beans.iterator();
        while (iter.hasNext())
        {
            ObjectInstance instance = (ObjectInstance) iter.next();
            getServer().removeNotificationListener(instance.getObjectName(), this);
        }
        synchronized(connectors)
        {
            // remove all connectors
            iter = connectors.values().iterator();
            while (iter.hasNext())
            {
                Connector con = (Connector) iter.next();
                iter.remove();
                unregisterRemoteServer(con.key, con);
            }
        }
    }

    /**
     * helper method for firing an attribute change event
     *
     * @param event
     * @param attribute
     * @param type
     * @param oldVal
     * @param newVal
     */
    protected void fireAttributeChange(String event, String attribute, Class type, Object oldVal, Object newVal)
    {
        if ((oldVal == null && newVal != null) ||
                (oldVal != null && newVal == null) ||
                (oldVal.equals(newVal) == false))
        {
            sendNotification(
                    new AttributeChangeNotification(
                            this,
                            attributeSequence.increment(),
                            System.currentTimeMillis(),
                            event,
                            attribute,
                            type.getName(),
                            oldVal,
                            newVal));
        }
    }

    /**
     * return the event types for the MBean
     *
     * @return notification metadata
     */
    public MBeanNotificationInfo[] getNotificationInfo()
    {
        MBeanNotificationInfo ni[] = new MBeanNotificationInfo[1];
        ni[0] = new MBeanNotificationInfo(new String[]{"cascadingagent.transports.changed", "cascadingagent.partition.changed"}, AttributeChangeNotification.class.getName(), "attribute change notification");
        return ni;
    }

    /**
     * set a comma-separated list of transports to use
     *
     * @param list
     */
    public void setTransports(String list)
    {
        String old = transportslist;
        if (list == null || list.trim().length() == 0)
        {
            transports = null;
            return;
        }
        StringTokenizer tok = new StringTokenizer(list, ",");
        transports = new String[tok.countTokens()];
        int c = 0;
        while (tok.hasMoreTokens())
        {
            String transport = tok.nextToken();
            transports[c++] = transport;
        }
        this.transportslist = list;

        fireAttributeChange("cascadingagent.transports.changed", "Transports", String.class, old, this.transportslist);
    }

    /**
     * get a set of transports used as a comma separated list
     *
     * @return list of supported transports
     */
    public String getTransports()
    {
        StringBuffer buf = new StringBuffer();
        if (transports != null)
        {
            for (int c = 0; c < transports.length; c++)
            {
                buf.append(transports[c]);
                if (c + 1 < transports.length)
                {
                    buf.append(",");
                }
            }
        }
        return buf.toString();
    }

    protected boolean isInterestedTransport(String trans)
    {
        if (transports == null)
        {
            return true;
        }
        for (int c = 0; c < transports.length; c++)
        {
            if (transports[c].equalsIgnoreCase(trans))
            {
                return true;
            }
        }
        return false;
    }

    private final class Connector
    {
        ObjectName remoteName;
        ObjectName localName;
        MBeanServer server;
        String serverId;
        String instanceId;
        InetAddress ip;
        String key;
        String transport;
    }

    private final class MBean
    {
        ObjectName localName;
        ObjectName remoteName;
        Connector connector;
        DynamicMBean mbean;
    }

    /**
     * returns true if the ObjectName already exists on the current server
     *
     * @param obj
     * @return true if object name exists; false otherwise
     */
    protected boolean alreadyExists(ObjectName obj)
    {
        return getServer().queryMBeans(obj, null).iterator().hasNext();
    }

    /**
     * register a remote server
     *
     * @param con
     * @throws Exception
     */
    protected void registerRemoteServer(String k, Connector con)
            throws Exception
    {
        synchronized (LOCK)
        {
            if (log.isDebugEnabled())
            {
                log.debug("Found a new server with id: "+con.serverId+" at: "+con.ip);
            }
            // register the remote mbean server as a local view under the RemoteJMImplementation domain
            ObjectName rmbs = JMXRemotingObjectName.create(con.serverId, con.ip, new ObjectName("RemoteJMImplementation:type=MBeanServerDelegate"), null, con.instanceId);
            if (alreadyExists(rmbs) == false)
            {
                NotificationJMXInvocationHandler nh = new NotificationJMXInvocationHandler(con.server, JMXUtil.getMBeanServerObjectName());
                Object obj = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{ProxyContext.class, DynamicMBean.class}, nh);
                getServer().registerMBean(obj, rmbs);
            }

            // add a remote notification listener for the remote mbean server
            con.server.addNotificationListener(JMXUtil.getMBeanServerObjectName(), this, new LocalMBeanEventFilter(), k);

            // add a remote notification listener for the remote connector mbean
            CompositeEventFilter filter = new CompositeEventFilter(new NotificationFilter[]{new ClassEventFilter(AttributeChangeNotification.class), new LocalMBeanEventFilter()});
            con.server.addNotificationListener(con.localName, this, filter, null);

            // query all beans on the remote MBeanServer, excluding any mbeans under the JMImplementation domain (the MBeanServerDelegate)
            // and that aren't private
            Set beans = JMXUtil.queryLocalMBeans(con.server, new ObjectName("*:*"), query);
            Iterator iter = beans.iterator();
            while (iter.hasNext())
            {
                ObjectInstance instance = (ObjectInstance) iter.next();
                ObjectName ro = instance.getObjectName();
                if (query.apply(ro))
                {
                    registerMBean(con, ro);
                }
            }
        }
    }

    /**
     * registered a remote mbean
     *
     * @param con
     * @param ro
     * @throws Exception
     */
    protected void registerMBean(Connector con, ObjectName ro)
            throws Exception
    {
        synchronized (LOCK)
        {
            ObjectName on = JMXRemotingObjectName.create(con.serverId, con.ip, ro, null, con.instanceId);

            if (alreadyExists(on))
            {
                return;
            }
            /*
            //FIXME - this should be moved into the QueryExp above so we don't pull across MBeans
            //and then ignore them ... (JGH)
            String partition = ro.getKeyProperty("partition");
            if (partition != null)
            {
                if (partitions.containsKey(partition) == false)
                {
                    if (log.isDebugEnabled())
                    {
                        log.debug(" ... skipping since partition (" + partitions + ") isn't part of (" + partitionslist + ")");
                    }
                    return;
                }
            }
            try
            {
                String p = (String) con.server.getAttribute(ro, "Partitions");
                if (p != null)
                {
                    // check and see if our partition is contained
                    StringTokenizer tok = new StringTokenizer(p, ",");
                    while (tok.hasMoreTokens())
                    {
                        String token = (String) tok.nextToken();
                        if (partitions.containsKey(token) == false)
                        {
                            if (log.isDebugEnabled())
                            {
                                log.debug(" ... ignoring mbean because it isn't part of our partition set [" + token + "], ours [" + partitionslist + "]");
                            }
                            return;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                // maybe we don't have that attribute, that's OK since it's
                // not mandatory
            } */
            MBean mbean = new MBean();
            mbean.localName = on;
            mbean.remoteName = ro;
            mbean.connector = con;
            if (log.isDebugEnabled())
            {
                log.debug("adding remote MBean... server: " + con.server + ", LocalObjectName: " + on + ", RemoteObjectName: " + ro);
            }
            // create a proxy to the remote mbean
            mbean.mbean = (DynamicMBean) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{ProxyContext.class, DynamicMBean.class}, new NotificationJMXInvocationHandler(con.server, ro));

            // put the mbean in our local store so we can map info back to it (use the local name)
            objectmap.put(on, mbean);

            // register the mbean
            getServer().registerMBean(mbean.mbean, on);

            String t = mbean.localName.getKeyProperty("type");
            if (t != null && t.equals("Detector"))
            {
                // add a special filter/listener for receiving attribute changes on detectors
                CompositeEventFilter filter = new CompositeEventFilter(new NotificationFilter[]{new ClassEventFilter(AttributeChangeNotification.class), new LocalMBeanEventFilter()});
                con.server.addNotificationListener(ro, this, filter, null);
            }
        }
    }

    /**
     * unregister any associated MBeans for the connector
     *
     * @param con
     * @throws Exception
     */
    protected void unregisterRemoteServer(String key, Connector con)
            throws Exception
    {
        synchronized (LOCK)
        {
            if (log.isDebugEnabled())
            {
                log.debug("unregisterRemoteServer -> " + con);
            }

            ObjectName rmbs = JMXRemotingObjectName.create(con.serverId, con.ip, new ObjectName("RemoteJMImplementation:type=MBeanServerDelegate"), null, con.instanceId);
            if (alreadyExists(rmbs))
            {
                try
                {
                    getServer().unregisterMBean(rmbs);
                }
                catch (Throwable ex)
                {
                    // ignore exception
                    if (log.isDebugEnabled())
                    {
                        log.debug("Exception unregistering MBean: " + rmbs, ex);
                    }
                }
            }

            try
            {
                con.server.removeNotificationListener(JMXUtil.getMBeanServerObjectName(), this);
            }
            catch (Throwable ex)
            {
                // ignore,since it will almost always fail (connector is down...)
                // but just in case we are stopping
            }
            try
            {
                con.server.removeNotificationListener(con.localName, this);
            }
            catch (Throwable ex)
            {
                // ignore, since it almost always fail
            }
            // destroy connector
            ConnectorFactory.destroyConnector(con.serverId);
            connectors.remove(con.key);

            String serverId = con.serverId;
            Iterator iter = objectmap.values().iterator();
            // remove all remote MBeans from the local MBeanServer
            // that match our serverid
            while (iter.hasNext())
            {
                MBean mbean = (MBean) iter.next();
                if (mbean.connector.serverId.equals(serverId))
                {
                    try
                    {
                        unregisterMBean(con, mbean);
                    }
                    catch (Throwable ex)
                    {
                        // ignore
                        if (log.isDebugEnabled())
                        {
                            log.debug("Error unregistering MBean: " + mbean, ex);
                        }
                    }

                    iter.remove();
                }
            }
        }
    }

    /**
     * search from a specific MBean using the remote ObjectName
     * and return the internal MBean holder class or null if not found
     *
     * @param remoteObjectName
     * @return mbean or null
     */
    protected MBean findMBean(ObjectName remoteObjectName)
    {
        Iterator i = objectmap.values().iterator();
        while (i.hasNext())
        {
            MBean mbean = (MBean) i.next();
            if (mbean.remoteName.equals(remoteObjectName))
            {
                return mbean;
            }
        }
        return null;
    }

    protected MBean findMBean(Connector con, ObjectName remoteObjectName)
    {
        if (log.isDebugEnabled())
        {
            log.debug("searching for: " + remoteObjectName + ", in: " + con.serverId);
        }
        Iterator i = objectmap.values().iterator();
        while (i.hasNext())
        {
            MBean mbean = (MBean) i.next();
            if (con.serverId.equals(mbean.connector.serverId) &&
                    mbean.localName.equals(remoteObjectName))
            {
                return mbean;
            }
        }
        return null;
    }

    /**
     * unregister an MBean
     *
     * @param con
     * @param mbean
     * @throws Exception
     */
    protected void unregisterMBean(Connector con, MBean mbean)
            throws Exception
    {
        synchronized (LOCK)
        {
            if (mbean.connector.serverId.equals(con.serverId))
            {
                if (log.isDebugEnabled())
                {
                    log.debug(" ... Removing ... " + mbean.localName);
                }
                try
                {
                    // unregister from local mbeanserver
                    getServer().unregisterMBean(mbean.localName);
                }
                catch (Exception ex)
                {
                    if (ex instanceof InstanceNotFoundException)
                    {
                        // NO OP
                    }
                    else
                    {
                        if (log.isDebugEnabled())
                        {
                            log.debug("Error removing mbean: " + mbean.localName, ex);
                        }
                    }
                }

                String t = mbean.localName.getKeyProperty("type");
                if (t != null && t.equals("Detector"))
                {
                    try
                    {
                        con.server.removeNotificationListener(mbean.remoteName, this);
                    }
                    catch (Exception ex)
                    {
                        boolean display=true;
                        if (ex instanceof IllegalStateException)
                        {
                            if (ex.getMessage().equals("socket has been closed"))
                            {
                                display=false;
                            }
                        }
                        if (display && log.isDebugEnabled())
                        {
                            log.debug("Error removing notification listener: " + mbean.remoteName, ex);
                        }
                    }
                }
            }
        }
    }

    /**
     * called when detection activity is made on a remote connector
     *
     * @param detectionMsg
     */
    public void detectionPerformed(DetectionNotification detectionMsg)
    {
        if (log.isDebugEnabled())
        {
            log.debug("detection: " + detectionMsg);
        }

        if (detectionMsg.getType() == DetectionNotification.STARTUP &&
                isInterestedTransport(detectionMsg.getTransport()) == false)
        {
            if (log.isDebugEnabled())
            {
                log.debug("ignoring detection: " + detectionMsg + " ... not interested in this transport");
            }
            // filter out transports we aren't interested in
            return;
        }
        try
        {
            if (JMXUtil.getServerId(getServer()).equals(detectionMsg.getServerId()))
            {
                // shouldn't normally get here unless some issue in our logic
                if (log.isDebugEnabled())
                {
                    log.debug("ignoring detection from ourself: " + detectionMsg);
                }
                return;
            }
        }
        catch (Exception ex)
        {
            log.error("Error getting our own JMImplementation MBeanServerId attribute", ex);
        }

        if (detectionMsg.getType().equals(DetectionNotification.STARTUP))
        {
            try
            {
                MBeanServerConnection conn = new MBeanServerConnection (detectionMsg);
                MBeanServer server = ConnectorFactory.createConnector(conn);
                // unique key is the server id + connector type
                String key = detectionMsg.getServerId() + ":" + detectionMsg.getTransport();
                Connector con = new Connector();
                con.remoteName = new ObjectName(detectionMsg.getLocalName());
                con.localName = new ObjectName(detectionMsg.getRemoteName());
                con.server = server;
                con.serverId = detectionMsg.getServerId();
                con.instanceId = detectionMsg.getInstanceId();
                con.transport = detectionMsg.getTransport();
                con.ip = detectionMsg.getIPAddress();
                con.key = key;
                //log.debug("create new Connector with key ... " + key + " to ip: " + con.ip);
                // add to connectors
                connectors.put(key, con);
                registerRemoteServer(key, con);
            }
            catch (Exception ex)
            {
                log.warn("Error creating Connector from detection message: " + detectionMsg, ex);
            }
        }
        else if (detectionMsg.getType().equals(DetectionNotification.FAILURE))
        {
            String key = detectionMsg.getServerId() + ":" + detectionMsg.getTransport();
            //log.debug("removing key ... "+key);
            Connector con = (Connector) connectors.get(key);
            if (con != null)
            {
                try
                {
                    unregisterRemoteServer(key, con);
                }
                catch (Exception ex)
                {
                    log.warn("Error removing Connector from detection message: " + detectionMsg, ex);
                }
            }
            else
            {
                if (log.isDebugEnabled())
                {
                    log.debug("Couldn't find ... " + key);
                }
            }
        }

    }

    /**
     * JMX Notification handler
     *
     * @param notification
     * @param o
     */
    public void handleNotification(Notification notification, Object o)
    {
        if (log.isDebugEnabled())
        {
            log.debug("Notification: " + notification + ", handback: " + o+", type: "+notification.getType());
        }

        if (notification instanceof DetectionNotification)
        {
            detectionPerformed((DetectionNotification) notification);
            return;
        }
        Object src = notification.getSource();
        if (src instanceof ObjectName)
        {
            ObjectName source = (ObjectName) src;

            try
            {
                ObjectName mbs = JMXUtil.getMBeanServerObjectName();
//                log.debug("mbs="+mbs+", src="+src);
                if (mbs.equals((ObjectName) src))
                {
                    MBeanServerNotification mn = (MBeanServerNotification) notification;
                    String type = notification.getType();

                    // MBeanServer notification
                    if (o != null)
                    {
                        // remove MBeanServer notification
                        String key = (String) o;
                        Connector con = (Connector) connectors.get(key);
                        if (con != null)
                        {

                            if (type.equals("JMX.mbean.registered"))
                            {
                                // remote mbean is registered
                                ObjectName on = mn.getMBeanName();
                                if (on.getKeyProperty("MBeanServerId") == null)
                                {
                                    //FIXME: double check this...
                                    registerMBean(con, (ObjectName) mn.getMBeanName());
                                    return;
                                }
                            }
                            else if (type.equals("JMX.mbean.unregistered"))
                            {
                                // remove mbean is unregistered
                                MBean mbean = findMBean(con, (ObjectName) mn.getMBeanName());
                                if (mbean != null)
                                {
                                    log.debug("found mbean: " + mbean + ", unregistering...");
                                    unregisterMBean(con, mbean);
                                    return;
                                }
                            }
                        }
                    }
                    else
                    {
                        // a connector is added, removed
                        if (type.equals("JMX.mbean.registered"))
                        {
                            // new connector -- add notification listener
                            getServer().addNotificationListener(source, this, new LocalMBeanEventFilter(), null);
                            return;
                        }
                        else if (type.equals("JMX.mbean.unregistered"))
                        {
                            // removed connector  -- remove listener
                            getServer().removeNotificationListener(source, this);
                            return;
                        }
                    }
                }
                else
                {
                    if (notification instanceof AttributeChangeNotification)
                    {
                        AttributeChangeNotification ac = (AttributeChangeNotification) notification;
                        String msg = notification.getMessage();
                        if (msg != null && msg.equals("detector.partition.changed"))
                        {
                            // detector update
                            Object nv = ac.getNewValue();
                            if (partitions.containsKey(nv.toString()) == false)
                            {
                                if (log.isDebugEnabled())
                                {
                                    log.debug("Detector changed partition to: " + nv + " from: " + nv);
                                }
                                // new partition doesn't equal our partition, we need
                                // to unexpose these
                                MBean mbean = findMBean(source);
                                if (mbean != null)
                                {
                                    // unregister this connector and associated mbeans
                                    unregisterRemoteServer(mbean.connector.key, mbean.connector);
                                    return;
                                }
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                log.warn("Error on handleNotification for: " + notification, ex);
            }
        }

    }

}
TOP

Related Classes of org.jboss.mx.remote.server.CascadingAgent$Connector

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.