JvmContext.java

package nl.tudelft.simulation.naming.context;

import java.rmi.RemoteException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.naming.NameAlreadyBoundException;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.naming.NotContextException;

import org.djutils.event.LocalEventProducer;
import org.djutils.exceptions.Throw;
import org.djutils.logger.CategoryLogger;

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

/**
 * The JvmContext is an in-memory, thread-safe context implementation of the ContextInterface.
 * <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>
 */
public class JvmContext extends LocalEventProducer implements ContextInterface
{
    /** */
    private static final long serialVersionUID = 20200101L;

    /** the parent context. */
    protected ContextInterface parent;

    /** the atomicName. */
    private String atomicName;

    /** the absolute path of this context. */
    private String absolutePath;

    /** the children. */
    protected Map<String, Object> elements = Collections.synchronizedMap(new TreeMap<String, Object>());

    /**
     * constructs a new root JvmContext.
     * @param atomicName String; the name under which the root context will be registered
     */
    public JvmContext(final String atomicName)
    {
        this(null, atomicName);
    }

    /**
     * Constructs a new JvmContext.
     * @param parent Context; the parent context
     * @param atomicName String; the name under which the context will be registered
     */
    public JvmContext(final ContextInterface parent, final String atomicName)
    {
        Throw.whenNull(atomicName, "name under which a context is registered cannot be null");
        Throw.when(atomicName.contains(ContextInterface.SEPARATOR), IllegalArgumentException.class,
                "name %s under which a context is registered cannot contain the separator string %s", atomicName,
                ContextInterface.SEPARATOR);
        this.parent = parent;
        this.atomicName = atomicName;
        try
        {
            this.absolutePath = parent == null ? "" : parent.getAbsolutePath() + ContextInterface.SEPARATOR + this.atomicName;
        }
        catch (RemoteException exception)
        {
            CategoryLogger.always().warn(exception);
            throw new RuntimeException(exception);
        }
    }

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

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

    /** {@inheritDoc} */
    @Override
    public ContextInterface getRootContext() throws RemoteException
    {
        ContextInterface result = this;
        while (result.getParent() != null)
        {
            result = result.getParent();
        }
        return result;
    }

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

    /** {@inheritDoc} */
    @Override
    public Object getObject(final String key) throws 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 (!this.elements.containsKey(key))
        {
            throw new NameNotFoundException("key " + key + " does not exist in Context");
        }
        // can be null -- null objects are allowed in the context tree
        return this.elements.get(key);
    }

    /** {@inheritDoc} */
    @Override
    public Object get(final String name) throws NamingException, RemoteException
    {
        ContextName contextName = lookup(name);
        if (contextName.getName().length() == 0)
        {
            return contextName.getContext();
        }
        Object result = contextName.getContext().getObject(contextName.getName());
        return result;
    }

    /** {@inheritDoc} */
    @Override
    public boolean exists(final String name) throws NamingException, RemoteException
    {
        ContextName contextName = lookup(name);
        if (contextName.getName().length() == 0)
        {
            return true;
        }
        return contextName.getContext().hasKey(contextName.getName());
    }

    /** {@inheritDoc} */
    @Override
    public boolean hasKey(final String key) throws 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);
        return this.elements.containsKey(key);
    }

    /**
     * Indicates whether the object has been registered (once or more) in the current Context. The object may be null.
     * @param object Object; the object to look up; mey be null
     * @return boolean; whether an object with the given key has been registered once or more in the current context
     */
    /** {@inheritDoc} */
    @Override
    public boolean hasObject(final Object object) throws RemoteException
    {
        return this.elements.containsValue(object);
    }

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

    /** {@inheritDoc} */
    @Override
    public void bindObject(final String key, final Object object) throws 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 (this.elements.containsKey(key))
        {
            throw new NameAlreadyBoundException("key " + key + " already bound to object in Context");
        }
        checkCircular(object);
        this.elements.put(key, object);
        fireEvent(ContextInterface.OBJECT_ADDED_EVENT, new Object[] {getAbsolutePath(), key, object});
    }

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

    /** {@inheritDoc} */
    @Override
    public void bind(final String name, final Object object) throws NamingException, RemoteException
    {
        ContextName contextName = lookup(name);
        contextName.getContext().bindObject(contextName.getName(), object);
    }

    /** {@inheritDoc} */
    @Override
    public void unbindObject(final String key) throws 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 (this.elements.containsKey(key))
        {
            Object object = this.elements.remove(key);
            fireEvent(ContextInterface.OBJECT_REMOVED_EVENT, new Object[] {getAbsolutePath(), key, object});
        }
    }

    /** {@inheritDoc} */
    @Override
    public void unbind(final String name) throws NamingException, RemoteException
    {
        ContextName contextName = lookup(name);
        contextName.getContext().unbindObject(contextName.getName());
    }

    /** {@inheritDoc} */
    @Override
    public void rebindObject(final String key, final Object object) throws 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);
        checkCircular(object);
        if (this.elements.containsKey(key))
        {
            this.elements.remove(key);
            fireEvent(ContextInterface.OBJECT_REMOVED_EVENT, new Object[] {getAbsolutePath(), key, object});
        }
        this.elements.put(key, object);
        fireEvent(ContextInterface.OBJECT_ADDED_EVENT, new Object[] {getAbsolutePath(), key, object});
    }

    /** {@inheritDoc} */
    @Override
    public void rebind(final String name, final Object object) throws NamingException, RemoteException
    {
        ContextName contextName = lookup(name);
        contextName.getContext().rebindObject(contextName.getName(), object);
    }

    /** {@inheritDoc} */
    @Override
    public void rename(final String oldName, final String newName) throws NamingException, RemoteException
    {
        ContextName contextNameOld = lookup(oldName);
        ContextName contextNameNew = lookup(newName);
        if (contextNameNew.getContext().hasKey(contextNameNew.getName()))
        {
            throw new NameAlreadyBoundException("key " + newName + " already bound to object in Context");
        }
        Object object = contextNameOld.getContext().getObject(contextNameOld.getName());
        contextNameNew.getContext().checkCircular(object);
        contextNameOld.getContext().unbindObject(contextNameOld.getName());
        contextNameNew.getContext().bindObject(contextNameNew.getName(), object);
    }

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

    /** {@inheritDoc} */
    @Override
    public void destroySubcontext(final String name) throws NamingException, RemoteException
    {
        ContextName contextName = lookup(name);
        Object object = contextName.getContext().getObject(contextName.getName());
        if (!(object instanceof ContextInterface))
        {
            throw new NotContextException("name " + name + " is bound but does not name a context");
        }
        destroy((ContextInterface) object);
        contextName.getContext().unbindObject(contextName.getName());
    }

    /**
     * Take a (compound) name such as "sub1/sub2/key" or "key" or "/sub/key" and lookup and/or create all intermediate contexts
     * as well as the final sub-context of the path.
     * @param name the (possibly compound) name
     * @return the context, possibly built new
     * @throws NamingException as a placeholder overarching exception
     * @throws RemoteException when the JVM context is used over a network and a network error occurs
     * @throws NameNotFoundException when an intermediate context does not exist
     * @throws NullPointerException when name is null
     */
    protected ContextInterface lookupAndBuild(final String name) throws NamingException, RemoteException
    {
        Throw.whenNull(name, "name cannot be null");

        // Handle current context lookup
        if (name.length() == 0 || name.equals(ContextInterface.SEPARATOR))
        {
            throw new NamingException("the terminal reference is '/' or empty");
        }

        // determine absolute or relative path
        String reference;
        ContextInterface subContext;
        if (name.startsWith(ContextInterface.ROOT))
        {
            reference = name.substring(ContextInterface.SEPARATOR.length());
            subContext = getRootContext();
        }
        else
        {
            reference = name;
            subContext = this;
        }

        while (true)
        {
            int index = reference.indexOf(ContextInterface.SEPARATOR);
            if (index == -1)
            {
                ContextInterface newContext = new JvmContext(subContext, reference);
                subContext.bind(reference, newContext);
                subContext = newContext;
                break;
            }
            String sub = reference.substring(0, index);
            reference = reference.substring(index + ContextInterface.SEPARATOR.length());
            if (!subContext.hasKey(sub))
            {
                ContextInterface newContext = new JvmContext(subContext, sub);
                subContext.bind(sub, newContext);
                subContext = newContext;
            }
            else
            {
                Object subObject = subContext.getObject(sub); // can throw NameNotFoundException
                if (!(subObject instanceof ContextInterface))
                {
                    throw new NameNotFoundException(
                            "parsing name " + name + " in context -- bound object " + sub + " is not a subcontext");
                }
                subContext = (ContextInterface) subObject;
            }
        }
        return subContext;
    }

    /**
     * Recursively unbind and destroy all keys and subcontexts from the given context, leaving it empty. All removals will fire
     * an OBJECT_REMOVED event, depth first.
     * @param context the context to empty
     * @throws NamingException on tree inconsistencies
     * @throws RemoteException on RMI error
     */
    protected void destroy(final ContextInterface context) throws RemoteException, NamingException
    {
        // depth-first subcontext removal
        Set<String> copyKeySet = new LinkedHashSet<>(context.keySet());
        for (String key : copyKeySet)
        {
            if (context.getObject(key) instanceof ContextInterface && context.getObject(key) != null)
            {
                destroy((ContextInterface) context.getObject(key));
                context.unbindObject(key);
            }
        }

        // leaf removal
        copyKeySet = new LinkedHashSet<>(context.keySet());
        for (String key : copyKeySet)
        {
            if (context.getObject(key) instanceof ContextInterface)
            {
                throw new NamingException("Tree inconsistent -- Context not removed or added during destroy operation");
            }
            context.unbindObject(key);
        }
    }

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

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

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

    /** {@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
        {
            fireEvent(ContextInterface.OBJECT_CHANGED_EVENT, new Object[] {getAbsolutePath(), 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 void checkCircular(final Object newObject) throws NamingException, RemoteException
    {
        if (!(newObject instanceof ContextInterface))
            return;
        for (ContextInterface parentContext = getParent(); parentContext != null; parentContext = parentContext.getParent())
        {
            if (parentContext.equals(newObject))
            {
                throw new NamingException(
                        "circular reference for object " + newObject + "; prevented insertion into " + toString());
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    public void close() throws NamingException, RemoteException
    {
        this.elements.clear();
        this.atomicName = "";
        this.parent = null;
    }

    /** {@inheritDoc} */
    @Override
    public String toString()
    {
        String parentName;
        try
        {
            parentName = this.parent == null ? "null" : this.parent.getAtomicName();
        }
        catch (RemoteException exception)
        {
            parentName = "unreachable";
        }
        return "JvmContext[parent=" + parentName + ", atomicName=" + this.atomicName + "]";
    }

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

    /**
     * Take a (compound) name such as "sub1/sub2/key" or "key" or "/sub/key" and lookup the final sub-context of the path, and
     * store it in the ContextName. Store the key String in the ContextName without checking whether it exists.
     * @param name the (possibly compound) name
     * @return a ContextName combination string the subcontext and final reference name
     * @throws NamingException as a placeholder overarching exception
     * @throws NameNotFoundException when an intermediate context does not exist
     * @throws RemoteException when the JVM context is used over a network and a network error occurs
     * @throws NullPointerException when name is null
     */
    protected ContextName lookup(final String name) throws NamingException, RemoteException
    {
        Throw.whenNull(name, "name cannot be null");

        // Handle current context lookup
        if (name.length() == 0)
        {
            return new ContextName(this, "");
        }

        // determine absolute or relative path
        String reference;
        ContextInterface subContext;
        if (name.startsWith(ContextInterface.ROOT))
        {
            reference = name.substring(ContextInterface.SEPARATOR.length());
            subContext = getRootContext();
        }
        else
        {
            reference = name;
            subContext = this;
        }

        while (true)
        {
            int index = reference.indexOf(ContextInterface.SEPARATOR);
            if (index == -1)
            {
                break;
            }
            String sub = reference.substring(0, index);
            reference = reference.substring(index + ContextInterface.SEPARATOR.length());
            Object subObject = subContext.getObject(sub); // can throw NameNotFoundException
            if (!(subObject instanceof ContextInterface))
            {
                throw new NameNotFoundException(
                        "parsing name " + name + " in context -- bound object " + sub + " is not a subcontext");
            }
            subContext = (ContextInterface) subObject;
        }
        return new ContextName(subContext, reference);
    }

    /**
     * Record with Context and Name combination.
     * <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>
     */
    public static class ContextName
    {
        /** the context. */
        private final ContextInterface context;

        /** the name (key) in the context. */
        private final String name;

        /**
         * Instantiate a Context and Name combination.
         * @param context the context
         * @param name the name (key) in the context
         */
        public ContextName(final ContextInterface context, final String name)
        {
            this.context = context;
            this.name = name;
        }

        /**
         * @return context
         */
        public ContextInterface getContext()
        {
            return this.context;
        }

        /**
         * @return name
         */
        public String getName()
        {
            return this.name;
        }

        /** {@inheritDoc} */
        @Override
        public int hashCode()
        {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((this.context == null) ? 0 : this.context.hashCode());
            result = prime * result + ((this.name == null) ? 0 : this.name.hashCode());
            return result;
        }

        /** {@inheritDoc} */
        @Override
        public boolean equals(final Object obj)
        {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            ContextName other = (ContextName) obj;
            if (this.context == null)
            {
                if (other.context != null)
                    return false;
            }
            else if (!this.context.equals(other.context))
                return false;
            if (this.name == null)
            {
                if (other.name != null)
                    return false;
            }
            else if (!this.name.equals(other.name))
                return false;
            return true;
        }

        /** {@inheritDoc} */
        @Override
        public String toString()
        {
            return "ContextName[context=" + this.context + ", name=" + this.name + "]";
        }
    }

}