au.com.jwatmuff.genericp2p.rmi.RMIPeerManager.java Source code

Java tutorial

Introduction

Here is the source code for au.com.jwatmuff.genericp2p.rmi.RMIPeerManager.java

Source

/*
 * EventManager
 * Copyright (c) 2008-2017 James Watmuff & Leonard Hall
 *
 * This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program 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 General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package au.com.jwatmuff.genericp2p.rmi;

import au.com.jwatmuff.genericp2p.NoSuchServiceException;
import au.com.jwatmuff.genericp2p.Peer;
import au.com.jwatmuff.genericp2p.PeerConnectionEvent;
import au.com.jwatmuff.genericp2p.PeerConnectionListener;
import au.com.jwatmuff.genericp2p.PeerDiscoveryEvent;
import au.com.jwatmuff.genericp2p.PeerDiscoveryListener;
import au.com.jwatmuff.genericp2p.PeerInfo;
import au.com.jwatmuff.genericp2p.PeerManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.proxy.Interceptor;
import org.apache.commons.proxy.Invocation;
import org.apache.commons.proxy.ProxyFactory;
import org.apache.log4j.Logger;
import org.springframework.remoting.RemoteLookupFailureException;
import org.springframework.remoting.rmi.RmiProxyFactoryBean;
import org.springframework.remoting.rmi.RmiServiceExporter;

/**
 *
 * @author James
 */
public class RMIPeerManager
        implements PeerManager, PeerDiscoveryListener, AnnounceService, IdentifyService, LookupService {
    private static final Logger log = Logger.getLogger(RMIPeerManager.class);

    private class PeerService<T> {
        String name;
        Class<T> serviceClass;
        T implementation;
        RmiServiceExporter exporter;
    }

    private int registryPort;
    private File idFile;
    private UUID uuid;
    private Collection<PeerConnectionListener> listeners = new ArrayList<>();
    private Map<String, PeerService> peerServiceMap = new HashMap<>();
    private String name;
    private final Collection<ManagedPeer> peers = Collections.synchronizedCollection(new ArrayList<ManagedPeer>());
    private final Set<InetSocketAddress> addresses = Collections.synchronizedSet(new HashSet<InetSocketAddress>());
    private Registry registry;

    private ScheduledExecutorService slowPeerCheckExecutor;
    private ScheduledExecutorService fastPeerCheckExecutor;
    private ExecutorService timeoutExecutor = Executors.newCachedThreadPool();

    public RMIPeerManager(int registryPort, File idFile) {
        try {
            registry = LocateRegistry.createRegistry(registryPort);
        } catch (RemoteException e) {
            log.error("Failed to create RMI registry", e);
        }

        this.registryPort = registryPort;
        this.idFile = idFile;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void start() {
        log.debug("Obtained local ID: " + this.getUUID());

        registerService(AnnounceService.class, this);
        registerService(IdentifyService.class, this);
        registerService(LookupService.class, this);

        fastPeerCheckExecutor = Executors.newScheduledThreadPool(2);
        slowPeerCheckExecutor = Executors.newScheduledThreadPool(4);

        fastPeerCheckExecutor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                for (ManagedPeer peer : getManagedPeers()) {
                    log.debug("FAST CHECK CHECKING: " + peer);
                    checkPeer(peer);
                }
            }
        }, 30, 30, TimeUnit.SECONDS);

        slowPeerCheckExecutor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                addSelf();
                for (InetSocketAddress address : getAddresses()) {
                    log.debug("SLOW CHECK CHECKING: " + address);
                    checkAddress(address);
                }
            }
        }, 30, 30, TimeUnit.SECONDS);

        addSelf();

        log.debug("Completed RMIPeerManager startup");
    }

    /**
     * This method is to be called remotely by a peer to determine the ID of the
     * local client. This ID is unique on an operating system user basis, using
     * a mechanism such as a user registry (under Windows) or a user
     * configuration file (under Linux) to store the ID.
     * 
     * The ID is a random 128-bit Universally Unique ID, guaranteed to be
     * globally unique to a very high level of certainty.
     * 
     * @return  The ID of the local client
     */
    @Override
    public UUID getUUID() {
        if (uuid != null) {
            return uuid;
        }

        Properties props = new Properties();
        try {
            props.load(new FileInputStream(idFile));
            uuid = UUID.fromString(props.getProperty("uuid"));
        } catch (IOException e) {
            try {
                uuid = UUID.randomUUID();
                props.setProperty("uuid", uuid.toString());
                props.store(new FileOutputStream(idFile), null);
            } catch (IOException ex) {
                log.error("Unable to record peer UUID to file", ex);
            }
        }

        return uuid;
    }

    /**
     * This method is to be called remotely (via RMI) by a peer to inform the
     * local client of its existence on the network. The local client may
     * already be aware of the peer, by this mechanism or otherwise.
     *
     * @param name      The name of the remote peer
     * @param address   The socket address by which the peer may be contacted
     */
    @Override
    public void announce(String name, InetSocketAddress address, UUID id) {
        handlePeerInfo(new PeerInfo(name, address, id));
    }

    /**
     * This method is to be called remotely (via RMI) by a peer to inform the
     * local client of its existence on the network. The local client may
     * already be aware of the peer, by this mechanism or otherwise.
     *
     * @param name      The name of the remote peer
     * @param address   The socket address by which the peer may be contacted
     */
    @Override
    public void announceDisconnected(String name, UUID id) {
        ManagedPeer peer = getPeerWithUUID(id);
        if (peer != null)
            peer.setDisconnected();
    }

    public void handlePeerInfo(final PeerInfo info) {
        addresses.add(info.getAddress());
        fastPeerCheckExecutor.submit(new Runnable() {
            @Override
            public void run() {
                checkAddress(info.getAddress());
            }
        });
    }

    private void checkAddress(InetSocketAddress address) {
        if (getPeerWithAddress(address) != null)
            return;

        ManagedPeer peer = new ManagedPeer(address);
        peer.checkConnectivity();
        if (peer.isConnected()) {
            peer.populatePeerInfo();
            boolean notify = false;
            synchronized (peers) {
                if (getPeerWithUUID(peer.getUUID()) == null) {
                    peers.add(peer);
                    notify = true;
                }
            }
            if (notify) {
                notifyPeerConnectionListeners(new PeerConnectionEvent(PeerConnectionEvent.Type.CONNECTED, peer));
                announcePeer(peer);
            }
        }
    }

    private void checkPeer(ManagedPeer peer) {
        peer.checkConnectivity();
        if (peer.isConnected()) {
            announcePeer(peer);
        }
    }

    private void announcePeer(ManagedPeer p) {
        try {
            AnnounceService service = p.getService(AnnounceService.class);
            for (InetSocketAddress address : getAddresses()) {
                service.announce(null, address, null);
            }
        } catch (NoSuchServiceException e) {
            log.error("Exception announcing peers", e);
        }
    }

    private ManagedPeer getPeerWithAddress(InetSocketAddress address) {
        synchronized (peers) {
            for (ManagedPeer peer : peers) {
                if (peer.getAddress().equals(address))
                    return peer;
            }
        }
        return null;
    }

    private ManagedPeer getPeerWithUUID(UUID uuid) {
        synchronized (peers) {
            for (ManagedPeer peer : peers) {
                if (peer.getUUID().equals(uuid))
                    return peer;
            }
        }
        return null;
    }

    private void notifyPeerConnectionListeners(PeerConnectionEvent event) {
        int n = listeners.size();
        int i = 0;
        for (PeerConnectionListener listener : new ArrayList<PeerConnectionListener>(listeners)) {
            log.debug("NOTIFYING LISTENER " + (++i) + "/" + n + " - " + listener);
            try {
                listener.handleConnectionEvent(event);
            } catch (Exception e) {
                log.error("Error notifying listener of peer connection event", e);
            }
        }
    }

    /**
     * Handles a peer FOUND or LOST event generated by a PeerDiscoveryService.
     * If a peer has been found, the manager will attempt to connect to that
     * peer.
     * If a peer has been lost, the manager will mark the peer as disconnected
     * and not attempt to re-connect to it.
     * 
     * @param event
     */
    @Override
    public void handleDiscoveryEvent(PeerDiscoveryEvent event) {
        if (event.getType() == PeerDiscoveryEvent.Type.FOUND) {
            this.handlePeerInfo(event.getPeerInfo());
        }
    }

    /**
     * I want this to tell all peers to set this peer to disconnected.
     */
    @Override
    public void stop() {
        slowPeerCheckExecutor.shutdown();
        fastPeerCheckExecutor.shutdown();
        try {
            for (Peer peer : getPeers())
                peer.getService(AnnounceService.class).announceDisconnected(name, uuid);
        } catch (NoSuchServiceException e) {
            log.error("Exception announcing disconnection to peers", e);
        }
    }

    /**
     * Registers a PeerConnectionListener which will be notified when peers are
     * connected to or disconnected from the peer manager.
     *
     * @param listener  The listener to be registered
     */
    @Override
    public void addConnectionListener(PeerConnectionListener listener) {
        listeners.add(listener);
    }

    /**
     * Unregisters a PeerConnectionListener
     * 
     * @param listener  The listener to be unregistered
     * 
     * @see addConnectionListener
     */
    @Override
    public void removeConnectionListener(PeerConnectionListener listener) {
        listeners.remove(listener);
    }

    /**
     * Associates the given class and service name, so that the service may
     * be obtained from any Peer objects supplied by this PeerManager.
     * If an implementation is supplied, remote client may obtain an instance
     * of the service from the this client.
     * 
     * For the service to be obtainable from a given remote peer, that peer must
     * have also registered the service and supplied an implementation of the
     * service.
     * 
     * @param <T>
     * @param serviceName
     * @param serviceClass
     * @param implementation
     */
    @Override
    public <T> void registerService(String serviceName, Class<T> serviceClass, T implementation) {
        PeerService<T> service = new PeerService<T>();
        service.name = serviceName;
        service.serviceClass = serviceClass;
        service.implementation = implementation;

        if (peerServiceMap.containsKey(serviceName)) {
            log.warn("Service with name " + serviceName + " has already been registered");
        }

        if (implementation != null) {
            RmiServiceExporter exporter = new RmiServiceExporter();
            //exporter.setRegistryPort(registryPort);
            exporter.setRegistry(registry);
            exporter.setServiceName(serviceName);
            exporter.setServiceInterface(serviceClass);
            exporter.setService(implementation);
            try {
                exporter.afterPropertiesSet();
                exporter.prepare();
                service.exporter = exporter;
                peerServiceMap.put(serviceName, service);
            } catch (RemoteException e) {
                log.error("Exporting service " + serviceName + " (" + serviceClass + ") failed", e);
            }
        }
    }

    @Override
    public <T> void registerService(Class<T> serviceClass, T implementation) {
        registerService(serviceClass.getCanonicalName(), serviceClass, implementation);
    }

    @Override
    public void unregisterService(String serviceName) {
        PeerService service = peerServiceMap.remove(serviceName);
        if (service == null) {
            log.warn("Attempt to unregister unknown service: " + serviceName);
            return;
        }
        try {
            service.exporter.destroy();
        } catch (RemoteException e) {
            log.error(e.getMessage(), e);
        }
    }

    private void addSelf() {
        // Add ourselves as a peer on our new IP address
        try {
            InetSocketAddress local = new InetSocketAddress(InetAddress.getLocalHost().getHostAddress(),
                    registryPort);
            handlePeerInfo(new PeerInfo(name, local, uuid));
        } catch (UnknownHostException e) {
            log.error("Could not find local host", e);
        }
    }

    @Override
    public void refreshServices() {
        try {
            // Restart RMI Registry
            UnicastRemoteObject.unexportObject(registry, true);
            registry = LocateRegistry.createRegistry(registryPort);

            // Re-export services to new RMI Registry
            for (PeerService service : peerServiceMap.values()) {
                refreshService(service);
            }

            addSelf();
        } catch (Exception e) {
            log.error("Failed to refresh services", e);
        }
    }

    private void refreshService(PeerService service) {
        RmiServiceExporter exporter = new RmiServiceExporter();
        exporter.setRegistryPort(registryPort);
        exporter.setServiceName(service.name);
        exporter.setServiceInterface(service.serviceClass);
        exporter.setService(service.implementation);
        try {
            service.exporter.destroy();
            exporter.afterPropertiesSet();
            exporter.prepare();
            service.exporter = exporter;
        } catch (RemoteException e) {
            log.error("Refreshing service " + service.name + " (" + service + ") failed", e);
        }
    }

    @SuppressWarnings("unchecked")
    private static <T> T getService(InetSocketAddress address, String serviceName, Class<T> serviceClass)
            throws NoSuchServiceException {
        String url = "rmi://" + address.getHostName() + ":" + address.getPort() + "/" + serviceName;
        try {
            /* maybe should use address.getAddress().getHostAddress() */
            RmiProxyFactoryBean factory = new RmiProxyFactoryBean();
            factory.setServiceInterface(serviceClass);
            factory.setServiceUrl(url);
            factory.afterPropertiesSet();
            return (T) factory.getObject();
        } catch (RemoteLookupFailureException rlfe) {
            throw new NoSuchServiceException("Unable to resolve service " + serviceName + " at url: " + url);
        } catch (Exception e) {
            throw new RuntimeException("getService failed", e);
        }
    }

    @Override
    public Map<String, Class> getServices() {
        Map<String, Class> services = new HashMap<String, Class>();
        for (String serviceName : peerServiceMap.keySet()) {
            services.put(serviceName, peerServiceMap.get(serviceName).serviceClass);
        }
        return services;
    }

    @Override
    public Collection<String> getServices(Class serviceClass) {
        Collection<String> names = new ArrayList<String>();
        for (String serviceName : peerServiceMap.keySet()) {
            if (peerServiceMap.get(serviceName).serviceClass.equals(serviceClass)) {
                names.add(serviceName);
            }
        }
        return names;
    }

    /**
     * Returns the list of peers which this manager is currently connected to.
     *
     * @return
     */
    @Override
    public Collection<Peer> getPeers() {
        Collection<Peer> connectedPeers = new ArrayList<Peer>();
        synchronized (peers) {
            for (ManagedPeer peer : peers) {
                if (peer.isConnected()) {
                    connectedPeers.add(peer);
                }
            }
        }

        return connectedPeers;
    }

    private Collection<ManagedPeer> getManagedPeers() {
        synchronized (peers) {
            return new ArrayList<>(peers);
        }
    }

    private Collection<InetSocketAddress> getAddresses() {
        synchronized (addresses) {
            return new ArrayList<>(addresses);
        }
    }

    @Override
    public boolean isRegistered(String serviceName) {
        return peerServiceMap.containsKey(serviceName);
    }

    @Override
    public boolean initialisedOk() {
        return true;
    }

    private static enum PeerStatus {
        DISCONNECTED, CONNECTED
    }

    private static final ProxyFactory pf = new ProxyFactory();

    private class ManagedPeer implements Peer {
        boolean connected = true;
        PeerInfo info;
        InetSocketAddress address;

        public ManagedPeer(InetSocketAddress address) {
            this.address = address;
            populatePeerInfo();
        }

        private Interceptor interceptor = new Interceptor() {
            @Override
            public Object intercept(Invocation inv) throws Throwable {
                if (!connected) {
                    throw new RuntimeException("Peer is disconnected");
                }
                try {
                    return inv.proceed();
                } catch (RemoteException e) {
                    log.error(this + " Connectivity failure", e);
                    setDisconnected();
                    throw e;
                }
            }
        };

        private void populatePeerInfo() {
            try {
                IdentifyService service = getService(IdentifyService.class);
                info = new PeerInfo(service.getName(), address, service.getUUID());
            } catch (NoSuchServiceException e) {
                log.error(e.getMessage(), e);
            }
        }

        public void checkConnectivity() {
            if (!connected)
                return;

            log.debug("Checking connectivity of: " + this);
            final AtomicBoolean connected = new AtomicBoolean(false);
            Future<?> f = timeoutExecutor.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        UUID uuid = getServiceWithoutProxy(IdentifyService.class).getUUID();
                        if (info == null || uuid.equals(info.getID()))
                            connected.set(true);
                    } catch (Exception e) {
                    }
                }
            });

            try {
                f.get(5, TimeUnit.SECONDS);
            } catch (TimeoutException e) {
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }

            if (!connected.get()) {
                setDisconnected();
            } else {
                log.debug("Connectivity check: OK");
            }
        }

        public void setDisconnected() {
            if (!connected)
                return;

            if (peers.remove(this)) {
                log.debug("Setting peer " + this + " Disconnected");
                connected = false;

                timeoutExecutor.submit(new Runnable() {
                    @Override
                    public void run() {
                        notifyPeerConnectionListeners(
                                new PeerConnectionEvent(PeerConnectionEvent.Type.DISCONNECTED, ManagedPeer.this));
                    }
                });
            }
        }

        public boolean isConnected() {
            return connected;
        }

        @Override
        @SuppressWarnings("unchecked")
        public <T> T getService(final String name, final Class<T> serviceClass) throws NoSuchServiceException {
            Future<T> future = timeoutExecutor.submit(new Callable<T>() {
                @Override
                public T call() throws Exception {
                    return RMIPeerManager.getService(address, name, serviceClass);
                }
            });

            try {
                T service = future.get(5, TimeUnit.SECONDS);
                T proxy = (T) pf.createInterceptorProxy(service, interceptor, new Class[] { serviceClass });
                return proxy;
            } catch (TimeoutException e) {
                setDisconnected();
                throw new NoSuchServiceException("No such service: " + name + " - due to timeout");
            } catch (ExecutionException e) {
                if (e.getCause() instanceof NoSuchServiceException)
                    throw (NoSuchServiceException) e.getCause();
                else
                    throw new RuntimeException(e);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public <T> T getService(Class<T> serviceClass) throws NoSuchServiceException {
            return getService(serviceClass.getCanonicalName(), serviceClass);
        }

        private <T> T getServiceWithoutProxy(String name, Class<T> serviceClass) throws NoSuchServiceException {
            return RMIPeerManager.getService(address, name, serviceClass);
        }

        private <T> T getServiceWithoutProxy(Class<T> serviceClass) throws NoSuchServiceException {
            return getServiceWithoutProxy(serviceClass.getCanonicalName(), serviceClass);
        }

        @Override
        public String getName() {
            return info.getName();
        }

        @Override
        public UUID getUUID() {
            return info.getID();
        }

        public InetSocketAddress getAddress() {
            return address;
        }

        @Override
        public String toString() {
            return getName() + " @ " + address + " : " + getUUID();
        }
    }
}