STCNamespace.java
package de.dlr.bt.stc.opcuaserver;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.eclipse.milo.opcua.sdk.core.AccessLevel;
import org.eclipse.milo.opcua.sdk.core.Reference;
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.api.DataItem;
import org.eclipse.milo.opcua.sdk.server.api.ManagedNamespaceWithLifecycle;
import org.eclipse.milo.opcua.sdk.server.api.MonitoredItem;
import org.eclipse.milo.opcua.sdk.server.api.methods.AbstractMethodInvocationHandler;
import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectTypeNode;
import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode;
import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilterContext;
import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilters;
import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import de.dlr.bt.stc.config.ACfg;
import de.dlr.bt.stc.eventbus.ConfigurationModifiedEvent;
import de.dlr.bt.stc.eventbus.ConfigurationModifiedEvent.ConfigurationModificationType;
import de.dlr.bt.stc.eventbus.TaskModifiedEvent;
import de.dlr.bt.stc.eventbus.TaskModifiedEvent.TaskModificationType;
import de.dlr.bt.stc.opcuaserver.method.QuitMethod;
import de.dlr.bt.stc.opcuaserver.method.ReloadConfigurationMethod;
import de.dlr.bt.stc.opcuaserver.method.StartMethod;
import de.dlr.bt.stc.opcuaserver.method.StopMethod;
import de.dlr.bt.stc.opcuaserver.method.UnloadConfigurationMethod;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class STCNamespace extends ManagedNamespaceWithLifecycle {
private static final String NAMESPACE_URI = "urn:dlr:stc:namespace";
private final SubscriptionModel subscriptionModel;
private final EventBus eventBus;
@Value
public static class Folders {
private UaFolderNode stcFolder;
private UaFolderNode configFolder;
private UaFolderNode sourceFolder;
private UaFolderNode sinkFolder;
private UaFolderNode bridgeFolder;
}
private Folders folders;
private Map<Class<?>, INodeCreator> nodeCreators;
public STCNamespace(OpcUaServer server, EventBus eventBus) {
super(server, NAMESPACE_URI);
this.eventBus = eventBus;
subscriptionModel = new SubscriptionModel(server, this);
getLifecycleManager().addLifecycle(subscriptionModel);
getLifecycleManager().addStartupTask(this::createAndAddNodes);
getLifecycleManager().addShutdownTask(() -> eventBus.unregister(this));
nodeCreators = NodeFactory.getInstance().getCreatorInstances(this);
}
private void createAndAddNodes() {
var stcFolder = createFolderNode("STC", newNodeId("stc"), Identifiers.ObjectsFolder);
var configFolder = createFolderNode("Configuration", newNodeId("stc", "configuration"), stcFolder.getNodeId());
var sourceFolder = createFolderNode("Sources", newNodeId("stc", "sources"), stcFolder.getNodeId());
var sinkFolder = createFolderNode("Sinks", newNodeId("stc", "sinks"), stcFolder.getNodeId());
var bridgeFolder = createFolderNode("Bridges", newNodeId("stc", "bridges"), stcFolder.getNodeId());
folders = new Folders(stcFolder, configFolder, sourceFolder, sinkFolder, bridgeFolder);
// Load object types
for (var creator : nodeCreators.values()) {
creator.createObjectType();
}
createManagementNodes();
eventBus.register(this);
}
public NodeId newNodeId(String... path) {
StringJoiner sj = new StringJoiner("/");
for (var p : path)
sj.add(p.toLowerCase());
return newNodeId(sj.toString());
}
@Subscribe
public void onConfigurationModified(ConfigurationModifiedEvent cme) {
log.info("Configuration modified event {}", cme);
var modtype = cme.getModification();
var creator = nodeCreators.get(cme.getConfiguration().getClass());
if (modtype == ConfigurationModificationType.ADD) {
// Check whether there is a specialized creator available for the configuration
// object, otherwise use only generic information
if (creator != null)
creator.createInstance(cme.getConfiguration(), folders);
else
addGenericConfigurationNode(cme.getKey(), cme.getConfiguration());
} else if (modtype == ConfigurationModificationType.REMOVE) {
if (creator != null)
creator.removeInstance(cme.getConfiguration(), folders);
else
removeGenericConfigurationNode(cme.getKey());
}
}
@Subscribe
public void onTaskModified(TaskModifiedEvent tme) {
log.info("Task modified event: {}", tme);
var creator = nodeCreators.get(tme.getTask().getClass());
TaskModificationType modtype = tme.getModification();
if (modtype == TaskModificationType.INITIALIZE && creator != null) {
creator.createInstance(tme.getTask(), folders);
} else if (modtype == TaskModificationType.REMOVE && creator != null) {
creator.removeInstance(tme.getTask(), folders);
}
}
private void createManagementNodes() {
createMethodNode("stop", "stc/stop", "Stop sTC", StopMethod::new, folders.stcFolder.getNodeId());
createMethodNode("quit", "stc/quit", "Quit sTC", QuitMethod::new, folders.stcFolder.getNodeId());
createMethodNode("start", "stc/start", "Start sTC", StartMethod::new, folders.stcFolder.getNodeId());
createMethodNode("reloadConfiguration", "stc/reloadConfiguration", "Reload sTC configuration",
ReloadConfigurationMethod::new, folders.stcFolder.getNodeId());
createMethodNode("unloadConfiguration", "stc/unloadConfiguration", "Reload sTC configuration",
UnloadConfigurationMethod::new, folders.stcFolder.getNodeId());
}
private void createMethodNode(String qualifiedName, String nodeId, String description,
BiFunction<UaMethodNode, EventBus, AbstractMethodInvocationHandler> handlerFunc, NodeId folder) {
UaMethodNode methodNode = UaMethodNode.builder(getNodeContext()).setNodeId(newNodeId(nodeId))
.setBrowseName(newQualifiedName(qualifiedName)).setDisplayName(LocalizedText.english(qualifiedName))
.setDescription(LocalizedText.english(description)).build();
var handler = handlerFunc.apply(methodNode, eventBus);
methodNode.setInputArguments(handler.getInputArguments());
methodNode.setOutputArguments(handler.getOutputArguments());
methodNode.setInvocationHandler(handler);
getNodeManager().addNode(methodNode);
methodNode.addReference(
new Reference(methodNode.getNodeId(), Identifiers.HasComponent, folder.expanded(), false));
}
/**
* Adds a generic configuration information node. Used when no specific
* implementation for a certain configuration type is available. Only displays
* generic information.
*
* @param key Configuration key
* @param configuration The abstract configuration to display
*/
private void addGenericConfigurationNode(String key, ACfg configuration) {
UaVariableNode configNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
.setNodeId(newNodeId("stc/config/" + key)).setAccessLevel(AccessLevel.READ_ONLY)
.setUserAccessLevel(AccessLevel.READ_ONLY).setBrowseName(newQualifiedName(key))
.setDisplayName(LocalizedText.english(key)).setDataType(Identifiers.String)
.setTypeDefinition(Identifiers.BaseDataVariableType).build();
configNode.setValue(new DataValue(new Variant(configuration.toString())));
getNodeManager().addNode(configNode);
folders.getConfigFolder().addOrganizes(configNode);
}
/**
* Remove a generic configuration information node that has been created using
* {@link #addGenericConfigurationNode(String, ACfg)}.
*
* @param key The configuration key used to create the information node.
*/
private void removeGenericConfigurationNode(String key) {
UaNode configNode = getNodeManager().get(newNodeId("stc/config/" + key));
if (configNode != null)
removeObjectNode(configNode);
}
public UaVariableNode createVariableNode(String name, NodeId newNodeId, NodeId dataType) {
var node = new UaVariableNode.UaVariableNodeBuilder(getNodeContext()).setNodeId(newNodeId)
.setBrowseName(newQualifiedName(name.toLowerCase())).setDisplayName(LocalizedText.english(name))
.setDataType(dataType).setAccessLevel(AccessLevel.READ_ONLY)
.setTypeDefinition(Identifiers.BaseDataVariableType).build();
getNodeManager().addNode(node);
return node;
}
public UaFolderNode createFolderNode(String name, NodeId newNodeId, NodeId parent) {
var fldr = new UaFolderNode(getNodeContext(), newNodeId, newQualifiedName(name.toLowerCase()),
LocalizedText.english(name));
getNodeManager().addNode(fldr);
fldr.addReference(new Reference(fldr.getNodeId(), Identifiers.Organizes, parent.expanded(), false));
return fldr;
}
public UaObjectNode createObjectNode(UaObjectTypeNode type, String name, NodeId newNodeId, UaNode parentFolder)
throws UaException {
UaObjectNode on = (UaObjectNode) getNodeFactory().createNode(newNodeId, type.getNodeId());
on.setBrowseName(newQualifiedName(name.toLowerCase()));
on.setDisplayName(LocalizedText.english(name));
if (parentFolder instanceof UaFolderNode folder)
folder.addOrganizes(on);
else if (parentFolder instanceof UaObjectNode pon)
pon.addComponent(on);
return on;
}
public void removeObjectNode(UaNode node) {
for (var ref : node.getReferences()) {
if (ref.isForward()) {
Set<ExpandedNodeId> nodesToRemove = new HashSet<>();
if (ref.subtypeOf(Identifiers.HierarchicalReferences)) {
nodesToRemove.add(ref.getTargetNodeId());
}
for (var nd : nodesToRemove) {
UaNode fnd = getNodeManager().get(nd, getServer().getNamespaceTable());
if (fnd != null)
removeObjectNode(fnd);
}
} else {
log.debug("Removing reference {}", ref);
getNodeManager().removeReferences(ref, getServer().getNamespaceTable());
}
}
log.debug("Remove UA Node {}", node.getNodeId());
getNodeManager().removeNode(node);
}
public UaObjectTypeNode createObjectTypeNode(String name, NodeId newNodeId, UaNode... subNodes) {
var otn = UaObjectTypeNode.builder(getNodeContext()).setBrowseName(newQualifiedName(name.toLowerCase()))
.setDisplayName(LocalizedText.english(name)).setNodeId(newNodeId).setIsAbstract(false).build();
for (var sn : subNodes)
otn.addComponent(sn);
getServer().getObjectTypeManager().registerObjectType(otn.getNodeId(), UaObjectNode.class, UaObjectNode::new);
otn.addReference(
new Reference(otn.getNodeId(), Identifiers.HasSubtype, Identifiers.BaseObjectType.expanded(), false));
getNodeManager().addNode(otn);
for (var sn : subNodes)
getNodeManager().addNode(sn);
return otn;
}
public UaVariableNode createObjectTypeComponent(String name, NodeId newNodeId, NodeId dataType) {
var node = new UaVariableNode.UaVariableNodeBuilder(getNodeContext()).setNodeId(newNodeId)
.setBrowseName(newQualifiedName(name.toLowerCase())).setDisplayName(LocalizedText.english(name))
.setDataType(dataType).setAccessLevel(AccessLevel.READ_ONLY)
.setTypeDefinition(Identifiers.BaseDataVariableType).build();
node.addReference(new Reference(node.getNodeId(), Identifiers.HasModellingRule,
Identifiers.ModellingRule_Mandatory.expanded(), true));
return node;
}
public UaObjectNode createObjectTypeComponent(String name, NodeId newNodeId, UaObjectTypeNode objectType,
NodeId modellingRule) throws UaException {
UaObjectNode on = (UaObjectNode) getNodeFactory().createNode(newNodeId, objectType.getNodeId());
on.setBrowseName(newQualifiedName(name.toLowerCase()));
on.setDisplayName(LocalizedText.english(name));
on.addReference(new Reference(on.getNodeId(), Identifiers.HasModellingRule, modellingRule.expanded(), true));
return on;
}
public void setObjectNodeComponent(UaObjectNode on, String component, Variant value) {
var optNode = on.findNode(newQualifiedName(component.toLowerCase()));
if (optNode.isPresent()) {
var node = optNode.get();
if (node instanceof UaVariableNode varnode) {
varnode.setValue(new DataValue(value));
}
}
}
public void setObjectNodeComponent(UaObjectNode on, String component,
Function<AttributeFilterContext.GetAttributeContext, DataValue> getter) {
var optNode = on.findNode(newQualifiedName(component.toLowerCase()));
if (optNode.isPresent()) {
var node = optNode.get();
if (node instanceof UaVariableNode varnode) {
varnode.getFilterChain().addLast(AttributeFilters.getValue(getter));
}
}
}
public UaVariableNode getObjectNodeComponent(UaObjectNode on, String component) {
var optNode = on.findNode(newQualifiedName(component.toLowerCase()));
return optNode.isPresent() && optNode.get() instanceof UaVariableNode vn ? vn : null;
}
public UaObjectNode getObjectNodeInstance(UaNode parentNode, String component) {
var optNode = parentNode.findNode(newQualifiedName(component.toLowerCase()));
if (optNode.isPresent() && optNode.get() instanceof UaObjectNode uon)
return uon;
return null;
}
@Override
public void onDataItemsCreated(List<DataItem> dataItems) {
subscriptionModel.onDataItemsCreated(dataItems);
}
@Override
public void onDataItemsModified(List<DataItem> dataItems) {
subscriptionModel.onDataItemsModified(dataItems);
}
@Override
public void onDataItemsDeleted(List<DataItem> dataItems) {
subscriptionModel.onDataItemsDeleted(dataItems);
}
@Override
public void onMonitoringModeChanged(List<MonitoredItem> monitoredItems) {
subscriptionModel.onMonitoringModeChanged(monitoredItems);
}
}