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);
	}

}