View Javadoc
1   package de.dlr.bt.stc.opcuaserver;
2   
3   import java.util.HashSet;
4   import java.util.List;
5   import java.util.Map;
6   import java.util.Set;
7   import java.util.StringJoiner;
8   import java.util.function.BiFunction;
9   import java.util.function.Function;
10  
11  import org.eclipse.milo.opcua.sdk.core.AccessLevel;
12  import org.eclipse.milo.opcua.sdk.core.Reference;
13  import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
14  import org.eclipse.milo.opcua.sdk.server.api.DataItem;
15  import org.eclipse.milo.opcua.sdk.server.api.ManagedNamespaceWithLifecycle;
16  import org.eclipse.milo.opcua.sdk.server.api.MonitoredItem;
17  import org.eclipse.milo.opcua.sdk.server.api.methods.AbstractMethodInvocationHandler;
18  import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode;
19  import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode;
20  import org.eclipse.milo.opcua.sdk.server.nodes.UaNode;
21  import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode;
22  import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectTypeNode;
23  import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode;
24  import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilterContext;
25  import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilters;
26  import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel;
27  import org.eclipse.milo.opcua.stack.core.Identifiers;
28  import org.eclipse.milo.opcua.stack.core.UaException;
29  import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
30  import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
31  import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
32  import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
33  import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
34  import org.greenrobot.eventbus.EventBus;
35  import org.greenrobot.eventbus.Subscribe;
36  
37  import de.dlr.bt.stc.config.ACfg;
38  import de.dlr.bt.stc.eventbus.ConfigurationModifiedEvent;
39  import de.dlr.bt.stc.eventbus.ConfigurationModifiedEvent.ConfigurationModificationType;
40  import de.dlr.bt.stc.eventbus.TaskModifiedEvent;
41  import de.dlr.bt.stc.eventbus.TaskModifiedEvent.TaskModificationType;
42  import de.dlr.bt.stc.opcuaserver.method.QuitMethod;
43  import de.dlr.bt.stc.opcuaserver.method.ReloadConfigurationMethod;
44  import de.dlr.bt.stc.opcuaserver.method.StartMethod;
45  import de.dlr.bt.stc.opcuaserver.method.StopMethod;
46  import de.dlr.bt.stc.opcuaserver.method.UnloadConfigurationMethod;
47  import lombok.Value;
48  import lombok.extern.slf4j.Slf4j;
49  
50  @Slf4j
51  public class STCNamespace extends ManagedNamespaceWithLifecycle {
52  	private static final String NAMESPACE_URI = "urn:dlr:stc:namespace";
53  
54  	private final SubscriptionModel subscriptionModel;
55  
56  	private final EventBus eventBus;
57  
58  	@Value
59  	public static class Folders {
60  		private UaFolderNode stcFolder;
61  		private UaFolderNode configFolder;
62  		private UaFolderNode sourceFolder;
63  		private UaFolderNode sinkFolder;
64  		private UaFolderNode bridgeFolder;
65  	}
66  
67  	private Folders folders;
68  
69  	private Map<Class<?>, INodeCreator> nodeCreators;
70  
71  	public STCNamespace(OpcUaServer server, EventBus eventBus) {
72  		super(server, NAMESPACE_URI);
73  
74  		this.eventBus = eventBus;
75  
76  		subscriptionModel = new SubscriptionModel(server, this);
77  
78  		getLifecycleManager().addLifecycle(subscriptionModel);
79  
80  		getLifecycleManager().addStartupTask(this::createAndAddNodes);
81  		getLifecycleManager().addShutdownTask(() -> eventBus.unregister(this));
82  
83  		nodeCreators = NodeFactory.getInstance().getCreatorInstances(this);
84  	}
85  
86  	private void createAndAddNodes() {
87  		var stcFolder = createFolderNode("STC", newNodeId("stc"), Identifiers.ObjectsFolder);
88  		var configFolder = createFolderNode("Configuration", newNodeId("stc", "configuration"), stcFolder.getNodeId());
89  		var sourceFolder = createFolderNode("Sources", newNodeId("stc", "sources"), stcFolder.getNodeId());
90  		var sinkFolder = createFolderNode("Sinks", newNodeId("stc", "sinks"), stcFolder.getNodeId());
91  		var bridgeFolder = createFolderNode("Bridges", newNodeId("stc", "bridges"), stcFolder.getNodeId());
92  
93  		folders = new Folders(stcFolder, configFolder, sourceFolder, sinkFolder, bridgeFolder);
94  
95  		// Load object types
96  		for (var creator : nodeCreators.values()) {
97  			creator.createObjectType();
98  		}
99  
100 		createManagementNodes();
101 
102 		eventBus.register(this);
103 	}
104 
105 	public NodeId newNodeId(String... path) {
106 		StringJoiner sj = new StringJoiner("/");
107 		for (var p : path)
108 			sj.add(p.toLowerCase());
109 		return newNodeId(sj.toString());
110 	}
111 
112 	@Subscribe
113 	public void onConfigurationModified(ConfigurationModifiedEvent cme) {
114 		log.info("Configuration modified event {}", cme);
115 
116 		var modtype = cme.getModification();
117 
118 		var creator = nodeCreators.get(cme.getConfiguration().getClass());
119 		if (modtype == ConfigurationModificationType.ADD) {
120 			// Check whether there is a specialized creator available for the configuration
121 			// object, otherwise use only generic information
122 			if (creator != null)
123 				creator.createInstance(cme.getConfiguration(), folders);
124 			else
125 				addGenericConfigurationNode(cme.getKey(), cme.getConfiguration());
126 		} else if (modtype == ConfigurationModificationType.REMOVE) {
127 			if (creator != null)
128 				creator.removeInstance(cme.getConfiguration(), folders);
129 			else
130 				removeGenericConfigurationNode(cme.getKey());
131 		}
132 	}
133 
134 	@Subscribe
135 	public void onTaskModified(TaskModifiedEvent tme) {
136 		log.info("Task modified event: {}", tme);
137 
138 		var creator = nodeCreators.get(tme.getTask().getClass());
139 		TaskModificationType modtype = tme.getModification();
140 		if (modtype == TaskModificationType.INITIALIZE && creator != null) {
141 			creator.createInstance(tme.getTask(), folders);
142 		} else if (modtype == TaskModificationType.REMOVE && creator != null) {
143 			creator.removeInstance(tme.getTask(), folders);
144 		}
145 	}
146 
147 	private void createManagementNodes() {
148 		createMethodNode("stop", "stc/stop", "Stop sTC", StopMethod::new, folders.stcFolder.getNodeId());
149 
150 		createMethodNode("quit", "stc/quit", "Quit sTC", QuitMethod::new, folders.stcFolder.getNodeId());
151 
152 		createMethodNode("start", "stc/start", "Start sTC", StartMethod::new, folders.stcFolder.getNodeId());
153 
154 		createMethodNode("reloadConfiguration", "stc/reloadConfiguration", "Reload sTC configuration",
155 				ReloadConfigurationMethod::new, folders.stcFolder.getNodeId());
156 
157 		createMethodNode("unloadConfiguration", "stc/unloadConfiguration", "Reload sTC configuration",
158 				UnloadConfigurationMethod::new, folders.stcFolder.getNodeId());
159 
160 	}
161 
162 	private void createMethodNode(String qualifiedName, String nodeId, String description,
163 			BiFunction<UaMethodNode, EventBus, AbstractMethodInvocationHandler> handlerFunc, NodeId folder) {
164 		UaMethodNode methodNode = UaMethodNode.builder(getNodeContext()).setNodeId(newNodeId(nodeId))
165 				.setBrowseName(newQualifiedName(qualifiedName)).setDisplayName(LocalizedText.english(qualifiedName))
166 				.setDescription(LocalizedText.english(description)).build();
167 
168 		var handler = handlerFunc.apply(methodNode, eventBus);
169 
170 		methodNode.setInputArguments(handler.getInputArguments());
171 		methodNode.setOutputArguments(handler.getOutputArguments());
172 		methodNode.setInvocationHandler(handler);
173 
174 		getNodeManager().addNode(methodNode);
175 
176 		methodNode.addReference(
177 				new Reference(methodNode.getNodeId(), Identifiers.HasComponent, folder.expanded(), false));
178 	}
179 
180 	/**
181 	 * Adds a generic configuration information node. Used when no specific
182 	 * implementation for a certain configuration type is available. Only displays
183 	 * generic information.
184 	 * 
185 	 * @param key           Configuration key
186 	 * @param configuration The abstract configuration to display
187 	 */
188 	private void addGenericConfigurationNode(String key, ACfg configuration) {
189 		UaVariableNode configNode = new UaVariableNode.UaVariableNodeBuilder(getNodeContext())
190 				.setNodeId(newNodeId("stc/config/" + key)).setAccessLevel(AccessLevel.READ_ONLY)
191 				.setUserAccessLevel(AccessLevel.READ_ONLY).setBrowseName(newQualifiedName(key))
192 				.setDisplayName(LocalizedText.english(key)).setDataType(Identifiers.String)
193 				.setTypeDefinition(Identifiers.BaseDataVariableType).build();
194 
195 		configNode.setValue(new DataValue(new Variant(configuration.toString())));
196 
197 		getNodeManager().addNode(configNode);
198 
199 		folders.getConfigFolder().addOrganizes(configNode);
200 	}
201 
202 	/**
203 	 * Remove a generic configuration information node that has been created using
204 	 * {@link #addGenericConfigurationNode(String, ACfg)}.
205 	 * 
206 	 * @param key The configuration key used to create the information node.
207 	 */
208 	private void removeGenericConfigurationNode(String key) {
209 		UaNode configNode = getNodeManager().get(newNodeId("stc/config/" + key));
210 		if (configNode != null)
211 			removeObjectNode(configNode);
212 	}
213 
214 	public UaVariableNode createVariableNode(String name, NodeId newNodeId, NodeId dataType) {
215 		var node = new UaVariableNode.UaVariableNodeBuilder(getNodeContext()).setNodeId(newNodeId)
216 				.setBrowseName(newQualifiedName(name.toLowerCase())).setDisplayName(LocalizedText.english(name))
217 				.setDataType(dataType).setAccessLevel(AccessLevel.READ_ONLY)
218 				.setTypeDefinition(Identifiers.BaseDataVariableType).build();
219 
220 		getNodeManager().addNode(node);
221 
222 		return node;
223 	}
224 
225 	public UaFolderNode createFolderNode(String name, NodeId newNodeId, NodeId parent) {
226 		var fldr = new UaFolderNode(getNodeContext(), newNodeId, newQualifiedName(name.toLowerCase()),
227 				LocalizedText.english(name));
228 
229 		getNodeManager().addNode(fldr);
230 
231 		fldr.addReference(new Reference(fldr.getNodeId(), Identifiers.Organizes, parent.expanded(), false));
232 
233 		return fldr;
234 	}
235 
236 	public UaObjectNode createObjectNode(UaObjectTypeNode type, String name, NodeId newNodeId, UaNode parentFolder)
237 			throws UaException {
238 		UaObjectNode on = (UaObjectNode) getNodeFactory().createNode(newNodeId, type.getNodeId());
239 
240 		on.setBrowseName(newQualifiedName(name.toLowerCase()));
241 		on.setDisplayName(LocalizedText.english(name));
242 
243 		if (parentFolder instanceof UaFolderNode folder)
244 			folder.addOrganizes(on);
245 		else if (parentFolder instanceof UaObjectNode pon)
246 			pon.addComponent(on);
247 
248 		return on;
249 	}
250 
251 	public void removeObjectNode(UaNode node) {
252 		for (var ref : node.getReferences()) {
253 			if (ref.isForward()) {
254 				Set<ExpandedNodeId> nodesToRemove = new HashSet<>();
255 				if (ref.subtypeOf(Identifiers.HierarchicalReferences)) {
256 					nodesToRemove.add(ref.getTargetNodeId());
257 				}
258 
259 				for (var nd : nodesToRemove) {
260 					UaNode fnd = getNodeManager().get(nd, getServer().getNamespaceTable());
261 					if (fnd != null)
262 						removeObjectNode(fnd);
263 				}
264 			} else {
265 				log.debug("Removing reference {}", ref);
266 				getNodeManager().removeReferences(ref, getServer().getNamespaceTable());
267 			}
268 		}
269 
270 		log.debug("Remove UA Node {}", node.getNodeId());
271 		getNodeManager().removeNode(node);
272 	}
273 
274 	public UaObjectTypeNode createObjectTypeNode(String name, NodeId newNodeId, UaNode... subNodes) {
275 		var otn = UaObjectTypeNode.builder(getNodeContext()).setBrowseName(newQualifiedName(name.toLowerCase()))
276 				.setDisplayName(LocalizedText.english(name)).setNodeId(newNodeId).setIsAbstract(false).build();
277 
278 		for (var sn : subNodes)
279 			otn.addComponent(sn);
280 
281 		getServer().getObjectTypeManager().registerObjectType(otn.getNodeId(), UaObjectNode.class, UaObjectNode::new);
282 
283 		otn.addReference(
284 				new Reference(otn.getNodeId(), Identifiers.HasSubtype, Identifiers.BaseObjectType.expanded(), false));
285 
286 		getNodeManager().addNode(otn);
287 
288 		for (var sn : subNodes)
289 			getNodeManager().addNode(sn);
290 
291 		return otn;
292 	}
293 
294 	public UaVariableNode createObjectTypeComponent(String name, NodeId newNodeId, NodeId dataType) {
295 		var node = new UaVariableNode.UaVariableNodeBuilder(getNodeContext()).setNodeId(newNodeId)
296 				.setBrowseName(newQualifiedName(name.toLowerCase())).setDisplayName(LocalizedText.english(name))
297 				.setDataType(dataType).setAccessLevel(AccessLevel.READ_ONLY)
298 				.setTypeDefinition(Identifiers.BaseDataVariableType).build();
299 
300 		node.addReference(new Reference(node.getNodeId(), Identifiers.HasModellingRule,
301 				Identifiers.ModellingRule_Mandatory.expanded(), true));
302 
303 		return node;
304 	}
305 
306 	public UaObjectNode createObjectTypeComponent(String name, NodeId newNodeId, UaObjectTypeNode objectType,
307 			NodeId modellingRule) throws UaException {
308 		UaObjectNode on = (UaObjectNode) getNodeFactory().createNode(newNodeId, objectType.getNodeId());
309 
310 		on.setBrowseName(newQualifiedName(name.toLowerCase()));
311 		on.setDisplayName(LocalizedText.english(name));
312 
313 		on.addReference(new Reference(on.getNodeId(), Identifiers.HasModellingRule, modellingRule.expanded(), true));
314 
315 		return on;
316 	}
317 
318 	public void setObjectNodeComponent(UaObjectNode on, String component, Variant value) {
319 		var optNode = on.findNode(newQualifiedName(component.toLowerCase()));
320 
321 		if (optNode.isPresent()) {
322 			var node = optNode.get();
323 			if (node instanceof UaVariableNode varnode) {
324 				varnode.setValue(new DataValue(value));
325 			}
326 		}
327 	}
328 
329 	public void setObjectNodeComponent(UaObjectNode on, String component,
330 			Function<AttributeFilterContext.GetAttributeContext, DataValue> getter) {
331 		var optNode = on.findNode(newQualifiedName(component.toLowerCase()));
332 		if (optNode.isPresent()) {
333 			var node = optNode.get();
334 			if (node instanceof UaVariableNode varnode) {
335 				varnode.getFilterChain().addLast(AttributeFilters.getValue(getter));
336 			}
337 		}
338 	}
339 
340 	public UaVariableNode getObjectNodeComponent(UaObjectNode on, String component) {
341 		var optNode = on.findNode(newQualifiedName(component.toLowerCase()));
342 		return optNode.isPresent() && optNode.get() instanceof UaVariableNode vn ? vn : null;
343 	}
344 
345 	public UaObjectNode getObjectNodeInstance(UaNode parentNode, String component) {
346 		var optNode = parentNode.findNode(newQualifiedName(component.toLowerCase()));
347 		if (optNode.isPresent() && optNode.get() instanceof UaObjectNode uon)
348 			return uon;
349 
350 		return null;
351 	}
352 
353 	@Override
354 	public void onDataItemsCreated(List<DataItem> dataItems) {
355 		subscriptionModel.onDataItemsCreated(dataItems);
356 	}
357 
358 	@Override
359 	public void onDataItemsModified(List<DataItem> dataItems) {
360 		subscriptionModel.onDataItemsModified(dataItems);
361 	}
362 
363 	@Override
364 	public void onDataItemsDeleted(List<DataItem> dataItems) {
365 		subscriptionModel.onDataItemsDeleted(dataItems);
366 	}
367 
368 	@Override
369 	public void onMonitoringModeChanged(List<MonitoredItem> monitoredItems) {
370 		subscriptionModel.onMonitoringModeChanged(monitoredItems);
371 	}
372 
373 }