package org.dsa.iot.dslink.node;
import org.dsa.iot.dslink.link.Linkable;
import org.dsa.iot.dslink.node.NodeListener.ValueUpdate;
import org.dsa.iot.dslink.node.actions.Action;
import org.dsa.iot.dslink.node.value.Value;
import org.dsa.iot.dslink.node.value.ValuePair;
import org.dsa.iot.dslink.node.value.ValueType;
import org.dsa.iot.dslink.serializer.SerializationManager;
import org.dsa.iot.dslink.util.StringUtils;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Contains information about a node and its data.
*
* @author Samuel Grenier
*/
public class Node {
private static final char[] BANNED_CHARS = new char[] {
'%', '.', '/', '\\', '?', '*', ':', '|', '<', '>', '$', '@', ','
};
private final Object roConfigLock = new Object();
private final Object configLock = new Object();
private final Object attributeLock = new Object();
private final Object interfaceLock = new Object();
private final Object childrenLock = new Object();
private final Object passwordLock = new Object();
private final Object valueLock = new Object();
private final WeakReference<Node> parent;
private final Linkable link;
private final String path;
private final String name;
private boolean serializable = true;
private Map<String, Node> children;
private NodeListener listener;
private Writable writable;
private Object metaData;
private Map<String, Value> roConfigs;
private Map<String, Value> configs;
private Map<String, Value> attribs;
private Boolean hasChildren;
private boolean hidden;
private ValueType valueType;
private Value value;
private String displayName;
private String profile;
private Set<String> interfaces;
private Action action;
private char[] pass;
private boolean shouldPostCachedValue = true;
/**
* Constructs a node object.
*
* @param name Name of the node
* @param parent Parent of this node
* @param link Linkable class the node is handled on
*/
public Node(String name, Node parent, Linkable link) {
this(name, parent, link, true);
}
public Node(String name, Node parent, Linkable link, boolean shouldEncodeName) {
this.parent = new WeakReference<>(parent);
this.listener = new NodeListener(this);
this.link = link;
if (shouldEncodeName) {
name = StringUtils.encodeName(name);
}
if (name == null) {
throw new IllegalArgumentException("name");
}
if (parent != null) {
if (name.isEmpty()) {
throw new IllegalArgumentException("name");
}
this.name = name;
if (parent instanceof NodeManager.SuperRoot) {
this.path = "/" + name;
} else {
this.path = parent.getPath() + "/" + name;
}
} else {
this.path = "/" + name;
this.name = name;
}
}
/**
* @return Parent of this node, can be null if the parent was garbage
* collected or there is no parent.
*/
public Node getParent() {
return parent.get();
}
/**
* @return The link this node is attached to.
*/
public Linkable getLink() {
return link;
}
/**
* @return Encoded name of the node.
* @see StringUtils#decodeName(String)
*/
public String getName() {
return name;
}
/**
* @return Formalized path of this node.
*/
public String getPath() {
return path;
}
/**
* @param name Display name of the node to set
*/
public void setDisplayName(String name) {
displayName = name;
markChanged();
if (link != null) {
SubscriptionManager man = link.getSubscriptionManager();
if (name != null) {
man.postMetaUpdate(this, "$name", new Value(displayName));
} else {
man.postMetaUpdate(this, "$name", null);
}
}
}
/**
* @return Display name of the node
*/
public String getDisplayName() {
return displayName;
}
/**
* Sets the profile of the node
*
* @param profile Profile to set
*/
public void setProfile(String profile) {
this.profile = profile;
markChanged();
}
/**
* @return The profile this node belongs to
*/
public String getProfile() {
return profile;
}
/**
* The listener API provides functionality for listening to changes
* that occur within a node.
*
* @return The node's listener.
*/
public NodeListener getListener() {
return listener;
}
/**
* Used to set the listener to allow the node builder to override
* the internal listener.
*
* @param listener Listener to set.
*/
protected void setListener(NodeListener listener) {
if (listener == null) {
throw new NullPointerException("listener");
}
this.listener = listener;
}
public void addInterface(String _interface) {
synchronized (interfaceLock) {
if (_interface == null) {
throw new NullPointerException("_interface");
} else if (interfaces == null) {
interfaces = new HashSet<>();
}
interfaces.add(_interface);
markChanged();
}
}
@SuppressWarnings("unused")
public void removeInterface(String _interface) {
synchronized (interfaceLock) {
if (_interface == null) {
throw new NullPointerException("_interface");
} else if (interfaces != null) {
interfaces.remove(_interface);
markChanged();
}
}
}
public void setInterfaces(String interfaces) {
synchronized (interfaceLock) {
if (interfaces == null) {
this.interfaces = null;
return;
} else if (this.interfaces == null) {
this.interfaces = new HashSet<>();
}
String[] split = interfaces.split("\\|");
Collections.addAll(this.interfaces, split);
markChanged();
}
}
public Set<String> getInterfaces() {
Set<String> i = this.interfaces;
return i != null ? Collections.unmodifiableSet(i) : null;
}
public void setValue(Value value) {
setValue(value, false);
}
public void setValue(Value value, boolean externalSource) {
setValue(value, externalSource, true);
}
/**
* @param value Value to set.
* @param externalSource Whether the value was set from an external source
* like an action that got invoked.
* @param publish Whether to allow a publish to the network.
* @return Whether a value was actually set.
*/
protected boolean setValue(Value value,
boolean externalSource,
boolean publish) {
ValueType type = valueType;
if (type == null && value != null) {
String err = "Value type not set on node (" + getPath() + ")";
throw new RuntimeException(err);
}
ValuePair pair;
synchronized (valueLock) {
pair = new ValuePair(this.value, value, externalSource);
}
if (listener.postValueUpdate(pair)) {
return false;
}
value = pair.getCurrent();
if (value != null) {
if (type == null) {
String err = "Value type not set on node (" + getPath() + ")";
throw new RuntimeException(err);
}
value.setImmutable();
if (type.compare(ValueType.ENUM)) {
if (!value.getType().compare(ValueType.STRING)) {
String err = "[" + getPath() + "] ";
err += "Node has enum value type, value must be string";
throw new RuntimeException(err);
} else if (type.getEnums() == null
|| !type.getEnums().contains(value.getString())) {
if (value.getString() != null) {
String err = "[" + getPath() + "] ";
err += "New value does not contain a valid enum value";
throw new RuntimeException(err);
}
}
} else if (type.compare(ValueType.TIME)) {
if (!value.getType().compare(ValueType.STRING)) {
String err = "[" + getPath() + "] ";
err += "Node has time value type, value must be string";
throw new RuntimeException(err);
}
} else if (!(type.compare(ValueType.DYNAMIC)
|| type.compare(value.getType()))) {
String err = "[" + getPath() + "] ";
err += "Expected value type ";
err += "'" + type.toJsonString() + "' ";
err += "got '" + value.getType().toJsonString() + "'";
throw new RuntimeException(err);
}
}
synchronized (valueLock) {
Value prev = this.value;
this.value = value;
if ((prev != null && prev.isSerializable())
|| (value != null && value.isSerializable())
|| (prev == null && value == null)) {
markChanged();
}
if (publish && link != null) {
SubscriptionManager manager = link.getSubscriptionManager();
if (manager != null) {
manager.postValueUpdate(this);
}
}
}
return true;
}
/**
* @return The value of the node.
*/
public Value getValue() {
return value;
}
public void setValueType(ValueType type) {
this.valueType = type;
markChanged();
if (link != null) {
SubscriptionManager man = link.getSubscriptionManager();
if (type != null) {
String t = type.toJsonString();
man.postMetaUpdate(this, "$type", new Value(t));
} else {
man.postMetaUpdate(this, "$type", null);
}
}
}
public ValueType getValueType() {
return valueType;
}
/**
* @param writable Permission level required to write.
*/
public void setWritable(Writable writable) {
this.writable = writable;
markChanged();
}
/**
* @return The permission level needed to be writable.
*/
public Writable getWritable() {
return writable;
}
/**
* @return Whether the node's value should automatically
* be posted in response to a subscription request.
*/
public boolean shouldPostCachedValue() {
return shouldPostCachedValue;
}
/**
* @param should Whether the node's value should
* automatically be posted in response to a
* subscription request. Defaults to true.
*/
public void setShouldPostCachedValue(boolean should) {
shouldPostCachedValue = should;
}
/**
* @return Children of the node, can be null
*/
public Map<String, Node> getChildren() {
Map<String, Node> children = this.children;
return children != null ? Collections.unmodifiableMap(children) : null;
}
/**
* Clears the children in the node.
*/
@SuppressWarnings("unused")
public void clearChildren() {
synchronized (childrenLock) {
if (children != null) {
Map<String, Node> children = getChildren();
for (Node child : children.values()) {
removeChild(child);
}
}
markChanged();
}
}
/**
* @param name Child name
* @return Child, or null if non-existent
*/
@Deprecated
public Node getChild(String name) {
Map<String, Node> children = this.children;
if (children != null) {
name = StringUtils.encodeName(name);
return children.get(name);
}
return null;
}
public Node getChild(String name, boolean encodeName) {
Map<String, Node> children = this.children;
if (children != null) {
if (encodeName) {
name = StringUtils.encodeName(name);
}
return children.get(name);
}
return null;
}
/**
* Creates a child. The profile in the child node will be
* inherited from the parent.
*
* @param name Name of the child
* @return builder
*/
@Deprecated
public NodeBuilder createChild(String name) {
return createChild(name, profile);
}
public NodeBuilder createChild(String name, boolean encodeName) {
return createChild(name, profile, encodeName);
}
/**
* Creates a node builder to allow setting up the node data before
* any list subscriptions can be notified.
*
* @param name Name of the child.
* @param profile Profile to set on the child
* @return builder
* @see NodeBuilder#build
*/
public NodeBuilder createChild(String name, String profile) {
NodeBuilder b = new NodeBuilder(this, new Node(name, this, link));
if (profile != null) {
b.setProfile(profile);
}
return b;
}
public NodeBuilder createChild(String name, String profile, boolean encodeName) {
NodeBuilder b = new NodeBuilder(this, new Node(name, this, link, encodeName));
if (profile != null) {
b.setProfile(profile);
}
return b;
}
/**
* The child will be added if the node doesn't exist. If the child
* already exists then it will be returned and no new node will be
* created. This can be used as a special getter.
*
* @param node Child node to add.
* @return The node
*/
public Node addChild(Node node) {
synchronized (childrenLock) {
String name = node.getName();
maybeInitializeChildren();
if (children.containsKey(name)) {
return children.get(name);
}
SubscriptionManager manager = null;
if (link != null) {
manager = link.getSubscriptionManager();
}
if (node.getProfile() == null) {
node.setProfile(profile);
}
children.put(name, node);
if (manager != null) {
manager.postChildUpdate(node, false);
}
if (node.isSerializable()) {
markChanged();
}
return node;
}
}
/**
* Add multiple children at once.
* @param nodes Nodes to add.
*/
public void addChildren(List<Node> nodes) {
SubscriptionManager manager = null;
if (link != null) {
manager = link.getSubscriptionManager();
}
boolean reserialize = false;
synchronized (childrenLock) {
for (Node node : nodes) {
String name = node.getName();
maybeInitializeChildren();
if (children.containsKey(name)) {
continue;
}
node.maybeInitializeProfile(profile);
children.put(name, node);
if (node.isSerializable()) {
reserialize = true;
}
}
}
if (manager != null) {
manager.postMultiChildUpdate(this, nodes);
}
if (reserialize) {
markChanged();
}
}
private void maybeInitializeProfile(String profile) {
if (getProfile() == null) {
setProfile(profile);
}
}
private void maybeInitializeChildren() {
if (children == null) {
children = new ConcurrentHashMap<>();
}
}
/**
* Deletes this node from its parent.
*/
@Deprecated
public void delete() {
Node parent = getParent();
if (parent != null) {
parent.removeChild(this);
}
}
public void delete(boolean encodeName) {
Node parent = getParent();
if (parent != null) {
parent.removeChild(this, encodeName);
}
}
/**
* @param node Node to remove.
* @return The node if it existed.
*/
@Deprecated
public Node removeChild(Node node) {
if (node != null) {
return removeChild(node.getName());
} else {
return null;
}
}
public Node removeChild(Node node, boolean encodeName) {
if (node != null) {
return removeChild(node.getName(), encodeName);
} else {
return null;
}
}
/**
* @param name Node to remove.
* @return The node if it existed.
*/
@Deprecated
public Node removeChild(String name) {
return removeChild(name, true);
}
public Node removeChild(String name, boolean encodeName) {
synchronized (childrenLock) {
if (encodeName) {
name = StringUtils.encodeName(name);
}
Node child = children != null ? children.remove(name) : null;
SubscriptionManager manager = null;
if (link != null) {
manager = link.getSubscriptionManager();
}
if (child != null) {
child.getListener().postNodeRemoved();
child.getListener().kill();
if (manager != null) {
manager.postChildUpdate(child, true);
manager.removeValueSub(child);
manager.removePathSub(child);
}
if (isSerializable()) {
markChanged();
}
}
return child;
}
}
/**
* @param name Name of the child.
* @return Whether this node has the child or not.
*/
@Deprecated
public boolean hasChild(String name) {
Map<String, Node> children = this.children;
if (children != null) {
name = StringUtils.encodeName(name);
return children.containsKey(name);
}
return false;
}
public boolean hasChild(String name, boolean encodeName) {
Map<String, Node> children = this.children;
if (children != null) {
if (encodeName) {
name = StringUtils.encodeName(name);
}
return children.containsKey(name);
}
return false;
}
/**
* @return The configurations in this node.
*/
public Map<String, Value> getConfigurations() {
Map<String, Value> c = this.configs;
return c != null ? Collections.unmodifiableMap(c) : null;
}
/**
* @param name Configuration name to get
* @return Value of the configuration, if it exists
*/
public Value getConfig(String name) {
Map<String, Value> c = configs;
if (c != null) {
name = StringUtils.encodeName(name);
return c.get(name);
}
return null;
}
/**
* @param name Configuration name to remove
* @return Configuration value, or null if it didn't exist
*/
public Value removeConfig(String name) {
name = StringUtils.encodeName(name);
Value ret;
synchronized (configLock) {
ret = configs != null ? configs.remove(name) : null;
}
postRemoval("$", name, ret);
return ret;
}
/**
* Clears all the configurations from the node.
*
* @return All the previous configurations.
*/
public Map<String, Value> clearConfigs() {
Map<String, Value> configs;
synchronized (configLock) {
if (this.configs == null) {
return null;
}
configs = new HashMap<>(this.configs);
this.configs.clear();
}
for (Map.Entry<String, Value> entry : configs.entrySet()) {
postRemoval("$", entry.getKey(), entry.getValue());
}
return configs;
}
/**
* The name will be checked for validity. Certain names that are set
* through other APIs cannot be set here, otherwise it will throw an
* exception.
*
* @param name Name of the configuration
* @param value Value to set
* @return The previous configuration value, if any
* @see Action
*/
public Value setConfig(String name, Value value) {
synchronized (configLock) {
name = checkAndEncodeName(name);
if (value == null) {
throw new NullPointerException("value");
} else if (configs == null) {
configs = new ConcurrentHashMap<>();
}
switch (name) {
case "params":
case "columns":
case "name":
case "is":
case "invokable":
case "interface":
case "permission":
case "result":
case "type":
case "writable":
case "hidden":
String err = "Config `" + name + "` has special methods"
+ " for setting these properties";
throw new IllegalArgumentException(err);
}
value.setImmutable();
ValueUpdate update = new ValueUpdate(name, value, false);
NodeListener listener = this.listener;
if (listener != null) {
listener.postConfigUpdate(update);
}
SubscriptionManager man = link.getSubscriptionManager();
if (man != null) {
man.postMetaUpdate(this, "$" + name, value);
}
markChanged();
return configs.put(name, value);
}
}
/**
* @return The read-only configurations in this node.
*/
public Map<String, Value> getRoConfigurations() {
Map<String, Value> c = this.roConfigs;
return c != null ? Collections.unmodifiableMap(c) : null;
}
/**
* Removes a read-only configuration.
*
* @param name Name of the configuration.
* @return Previous value of the configuration, if any.
*/
public Value removeRoConfig(String name) {
name = StringUtils.encodeName(name);
Value ret;
synchronized (roConfigLock) {
ret = roConfigs != null ? roConfigs.remove(name) : null;
}
postRemoval("$$", name, ret);
return ret;
}
/**
* Clears all the read-only configurations from the node.
*
* @return All the previous read-only configurations.
*/
public Map<String, Value> clearRoConfigs() {
Map<String, Value> roConfigs;
synchronized (roConfigLock) {
if (this.roConfigs == null) {
return null;
}
roConfigs = new HashMap<>(this.roConfigs);
this.roConfigs.clear();
}
for (Map.Entry<String, Value> entry : roConfigs.entrySet()) {
postRemoval("$$", entry.getKey(), entry.getValue());
}
return roConfigs;
}
/**
* Retrieves a read-only configuration.
*
* @param name Name of the configuration.
* @return The value of the configuration name, if any.
*/
@SuppressWarnings("unused")
public Value getRoConfig(String name) {
Map<String, Value> c = roConfigs;
if (c != null) {
name = StringUtils.encodeName(name);
return c.get(name);
}
return null;
}
/**
* Sets a read-only configuration.
*
* @param name Name of the configuration.
* @param value Value to set.
* @return The previous value, if any.
*/
public Value setRoConfig(String name, Value value) {
synchronized (roConfigLock) {
name = checkAndEncodeName(name);
if (value == null) {
throw new NullPointerException("value");
} else if (roConfigs == null) {
roConfigs = new ConcurrentHashMap<>();
}
switch (name) {
case "password":
String err = "Config `" + name + "` has special methods"
+ " for setting these properties";
throw new IllegalArgumentException(err);
}
SubscriptionManager man = link.getSubscriptionManager();
if (man != null) {
man.postMetaUpdate(this, "$$" + name, value);
}
markChanged();
return roConfigs.put(name, value);
}
}
/**
* @return The attributes in this node.
*/
public Map<String, Value> getAttributes() {
Map<String, Value> a = attribs;
return a != null ? Collections.unmodifiableMap(a) : null;
}
/**
* @param name Attribute name to get
* @return Value of the attribute, if it exists
*/
public Value getAttribute(String name) {
Map<String, Value> a = attribs;
if (a != null) {
name = StringUtils.encodeName(name);
return a.get(name);
}
return null;
}
/**
* @param name Attribute name to remove.
* @return Attribute value or null if it didn't exist
*/
public Value removeAttribute(String name) {
name = StringUtils.encodeName(name);
Value ret;
synchronized (attributeLock) {
ret = attribs != null ? attribs.remove(name) : null;
}
postRemoval("@", name, ret);
return ret;
}
/**
* Clears all the attributes in the node.
*
* @return All the previous attributes that were cleared.
*/
public Map<String, Value> clearAttributes() {
Map<String, Value> attribs;
synchronized (attributeLock) {
if (this.attribs == null) {
return null;
}
attribs = new HashMap<>(this.attribs);
this.attribs.clear();
}
for (Map.Entry<String, Value> entry : attribs.entrySet()) {
postRemoval("@", entry.getKey(), entry.getValue());
}
return attribs;
}
/**
* @param name Name of the attribute
* @param value Value to set
* @return The previous attribute value, if any
*/
public Value setAttribute(String name, Value value) {
synchronized (attributeLock) {
name = checkAndEncodeName(name);
if (value == null) {
throw new NullPointerException("value");
} else if (attribs == null) {
attribs = new ConcurrentHashMap<>();
}
value.setImmutable();
ValueUpdate update = new ValueUpdate(name, value, false);
listener.postAttributeUpdate(update);
SubscriptionManager man = link.getSubscriptionManager();
if (man != null) {
man.postMetaUpdate(this, "@" + name, value);
}
markChanged();
return attribs.put(name, value);
}
}
/**
* @return Action this node can invoke
*/
public Action getAction() {
return action;
}
/**
* Sets the action of the node.
*
* @param action Action to set. Use {@code null} to remove an action.
*/
public void setAction(Action action) {
this.action = action;
markChanged();
if (link == null) {
return;
}
SubscriptionManager man = link.getSubscriptionManager();
if (man != null) {
if (!(action == null || action.isHidden())) {
Value params = new Value(action.getParams());
Value cols = new Value(action.getColumns());
man.postMetaUpdate(this, "$params", params);
man.postMetaUpdate(this, "$columns", cols);
action.setSubscriptionManager(this, man);
} else {
man.postMetaUpdate(this, "$params", null);
man.postMetaUpdate(this, "$columns", null);
}
}
}
/**
* Gets the password the node is configured to use. This is necessary
* for authentication to servers.
*
* @return Password the node is configured to use.
*/
public char[] getPassword() {
synchronized (passwordLock) {
return pass != null ? pass.clone() : null;
}
}
/**
* If this node accesses servers and requires authentication, the password
* must be set here. This will censor the password from being retrieved
* through the responder.
*
* @param password Password to set.
*/
public void setPassword(char[] password) {
synchronized (passwordLock) {
this.pass = password != null ? password.clone() : null;
markChanged();
}
}
/**
* Forcibly sets whether the node has children or not. This does <b>NOT</b>
* affect whether the node has children internally.
*
* @param hasChildren Whether the node hsa children or not.
*/
public void setHasChildren(Boolean hasChildren) {
this.hasChildren = hasChildren;
markChanged();
}
/**
* Checks whether the node is forced to have children or not. This does
* <b>NOT</b> check if the node has children internally. To check whether
* the node has children internally, use {@link #getChildren()}.
*
* @return Whether the node has children or not.
*/
public Boolean getHasChildren() {
return hasChildren;
}
/**
* @param hidden Whether the node is marked as hidden.
*/
public void setHidden(boolean hidden) {
this.hidden = hidden;
markChanged();
}
/**
* When a node is marked as hidden the UI should not display
* the node.
*
* @return Whether the node is marked as hidden.
*/
public boolean isHidden() {
return hidden;
}
/**
* Creates a fake node builder that wraps its methods around
* this node. This allows fitting a {@link Node} into a {@link NodeBuilder}
* when necessary.
*
* @return A fake node builder.
*/
public NodeBuilder createFakeBuilder() {
return new NodeBuilder(getParent(), this) {
@Override
public Node build() {
return Node.this;
}
};
}
/**
* If this node is not serializable, none of the children will be either
* by default.
*
* @return Whether this node should be serialized or not
*/
public boolean isSerializable() {
return serializable;
}
/**
* Sets whether this node and its children should be serialized.
*
* @param serializable Whether this node can be serialized.
*/
public void setSerializable(boolean serializable) {
this.serializable = serializable;
markChanged();
}
/**
* Sets the meta data of the node. Used for attaching extra information
* to a node. This meta data is not serialized. The sole purpose of meta
* data is to attach a custom instance that operates on this node.
*
* @param object Meta data object.
*/
public void setMetaData(Object object) {
if (object instanceof MetaData) {
((MetaData) object).setNode(this);
}
this.metaData = object;
}
/**
* @param <T> Meta data to cast to.
* @return The attached meta data of this node.
*/
@SuppressWarnings("unchecked")
public <T> T getMetaData() {
return (T) metaData;
}
/**
* Resets the node's exposed data.
*/
public void reset() {
clearChildren();
clearConfigs();
clearRoConfigs();
clearAttributes();
setPassword(null);
setDisplayName(null);
setAction(null);
setInterfaces(null);
setValue(null);
setValueType(null);
setWritable(null);
}
private void markChanged() {
if (!isSerializable()) {
return;
}
Linkable link = getLink();
if (link != null) {
SerializationManager sm = link.getSerialManager();
if (sm != null) {
sm.markChanged();
}
}
}
private void postRemoval(String prefix, String name, Value value) {
if (value == null) {
return;
}
ValueUpdate update = new ValueUpdate(name, value, true);
if ("$".equals(prefix)) {
listener.postConfigUpdate(update);
} else if ("@".equals(prefix)) {
listener.postAttributeUpdate(update);
}
SubscriptionManager man = link.getSubscriptionManager();
if (man != null) {
man.postMetaUpdate(this, prefix + name, null);
}
markChanged();
}
/**
* Checks the string and then returns it. An exception is thrown if the
* name is invalid in any way.
*
* @param name Name to check
* @return Name
*/
public static String checkAndEncodeName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("name");
}
return StringUtils.encodeName(name);
}
/**
* @return The banned characters not allowed to be in names.
*/
public static char[] getBannedCharacters() {
return BANNED_CHARS.clone();
}
}