RemoteContext.java

package nl.tudelft.simulation.naming.context;

import java.io.Serializable;
import java.net.URL;
import java.rmi.AccessException;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.naming.NameNotFoundException;
import javax.naming.NamingException;

import org.djutils.event.EventListener;
import org.djutils.event.EventListenerMap;
import org.djutils.event.EventProducer;
import org.djutils.event.EventType;
import org.djutils.event.reference.Reference;
import org.djutils.event.reference.ReferenceType;
import org.djutils.event.rmi.RmiEventProducer;
import org.djutils.exceptions.Throw;
import org.djutils.rmi.RmiObject;

import nl.tudelft.simulation.naming.context.util.ContextUtil;

/**
 * Context that exists on another computer.
 * <p>
 * Copyright (c) 2002-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
 * for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>. The DSOL
 * project is distributed under a three-clause BSD-style license, which can be found at
 * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">
 * https://https://simulation.tudelft.nl/dsol/docs/latest/license.html</a>.
 * </p>
 * @author <a href="https://www.linkedin.com/in/peterhmjacobs">Peter Jacobs </a>
 * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
 */
public class RemoteContext extends RmiObject implements RemoteContextInterface, EventProducer
{
    /** The default serial version UID for serializable classes. */
    private static final long serialVersionUID = 1L;

    /** The underlying event context. */
    protected ContextInterface embeddedContext = null;

    /** The (remote) event producer for this context. */
    protected RemoteChangeEventProducer remoteEventProducer;

    /**
     * Constructs a new RemoteContext. Register the new context in the RMI registry. When the RMI registry does not exist yet,
     * it will be created, but <b>only</b> on the local host. Remote creation of a registry on another computer is not possible.
     * Any attempt to do so will cause an AccessException to be fired.
     * @param host String; the host where the RMI registry resides or will be created. Creation is only possible on localhost.
     * @param port int; the port where the RMI registry can be found or will be created
     * @param bindingKey the key under which this context will be bound in the RMI registry
     * @param embeddedContext ContextInterface; the underlying context
     * @param eventProducerBindingKey String; the key under which the event producer will be bound
     * @throws RemoteException when there is a problem with the RMI registry
     * @throws AlreadyBoundException when there is already another object bound to the bindingKey
     * @throws NullPointerException when host, path, or bindingKey is null
     * @throws IllegalArgumentException when port &lt; 0 or port &gt; 65535
     * @throws AccessException when there is an attempt to create a registry on a remote host
     */
    public RemoteContext(final String host, final int port, final String bindingKey, final ContextInterface embeddedContext,
            final String eventProducerBindingKey) throws RemoteException, AlreadyBoundException
    {
        super(host, port, bindingKey);
        Throw.whenNull(embeddedContext, "embedded context cannot be null");
        this.embeddedContext = embeddedContext;
        this.remoteEventProducer = new RemoteChangeEventProducer(host, port, eventProducerBindingKey);
    }

    /**
     * Constructs a new RemoteContext. Register the new context in the RMI registry. When the host has not been specified in the
     * URL, 127.0.0.1 will be used. When the port has not been specified in the URL, the default RMI port 1099 will be used.
     * When the RMI registry does not exist yet, it will be created, but <b>only</b> on the local host. Remote creation of a
     * registry on another computer is not possible. Any attempt to do so will cause an AccessException to be fired.
     * @param registryURL URL; the URL of the registry, e.g., "http://localhost:1099" or "http://130.161.185.14:28452"
     * @param embeddedContext ContextInterface; the underlying context
     * @param eventProducerBindingKey String; the key under which the event producer will be bound
     * @throws RemoteException when there is a problem with the RMI registry
     * @throws AlreadyBoundException when there is already another object bound to the bindingKey
     * @throws NullPointerException when registryURL, bindingKey, or embeddedContext is null
     * @throws AccessException when there is an attempt to create a registry on a remote host
     */
    public RemoteContext(final URL registryURL, final ContextInterface embeddedContext, final String eventProducerBindingKey)
            throws RemoteException, AlreadyBoundException
    {
        super(registryURL, registryURL.getPath());
        Throw.whenNull(embeddedContext, "embedded context cannot be null");
        this.embeddedContext = embeddedContext;
        String host = registryURL.getHost() == null ? "127.0.0.1" : registryURL.getHost();
        int port = registryURL.getPort() == -1 ? 1099 : registryURL.getPort();
        this.remoteEventProducer = new RemoteChangeEventProducer(host, port, eventProducerBindingKey);
    }

    /** {@inheritDoc} */
    @Override
    public String getAtomicName() throws RemoteException
    {
        return this.embeddedContext.getAtomicName();
    }

    /** {@inheritDoc} */
    @Override
    public ContextInterface getParent() throws RemoteException
    {
        return this.embeddedContext.getParent();
    }

    /** {@inheritDoc} */
    @Override
    public ContextInterface getRootContext() throws RemoteException
    {
        return this.embeddedContext.getRootContext();
    }

    /** {@inheritDoc} */
    @Override
    public String getAbsolutePath() throws RemoteException
    {
        return this.embeddedContext.getAbsolutePath();
    }

    /** {@inheritDoc} */
    @Override
    public Object get(final String name) throws NamingException, RemoteException
    {
        return this.embeddedContext.get(name);
    }

    /** {@inheritDoc} */
    @Override
    public Object getObject(final String key) throws NamingException, RemoteException
    {
        return this.embeddedContext.getObject(key);
    }

    /** {@inheritDoc} */
    @Override
    public boolean exists(final String name) throws NamingException, RemoteException
    {
        return this.embeddedContext.exists(name);
    }

    /** {@inheritDoc} */
    @Override
    public boolean hasKey(final String key) throws NamingException, RemoteException
    {
        return this.embeddedContext.hasKey(key);
    }

    /** {@inheritDoc} */
    @Override
    public boolean hasObject(final Object object) throws RemoteException
    {
        return this.embeddedContext.hasObject(object);
    }

    /** {@inheritDoc} */
    @Override
    public boolean isEmpty() throws RemoteException
    {
        return this.embeddedContext.isEmpty();
    }

    /** {@inheritDoc} */
    @Override
    public void bind(final String name, final Object object) throws NamingException, RemoteException
    {
        this.embeddedContext.bind(name, object);
    }

    /** {@inheritDoc} */
    @Override
    public void bindObject(final String key, final Object object) throws NamingException, RemoteException
    {
        this.embeddedContext.bindObject(key, object);
    }

    /** {@inheritDoc} */
    @Override
    public void bindObject(final Object object) throws NamingException, RemoteException
    {
        this.embeddedContext.bindObject(object);
    }

    /** {@inheritDoc} */
    @Override
    public void unbind(final String name) throws NamingException, RemoteException
    {
        this.embeddedContext.unbind(name);
    }

    /** {@inheritDoc} */
    @Override
    public void unbindObject(final String key) throws NamingException, RemoteException
    {
        this.embeddedContext.unbindObject(key);
    }

    /** {@inheritDoc} */
    @Override
    public void rebind(final String name, final Object object) throws NamingException, RemoteException
    {
        this.embeddedContext.rebind(name, object);
    }

    /** {@inheritDoc} */
    @Override
    public void rebindObject(final String key, final Object object) throws NamingException, RemoteException
    {
        this.embeddedContext.rebindObject(key, object);
    }

    /** {@inheritDoc} */
    @Override
    public void rename(final String oldName, final String newName) throws NamingException, RemoteException
    {
        this.embeddedContext.rename(oldName, newName);
    }

    /** {@inheritDoc} */
    @Override
    public ContextInterface createSubcontext(final String name) throws NamingException, RemoteException
    {
        return this.embeddedContext.createSubcontext(name);
    }

    /** {@inheritDoc} */
    @Override
    public void destroySubcontext(final String name) throws NamingException, RemoteException
    {
        this.embeddedContext.destroySubcontext(name);
    }

    /** {@inheritDoc} */
    @Override
    public Set<String> keySet() throws RemoteException
    {
        return new LinkedHashSet<String>(this.embeddedContext.keySet());
    }

    /** {@inheritDoc} */
    @Override
    public Collection<Object> values() throws RemoteException
    {
        return new LinkedHashSet<Object>(this.embeddedContext.values());
    }

    /** {@inheritDoc} */
    @Override
    public Map<String, Object> bindings() throws RemoteException
    {
        return this.embeddedContext.bindings();
    }

    /** {@inheritDoc} */
    @Override
    public void checkCircular(final Object newObject) throws NamingException, RemoteException
    {
        this.embeddedContext.checkCircular(newObject);
    }

    /** {@inheritDoc} */
    @Override
    public void close() throws NamingException, RemoteException
    {
        // TODO: see if connection needs to be closed
        this.embeddedContext.close();
    }

    /** {@inheritDoc} */
    @Override
    public void fireObjectChangedEventValue(final Object object)
            throws NameNotFoundException, NullPointerException, NamingException, RemoteException
    {
        Throw.whenNull(object, "object cannot be null");
        fireObjectChangedEventKey(makeObjectKey(object));
    }

    /** {@inheritDoc} */
    @Override
    public void fireObjectChangedEventKey(final String key)
            throws NameNotFoundException, NullPointerException, NamingException, RemoteException
    {
        Throw.whenNull(key, "key cannot be null");
        Throw.when(key.length() == 0 || key.contains(ContextInterface.SEPARATOR), NamingException.class,
                "key [%s] is the empty string or key contains '/'", key);
        if (!hasKey(key))
        {
            throw new NameNotFoundException("Could not find object with key " + key + " for fireObjectChangedEvent");
        }
        try
        {
            this.remoteEventProducer.fireChangedEvent(ContextInterface.OBJECT_CHANGED_EVENT,
                    new Object[] {this, key, getObject(key)});
        }
        catch (Exception exception)
        {
            throw new NamingException(exception.getMessage());
        }
    }

    /**
     * Make a key for the object based on object.toString() where the "/" characters are replaced by "#".
     * @param object Object; the object for which the key has to be generated
     * @return the key based on toString() where the "/" characters are replaced by "#"
     */
    private String makeObjectKey(final Object object)
    {
        return object.toString().replace(ContextInterface.SEPARATOR, ContextInterface.REPLACE_SEPARATOR);
    }

    /** {@inheritDoc} */
    @Override
    public String toString()
    {
        if (this.embeddedContext != null)
            return this.embeddedContext.toString();
        return "RemoteContext[null]";
    }

    /** {@inheritDoc} */
    @Override
    public String toString(final boolean verbose) throws RemoteException
    {
        if (!verbose)
        {
            return "RemoteContext[" + getAtomicName() + "]";
        }
        return ContextUtil.toText(this);
    }

    /* ***************************************************************************************************************** */
    /* **************************************** EVENTPRODUCER IMPLEMENTATION ******************************************* */
    /* ***************************************************************************************************************** */

    /** {@inheritDoc} */
    @Override
    public synchronized boolean addListener(final EventListener listener, final EventType eventType) throws RemoteException
    {
        return this.embeddedContext.addListener(listener, eventType);
    }

    /** {@inheritDoc} */
    @Override
    public synchronized boolean addListener(final EventListener listener, final EventType eventType,
            final ReferenceType referenceType) throws RemoteException
    {
        return this.embeddedContext.addListener(listener, eventType, referenceType);
    }

    /** {@inheritDoc} */
    @Override
    public synchronized boolean addListener(final EventListener listener, final EventType eventType, final int position) throws RemoteException
    {
        return this.embeddedContext.addListener(listener, eventType, position);
    }

    /** {@inheritDoc} */
    @Override
    public synchronized boolean addListener(final EventListener listener, final EventType eventType, final int position,
            final ReferenceType referenceType) throws RemoteException
    {
        return this.embeddedContext.addListener(listener, eventType, position, referenceType);
    }

    /** {@inheritDoc} */
    @Override
    public synchronized int removeAllListeners() throws RemoteException
    {
        return this.remoteEventProducer.removeAllListeners();
    }

    /** {@inheritDoc} */
    @Override
    public synchronized int removeAllListeners(final Class<?> ofClass) throws RemoteException
    {
        return this.remoteEventProducer.removeAllListeners(ofClass);
    }

    /** {@inheritDoc} */
    @Override
    public synchronized boolean removeListener(final EventListener listener, final EventType eventType) throws RemoteException
    {
        return this.embeddedContext.removeListener(listener, eventType);
    }

    /** {@inheritDoc} */
    @Override
    public List<Reference<EventListener>> getListenerReferences(final EventType eventType)  throws RemoteException
    {
        return this.remoteEventProducer.getListenerReferences(eventType);
    }

    @Override
    public EventListenerMap getEventListenerMap() throws RemoteException
    {
        return this.remoteEventProducer.getEventListenerMap();
    }
    
    /* ***************************************************************************************************************** */
    /* ****************************************** REMOTECHANGEEVENTPRODUCER ******************************************** */
    /* ***************************************************************************************************************** */

    /**
     * The RemoteChangeEventProducer is a RmiEventProducer that can fire an OBJECT_CHANGED_EVENT on behalf of an object that was
     * changed, but does not extend an EventProducer itself.
     * <p>
     * Copyright (c) 2020-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved.
     * See for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>.
     * The DSOL project is distributed under a three-clause BSD-style license, which can be found at
     * <a href="https://https://simulation.tudelft.nl/dsol/docs/latest/license.html" target="_blank">
     * https://https://simulation.tudelft.nl/dsol/docs/latest/license.html</a>.
     * </p>
     * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
     */
    protected class RemoteChangeEventProducer extends RmiEventProducer
    {
        /** */
        private static final long serialVersionUID = 20200208L;

        /** the stored binding key that acts as the source id. */
        private final String bindingKey;

        /**
         * Create a remote event listener and register the listener in the RMI registry. When the RMI registry does not exist
         * yet, it will be created, but <b>only</b> on the local host. Remote creation of a registry on another computer is not
         * possible. Any attempt to do so will cause an AccessException to be fired.
         * @param host String; the host where the RMI registry resides or will be created. Creation is only possible on
         *            localhost.
         * @param port int; the port where the RMI registry can be found or will be created
         * @param bindingKey the key under which this object will be bound in the RMI registry
         * @throws RemoteException when there is a problem with the RMI registry
         * @throws AlreadyBoundException when there is already another object bound to the bindingKey
         * @throws NullPointerException when host, path, or bindingKey is null
         * @throws IllegalArgumentException when port &lt; 0 or port &gt; 65535
         * @throws AccessException when there is an attempt to create a registry on a remote host
         */
        public RemoteChangeEventProducer(final String host, final int port, final String bindingKey)
                throws RemoteException, AlreadyBoundException
        {
            super(host, port, bindingKey);
            this.bindingKey = bindingKey;
        }

        /**
         * Transmit an changed event with a serializable object as payload to all interested listeners.
         * @param eventType EventType; the eventType of the event
         * @param value Serializable; the object sent with the event
         * @throws RemoteException on network error
         */
        protected void fireChangedEvent(final EventType eventType, final Serializable value) throws RemoteException
        {
            super.fireEvent(eventType, value);
        }
    }

}