/*
* Copyright 2016 Kevin Herron
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.digitalpetri.opcua.sdk.client;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import com.digitalpetri.opcua.sdk.client.api.ServiceFaultListener;
import com.digitalpetri.opcua.sdk.client.api.UaClient;
import com.digitalpetri.opcua.sdk.client.api.UaSession;
import com.digitalpetri.opcua.sdk.client.api.config.OpcUaClientConfig;
import com.digitalpetri.opcua.sdk.client.api.nodes.AddressSpace;
import com.digitalpetri.opcua.sdk.client.api.nodes.NodeCache;
import com.digitalpetri.opcua.sdk.client.nodes.DefaultAddressSpace;
import com.digitalpetri.opcua.sdk.client.nodes.DefaultNodeCache;
import com.digitalpetri.opcua.sdk.client.subscriptions.OpcUaSubscriptionManager;
import com.digitalpetri.opcua.stack.client.UaTcpStackClient;
import com.digitalpetri.opcua.stack.core.UaServiceFaultException;
import com.digitalpetri.opcua.stack.core.serialization.UaRequestMessage;
import com.digitalpetri.opcua.stack.core.serialization.UaResponseMessage;
import com.digitalpetri.opcua.stack.core.types.builtin.ByteString;
import com.digitalpetri.opcua.stack.core.types.builtin.DateTime;
import com.digitalpetri.opcua.stack.core.types.builtin.ExtensionObject;
import com.digitalpetri.opcua.stack.core.types.builtin.NodeId;
import com.digitalpetri.opcua.stack.core.types.builtin.unsigned.UByte;
import com.digitalpetri.opcua.stack.core.types.builtin.unsigned.UInteger;
import com.digitalpetri.opcua.stack.core.types.enumerated.MonitoringMode;
import com.digitalpetri.opcua.stack.core.types.enumerated.TimestampsToReturn;
import com.digitalpetri.opcua.stack.core.types.structured.BrowseDescription;
import com.digitalpetri.opcua.stack.core.types.structured.BrowseNextRequest;
import com.digitalpetri.opcua.stack.core.types.structured.BrowseNextResponse;
import com.digitalpetri.opcua.stack.core.types.structured.BrowsePath;
import com.digitalpetri.opcua.stack.core.types.structured.BrowseRequest;
import com.digitalpetri.opcua.stack.core.types.structured.BrowseResponse;
import com.digitalpetri.opcua.stack.core.types.structured.CallMethodRequest;
import com.digitalpetri.opcua.stack.core.types.structured.CallRequest;
import com.digitalpetri.opcua.stack.core.types.structured.CallResponse;
import com.digitalpetri.opcua.stack.core.types.structured.CreateMonitoredItemsRequest;
import com.digitalpetri.opcua.stack.core.types.structured.CreateMonitoredItemsResponse;
import com.digitalpetri.opcua.stack.core.types.structured.CreateSubscriptionRequest;
import com.digitalpetri.opcua.stack.core.types.structured.CreateSubscriptionResponse;
import com.digitalpetri.opcua.stack.core.types.structured.DeleteMonitoredItemsRequest;
import com.digitalpetri.opcua.stack.core.types.structured.DeleteMonitoredItemsResponse;
import com.digitalpetri.opcua.stack.core.types.structured.DeleteSubscriptionsRequest;
import com.digitalpetri.opcua.stack.core.types.structured.DeleteSubscriptionsResponse;
import com.digitalpetri.opcua.stack.core.types.structured.HistoryReadDetails;
import com.digitalpetri.opcua.stack.core.types.structured.HistoryReadRequest;
import com.digitalpetri.opcua.stack.core.types.structured.HistoryReadResponse;
import com.digitalpetri.opcua.stack.core.types.structured.HistoryReadValueId;
import com.digitalpetri.opcua.stack.core.types.structured.HistoryUpdateDetails;
import com.digitalpetri.opcua.stack.core.types.structured.HistoryUpdateRequest;
import com.digitalpetri.opcua.stack.core.types.structured.HistoryUpdateResponse;
import com.digitalpetri.opcua.stack.core.types.structured.ModifyMonitoredItemsRequest;
import com.digitalpetri.opcua.stack.core.types.structured.ModifyMonitoredItemsResponse;
import com.digitalpetri.opcua.stack.core.types.structured.ModifySubscriptionRequest;
import com.digitalpetri.opcua.stack.core.types.structured.ModifySubscriptionResponse;
import com.digitalpetri.opcua.stack.core.types.structured.MonitoredItemCreateRequest;
import com.digitalpetri.opcua.stack.core.types.structured.MonitoredItemModifyRequest;
import com.digitalpetri.opcua.stack.core.types.structured.PublishRequest;
import com.digitalpetri.opcua.stack.core.types.structured.PublishResponse;
import com.digitalpetri.opcua.stack.core.types.structured.ReadRequest;
import com.digitalpetri.opcua.stack.core.types.structured.ReadResponse;
import com.digitalpetri.opcua.stack.core.types.structured.ReadValueId;
import com.digitalpetri.opcua.stack.core.types.structured.RegisterNodesRequest;
import com.digitalpetri.opcua.stack.core.types.structured.RegisterNodesResponse;
import com.digitalpetri.opcua.stack.core.types.structured.RepublishRequest;
import com.digitalpetri.opcua.stack.core.types.structured.RepublishResponse;
import com.digitalpetri.opcua.stack.core.types.structured.RequestHeader;
import com.digitalpetri.opcua.stack.core.types.structured.ServiceFault;
import com.digitalpetri.opcua.stack.core.types.structured.SetMonitoringModeRequest;
import com.digitalpetri.opcua.stack.core.types.structured.SetMonitoringModeResponse;
import com.digitalpetri.opcua.stack.core.types.structured.SetPublishingModeRequest;
import com.digitalpetri.opcua.stack.core.types.structured.SetPublishingModeResponse;
import com.digitalpetri.opcua.stack.core.types.structured.SetTriggeringRequest;
import com.digitalpetri.opcua.stack.core.types.structured.SetTriggeringResponse;
import com.digitalpetri.opcua.stack.core.types.structured.SubscriptionAcknowledgement;
import com.digitalpetri.opcua.stack.core.types.structured.TransferSubscriptionsRequest;
import com.digitalpetri.opcua.stack.core.types.structured.TransferSubscriptionsResponse;
import com.digitalpetri.opcua.stack.core.types.structured.TranslateBrowsePathsToNodeIdsRequest;
import com.digitalpetri.opcua.stack.core.types.structured.TranslateBrowsePathsToNodeIdsResponse;
import com.digitalpetri.opcua.stack.core.types.structured.UnregisterNodesRequest;
import com.digitalpetri.opcua.stack.core.types.structured.UnregisterNodesResponse;
import com.digitalpetri.opcua.stack.core.types.structured.ViewDescription;
import com.digitalpetri.opcua.stack.core.types.structured.WriteRequest;
import com.digitalpetri.opcua.stack.core.types.structured.WriteResponse;
import com.digitalpetri.opcua.stack.core.types.structured.WriteValue;
import com.digitalpetri.opcua.stack.core.util.ExecutionQueue;
import com.digitalpetri.opcua.stack.core.util.LongSequence;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.digitalpetri.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
import static com.digitalpetri.opcua.stack.core.util.ConversionUtil.a;
import static com.google.common.collect.Lists.newCopyOnWriteArrayList;
public class OpcUaClient implements UaClient {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final LongSequence requestHandles = new LongSequence(0, UInteger.MAX_VALUE);
private final List<ServiceFaultListener> faultListeners = newCopyOnWriteArrayList();
private final ExecutionQueue faultNotificationQueue;
private final AddressSpace addressSpace;
private final NodeCache nodeCache = new DefaultNodeCache();
private final OpcUaSubscriptionManager subscriptionManager;
private final UaTcpStackClient stackClient;
private final ClientSessionManager sessionManager;
private final OpcUaClientConfig config;
public OpcUaClient(OpcUaClientConfig config) {
this.config = config;
sessionManager = new ClientSessionManager(this);
stackClient = new UaTcpStackClient(config);
faultNotificationQueue = new ExecutionQueue(config.getExecutor());
addressSpace = new DefaultAddressSpace(this);
subscriptionManager = new OpcUaSubscriptionManager(this);
}
@Override
public OpcUaClientConfig getConfig() {
return config;
}
public UaTcpStackClient getStackClient() {
return stackClient;
}
@Override
public NodeCache getNodeCache() {
return nodeCache;
}
@Override
public AddressSpace getAddressSpace() {
return addressSpace;
}
/**
* Build a new {@link RequestHeader} using a null authentication token.
*
* @return a new {@link RequestHeader} with a null authentication token.
*/
public RequestHeader newRequestHeader() {
return newRequestHeader(NodeId.NULL_VALUE);
}
/**
* Build a new {@link RequestHeader} using {@code authToken}.
*
* @param authToken the authentication token (from the session) to use.
* @return a new {@link RequestHeader}.
*/
public RequestHeader newRequestHeader(NodeId authToken) {
return new RequestHeader(
authToken,
DateTime.now(),
uint(requestHandles.getAndIncrement()),
uint(0),
null,
config.getRequestTimeout(),
null);
}
/**
* @return the next {@link UInteger} to use as a request handle.
*/
public UInteger nextRequestHandle() {
return uint(requestHandles.getAndIncrement());
}
@Override
public CompletableFuture<UaClient> connect() {
return stackClient.connect().thenCompose(
c -> getSession().thenApply(s -> OpcUaClient.this));
}
@Override
public CompletableFuture<UaClient> disconnect() {
// Subscriptions must be cleared first, effectively stopping new
// PublishRequests from being sent, otherwise continued PublishRequests
// will initiate reconnection and re-activation.
subscriptionManager.clearSubscriptions();
return sessionManager
.closeSession()
.thenCompose(v -> stackClient.disconnect())
.thenApply(c -> (UaClient) OpcUaClient.this)
.exceptionally(ex -> OpcUaClient.this);
}
@Override
public OpcUaSubscriptionManager getSubscriptionManager() {
return subscriptionManager;
}
@Override
public CompletableFuture<ReadResponse> read(double maxAge,
TimestampsToReturn timestampsToReturn,
List<ReadValueId> readValueIds) {
return getSession().thenCompose(session -> {
ReadRequest request = new ReadRequest(
newRequestHeader(session.getAuthenticationToken()),
maxAge,
timestampsToReturn,
a(readValueIds, ReadValueId.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<WriteResponse> write(List<WriteValue> writeValues) {
return getSession().thenCompose(session -> {
WriteRequest request = new WriteRequest(
newRequestHeader(session.getAuthenticationToken()),
a(writeValues, WriteValue.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<HistoryReadResponse> historyRead(HistoryReadDetails historyReadDetails,
TimestampsToReturn timestampsToReturn,
boolean releaseContinuationPoints,
List<HistoryReadValueId> nodesToRead) {
return getSession().thenCompose(session -> {
HistoryReadRequest request = new HistoryReadRequest(
newRequestHeader(session.getAuthenticationToken()),
ExtensionObject.encode(historyReadDetails),
timestampsToReturn,
releaseContinuationPoints,
a(nodesToRead, HistoryReadValueId.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<HistoryUpdateResponse> historyUpdate(List<HistoryUpdateDetails> historyUpdateDetails) {
return getSession().thenCompose(session -> {
ExtensionObject[] details = historyUpdateDetails.stream()
.map(ExtensionObject::encode)
.toArray(ExtensionObject[]::new);
HistoryUpdateRequest request = new HistoryUpdateRequest(
newRequestHeader(session.getAuthenticationToken()),
details);
return sendRequest(request);
});
}
@Override
public CompletableFuture<BrowseResponse> browse(ViewDescription viewDescription,
UInteger maxReferencesPerNode,
List<BrowseDescription> nodesToBrowse) {
return getSession().thenCompose(session -> {
BrowseRequest request = new BrowseRequest(
newRequestHeader(session.getAuthenticationToken()),
viewDescription,
maxReferencesPerNode,
a(nodesToBrowse, BrowseDescription.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<BrowseNextResponse> browseNext(boolean releaseContinuationPoints,
List<ByteString> continuationPoints) {
return getSession().thenCompose(session -> {
BrowseNextRequest request = new BrowseNextRequest(
newRequestHeader(session.getAuthenticationToken()),
releaseContinuationPoints,
a(continuationPoints, ByteString.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<TranslateBrowsePathsToNodeIdsResponse> translateBrowsePaths(List<BrowsePath> browsePaths) {
return getSession().thenCompose(session -> {
TranslateBrowsePathsToNodeIdsRequest request = new TranslateBrowsePathsToNodeIdsRequest(
newRequestHeader(session.getAuthenticationToken()),
a(browsePaths, BrowsePath.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<RegisterNodesResponse> registerNodes(List<NodeId> nodesToRegister) {
return getSession().thenCompose(session -> {
RegisterNodesRequest request = new RegisterNodesRequest(
newRequestHeader(session.getAuthenticationToken()),
a(nodesToRegister, NodeId.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<UnregisterNodesResponse> unregisterNodes(List<NodeId> nodesToUnregister) {
return getSession().thenCompose(session -> {
UnregisterNodesRequest request = new UnregisterNodesRequest(
newRequestHeader(session.getAuthenticationToken()),
a(nodesToUnregister, NodeId.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<CallResponse> call(List<CallMethodRequest> methodsToCall) {
return getSession().thenCompose(session -> {
CallRequest request = new CallRequest(
newRequestHeader(session.getAuthenticationToken()),
a(methodsToCall, CallMethodRequest.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<CreateSubscriptionResponse> createSubscription(double requestedPublishingInterval,
UInteger requestedLifetimeCount,
UInteger requestedMaxKeepAliveCount,
UInteger maxNotificationsPerPublish,
boolean publishingEnabled,
UByte priority) {
return getSession().thenCompose(session -> {
CreateSubscriptionRequest request = new CreateSubscriptionRequest(
newRequestHeader(session.getAuthenticationToken()),
requestedPublishingInterval,
requestedLifetimeCount,
requestedMaxKeepAliveCount,
maxNotificationsPerPublish,
publishingEnabled,
priority);
return sendRequest(request);
});
}
@Override
public CompletableFuture<ModifySubscriptionResponse> modifySubscription(UInteger subscriptionId,
double requestedPublishingInterval,
UInteger requestedLifetimeCount,
UInteger requestedMaxKeepAliveCount,
UInteger maxNotificationsPerPublish,
UByte priority) {
return getSession().thenCompose(session -> {
ModifySubscriptionRequest request = new ModifySubscriptionRequest(
newRequestHeader(session.getAuthenticationToken()),
subscriptionId,
requestedPublishingInterval,
requestedLifetimeCount,
requestedMaxKeepAliveCount,
maxNotificationsPerPublish,
priority);
return sendRequest(request);
});
}
@Override
public CompletableFuture<DeleteSubscriptionsResponse> deleteSubscriptions(List<UInteger> subscriptionIds) {
return getSession().thenCompose(session -> {
DeleteSubscriptionsRequest request = new DeleteSubscriptionsRequest(
newRequestHeader(session.getAuthenticationToken()),
a(subscriptionIds, UInteger.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<TransferSubscriptionsResponse> transferSubscriptions(List<UInteger> subscriptionIds,
boolean sendInitialValues) {
return getSession().thenCompose(session -> {
TransferSubscriptionsRequest request = new TransferSubscriptionsRequest(
newRequestHeader(session.getAuthenticationToken()),
a(subscriptionIds, UInteger.class),
sendInitialValues);
return sendRequest(request);
});
}
@Override
public CompletableFuture<SetPublishingModeResponse> setPublishingMode(boolean publishingEnabled,
List<UInteger> subscriptionIds) {
return getSession().thenCompose(session -> {
SetPublishingModeRequest request = new SetPublishingModeRequest(
newRequestHeader(session.getAuthenticationToken()),
publishingEnabled,
a(subscriptionIds, UInteger.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<PublishResponse> publish(List<SubscriptionAcknowledgement> subscriptionAcknowledgements) {
return getSession().thenCompose(session -> {
PublishRequest request = new PublishRequest(
newRequestHeader(session.getAuthenticationToken()),
a(subscriptionAcknowledgements, SubscriptionAcknowledgement.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<RepublishResponse> republish(UInteger subscriptionId, UInteger retransmitSequenceNumber) {
return getSession().thenCompose(session -> {
RepublishRequest request = new RepublishRequest(
newRequestHeader(session.getAuthenticationToken()),
subscriptionId,
retransmitSequenceNumber);
return sendRequest(request);
});
}
@Override
public CompletableFuture<CreateMonitoredItemsResponse> createMonitoredItems(UInteger subscriptionId,
TimestampsToReturn timestampsToReturn,
List<MonitoredItemCreateRequest> itemsToCreate) {
return getSession().thenCompose(session -> {
CreateMonitoredItemsRequest request = new CreateMonitoredItemsRequest(
newRequestHeader(session.getAuthenticationToken()),
subscriptionId,
timestampsToReturn,
a(itemsToCreate, MonitoredItemCreateRequest.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<ModifyMonitoredItemsResponse> modifyMonitoredItems(UInteger subscriptionId,
TimestampsToReturn timestampsToReturn,
List<MonitoredItemModifyRequest> itemsToModify) {
return getSession().thenCompose(session -> {
ModifyMonitoredItemsRequest request = new ModifyMonitoredItemsRequest(
newRequestHeader(session.getAuthenticationToken()),
subscriptionId,
timestampsToReturn,
a(itemsToModify, MonitoredItemModifyRequest.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<DeleteMonitoredItemsResponse> deleteMonitoredItems(UInteger subscriptionId,
List<UInteger> monitoredItemIds) {
return getSession().thenCompose(session -> {
DeleteMonitoredItemsRequest request = new DeleteMonitoredItemsRequest(
newRequestHeader(session.getAuthenticationToken()),
subscriptionId,
a(monitoredItemIds, UInteger.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<SetMonitoringModeResponse> setMonitoringMode(UInteger subscriptionId,
MonitoringMode monitoringMode,
List<UInteger> monitoredItemIds) {
return getSession().thenCompose(session -> {
SetMonitoringModeRequest request = new SetMonitoringModeRequest(
newRequestHeader(session.getAuthenticationToken()),
subscriptionId,
monitoringMode,
a(monitoredItemIds, UInteger.class));
return sendRequest(request);
});
}
@Override
public CompletableFuture<SetTriggeringResponse> setTriggering(UInteger subscriptionId,
UInteger triggeringItemId,
List<UInteger> linksToAdd,
List<UInteger> linksToRemove) {
return getSession().thenCompose(session -> {
SetTriggeringRequest request = new SetTriggeringRequest(
newRequestHeader(session.getAuthenticationToken()),
subscriptionId,
triggeringItemId,
a(linksToAdd, UInteger.class),
a(linksToRemove, UInteger.class));
return sendRequest(request);
});
}
@Override
public final CompletableFuture<UaSession> getSession() {
return sessionManager.getSession().thenApply(s -> (UaSession) s);
}
@Override
public <T extends UaResponseMessage> CompletableFuture<T> sendRequest(UaRequestMessage request) {
CompletableFuture<T> f = stackClient.sendRequest(request);
if (faultListeners.size() > 0) {
f.whenComplete(this::maybeHandleServiceFault);
}
return f;
}
@Override
public void sendRequests(List<? extends UaRequestMessage> requests,
List<CompletableFuture<? extends UaResponseMessage>> futures) {
futures.forEach(f -> f.whenComplete(this::maybeHandleServiceFault));
stackClient.sendRequests(requests, futures);
}
private void maybeHandleServiceFault(UaResponseMessage response, Throwable ex) {
if (faultListeners.isEmpty()) return;
if (ex != null) {
if (ex instanceof UaServiceFaultException) {
UaServiceFaultException faultException = (UaServiceFaultException) ex;
ServiceFault serviceFault = faultException.getServiceFault();
logger.debug("Notifying {} ServiceFaultListeners", faultListeners.size());
faultNotificationQueue.submit(() ->
faultListeners.stream().forEach(h -> h.onServiceFault(serviceFault)));
} else if (ex.getCause() instanceof UaServiceFaultException) {
UaServiceFaultException faultException = (UaServiceFaultException) ex.getCause();
ServiceFault serviceFault = faultException.getServiceFault();
logger.debug("Notifying {} ServiceFaultListeners", faultListeners.size());
faultNotificationQueue.submit(() ->
faultListeners.stream().forEach(h -> h.onServiceFault(serviceFault)));
}
}
}
public void addFaultListener(ServiceFaultListener faultListener) {
faultListeners.add(faultListener);
logger.debug("Added ServiceFaultListener: {}", faultListener);
}
public void removeFaultListener(ServiceFaultListener faultListener) {
faultListeners.remove(faultListener);
logger.debug("Removed ServiceFaultListener: {}", faultListener);
}
public void addSessionActivityListener(SessionActivityListener listener) {
sessionManager.addListener(listener);
logger.debug("Added SessionActivityListener: {}", listener);
}
public void removeSessionActivityListener(SessionActivityListener listener) {
sessionManager.removeListener(listener);
logger.debug("Removed SessionActivityListener: {}", listener);
}
}