package nl.tudelft.simulation.naming.context;
import java.io.Serializable;
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.EventProducer;
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.
*
* Copyright (c) 2020-2021 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
* for project information https://simulation.tudelft.nl. The DSOL
* project is distributed under a three-clause BSD-style license, which can be found at
*
* https://simulation.tudelft.nl/dsol/3.0/license.html.
*
* @author Alexander Verbraeck
*/
public class JVMContext extends EventProducer 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 elements = Collections.synchronizedMap(new TreeMap());
/**
* 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);
}
/** {@inheritDoc} */
@Override
public Serializable getSourceId()
{
return this.absolutePath;
}
/**
* 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(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(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 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 keySet() throws RemoteException
{
return this.elements.keySet();
}
/** {@inheritDoc} */
@Override
public Collection