code: /*
   * Copyright (c) 2019 the Eclipse Milo Authors
   *
   * This program and the accompanying materials are made
   * available under the terms of the Eclipse Public License 2.0
   * which is available at https://www.eclipse.org/legal/epl-2.0/
   *
   * SPDX-License-Identifier: EPL-2.0
   */
  
  package org.eclipse.milo.opcua.sdk.server.namespaces;
  
  import java.util.List;
  import java.util.Locale;
  import java.util.UUID;
  import java.util.concurrent.TimeUnit;
  import java.util.function.Function;
  
  import com.google.common.collect.Lists;
  import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
  import org.eclipse.milo.opcua.sdk.server.Session;
  import org.eclipse.milo.opcua.sdk.server.api.DataItem;
  import org.eclipse.milo.opcua.sdk.server.api.EventItem;
  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.config.OpcUaServerConfigLimits;
  import org.eclipse.milo.opcua.sdk.server.api.methods.AbstractMethodInvocationHandler;
  import org.eclipse.milo.opcua.sdk.server.api.methods.Out;
  import org.eclipse.milo.opcua.sdk.server.items.BaseMonitoredItem;
  import org.eclipse.milo.opcua.sdk.server.items.MonitoredDataItem;
  import org.eclipse.milo.opcua.sdk.server.model.methods.ConditionRefreshMethod;
  import org.eclipse.milo.opcua.sdk.server.model.methods.GetMonitoredItemsMethod;
  import org.eclipse.milo.opcua.sdk.server.model.methods.ResendDataMethod;
  import org.eclipse.milo.opcua.sdk.server.model.nodes.objects.BaseEventTypeNode;
  import org.eclipse.milo.opcua.sdk.server.model.nodes.objects.OperationLimitsTypeNode;
  import org.eclipse.milo.opcua.sdk.server.model.nodes.objects.ServerCapabilitiesTypeNode;
  import org.eclipse.milo.opcua.sdk.server.model.nodes.objects.ServerTypeNode;
  import org.eclipse.milo.opcua.sdk.server.model.nodes.variables.ServerStatusTypeNode;
  import org.eclipse.milo.opcua.sdk.server.namespaces.loader.NodeLoader;
  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.UaVariableNode;
  import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilters;
  import org.eclipse.milo.opcua.sdk.server.subscriptions.Subscription;
  import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel;
  import org.eclipse.milo.opcua.stack.core.Identifiers;
  import org.eclipse.milo.opcua.stack.core.StatusCodes;
  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.DateTime;
  import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject;
  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.QualifiedName;
  import org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
  import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
  import org.eclipse.milo.opcua.stack.core.types.enumerated.RedundancySupport;
  import org.eclipse.milo.opcua.stack.core.types.enumerated.ServerState;
  import org.eclipse.milo.opcua.stack.core.types.structured.BuildInfo;
  import org.eclipse.milo.opcua.stack.core.types.structured.ServerStatusDataType;
  import org.eclipse.milo.opcua.stack.core.util.Namespaces;
  import org.eclipse.milo.opcua.stack.core.util.NonceUtil;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte;
  import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
  import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort;
  
  public class OpcUaNamespace extends ManagedNamespaceWithLifecycle {
  
      private static final double MIN_SAMPLING_INTERVAL = 100.0;
  
      private final Logger logger = LoggerFactory.getLogger(getClass());
  
      private final SubscriptionModel subscriptionModel;
  
      private final OpcUaServer server;
  
      public OpcUaNamespace(OpcUaServer server) {
          super(server, Namespaces.OPC_UA);
  
          this.server = server;
  
          subscriptionModel = new SubscriptionModel(server, this);
  
          getLifecycleManager().addStartupTask(() -> {
              loadNodes();
              configureServerObject();
              configureConditionRefresh();
  
              // Set a reasonable value for the MinimumSamplingInterval
              // attribute on all VariableNodes, otherwise it defaults to 0.
              getNodeManager().getNodes()
                  .stream()
                  .filter(node -> node instanceof UaVariableNode)
                  .map(UaVariableNode.class::cast)
                  .forEach(n -> n.setMinimumSamplingInterval(MIN_SAMPLING_INTERVAL));
          });
  
          getLifecycleManager().addLifecycle(subscriptionModel);
      }
  
      @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);
      }
  
      @Override
      public void onEventItemsCreated(List<EventItem> eventItems) {
          eventItems.stream()
              .filter(MonitoredItem::isSamplingEnabled)
              .forEach(item -> server.getEventBus().register(item));
      }
  
      @Override
      public void onEventItemsModified(List<EventItem> eventItems) {
          for (EventItem item : eventItems) {
              if (item.isSamplingEnabled()) {
                  server.getEventBus().register(item);
              } else {
                  server.getEventBus().unregister(item);
              }
          }
      }
  
      @Override
      public void onEventItemsDeleted(List<EventItem> eventItems) {
          eventItems.forEach(item -> server.getEventBus().unregister(item));
      }
  
      private void loadNodes() {
          try {
              long startTime = System.nanoTime();
              long startCount = getNodeManager().getNodes().size();
  
              new NodeLoader(getNodeContext(), getNodeManager()).loadNodes();
  
              long deltaMs = TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
              long deltaCount = getNodeManager().getNodes().size() - startCount;
  
              logger.info("Loaded {} nodes in {}ms.", deltaCount, deltaMs);
          } catch (Exception e) {
              logger.error("Error loading nodes.", e);
          }
      }
  
      private void configureServerObject() {
          ServerTypeNode serverTypeNode = (ServerTypeNode) getNodeManager().get(Identifiers.Server);
  
          assert serverTypeNode != null;
  
          // This Node is optional and we don't support it, so delete it entirely.
          serverTypeNode.getNamespacesNode().delete();
  
          serverTypeNode.getNamespaceArrayNode().getFilterChain().addLast(
              AttributeFilters.getValue(
                  ctx ->
                      new DataValue(new Variant(server.getNamespaceTable().toArray()))
              )
          );
  
          serverTypeNode.getServerArrayNode().getFilterChain().addLast(
              AttributeFilters.getValue(
                  ctx ->
                      new DataValue(new Variant(server.getServerTable().toArray()))
              )
          );
  
          serverTypeNode.setAuditing(false);
          serverTypeNode.getServerDiagnosticsNode().setEnabledFlag(false);
          serverTypeNode.setServiceLevel(ubyte(255));
  
          ServerStatusTypeNode serverStatus = serverTypeNode.getServerStatusNode();
  
          BuildInfo buildInfo = server.getConfig().getBuildInfo();
          serverStatus.setBuildInfo(buildInfo);
          serverStatus.getBuildInfoNode().setBuildDate(buildInfo.getBuildDate());
          serverStatus.getBuildInfoNode().setBuildNumber(buildInfo.getBuildNumber());
          serverStatus.getBuildInfoNode().setManufacturerName(buildInfo.getManufacturerName());
          serverStatus.getBuildInfoNode().setProductName(buildInfo.getProductName());
          serverStatus.getBuildInfoNode().setProductUri(buildInfo.getProductUri());
          serverStatus.getBuildInfoNode().setSoftwareVersion(buildInfo.getSoftwareVersion());
  
          serverStatus.setCurrentTime(DateTime.now());
          serverStatus.setSecondsTillShutdown(uint(0));
          serverStatus.setShutdownReason(LocalizedText.NULL_VALUE);
          serverStatus.setState(ServerState.Running);
          serverStatus.setStartTime(DateTime.now());
  
          serverStatus.getCurrentTimeNode().getFilterChain().addLast(
              AttributeFilters.getValue(
                  ctx ->
                      new DataValue(new Variant(DateTime.now()))
              )
          );
  
          serverStatus.getFilterChain().addLast(
              AttributeFilters.getValue(ctx -> {
                  ServerStatusTypeNode serverStatusNode = (ServerStatusTypeNode) ctx.getNode();
  
                  ExtensionObject xo = ExtensionObject.encode(
                      server.getSerializationContext(),
                      new ServerStatusDataType(
                          serverStatusNode.getStartTime(),
                          DateTime.now(),
                          serverStatusNode.getState(),
                          serverStatusNode.getBuildInfo(),
                          serverStatusNode.getSecondsTillShutdown(),
                          serverStatusNode.getShutdownReason()
                      )
                  );
  
                  return new DataValue(new Variant(xo));
              })
          );
  
          final OpcUaServerConfigLimits limits = server.getConfig().getLimits();
          ServerCapabilitiesTypeNode serverCapabilities = serverTypeNode.getServerCapabilitiesNode();
          serverCapabilities.setServerProfileArray(new String[]{"http://opcfoundation.org/UA-Profile/Server/StandardUA"});
          serverCapabilities.setLocaleIdArray(new String[]{Locale.ENGLISH.getLanguage()});
          serverCapabilities.setMaxArrayLength(limits.getMaxArrayLength());
          serverCapabilities.setMaxStringLength(limits.getMaxStringLength());
          serverCapabilities.setMaxByteStringLength(limits.getMaxByteStringLength());
          serverCapabilities.setMaxBrowseContinuationPoints(limits.getMaxBrowseContinuationPoints());
          serverCapabilities.setMaxHistoryContinuationPoints(limits.getMaxHistoryContinuationPoints());
          serverCapabilities.setMaxQueryContinuationPoints(limits.getMaxQueryContinuationPoints());
          serverCapabilities.setMinSupportedSampleRate(limits.getMinSupportedSampleRate());
  
          OperationLimitsTypeNode limitsNode = serverCapabilities.getOperationLimitsNode();
          limitsNode.setMaxMonitoredItemsPerCall(limits.getMaxMonitoredItemsPerCall());
          limitsNode.setMaxNodesPerBrowse(limits.getMaxNodesPerBrowse());
          limitsNode.setMaxNodesPerHistoryReadData(limits.getMaxNodesPerHistoryReadData());
          limitsNode.setMaxNodesPerHistoryReadEvents(limits.getMaxNodesPerHistoryReadEvents());
          limitsNode.setMaxNodesPerHistoryUpdateData(limits.getMaxNodesPerHistoryUpdateData());
          limitsNode.setMaxNodesPerHistoryUpdateEvents(limits.getMaxNodesPerHistoryUpdateEvents());
          limitsNode.setMaxNodesPerMethodCall(limits.getMaxNodesPerMethodCall());
          limitsNode.setMaxNodesPerNodeManagement(limits.getMaxNodesPerNodeManagement());
          limitsNode.setMaxNodesPerRead(limits.getMaxNodesPerRead());
          limitsNode.setMaxNodesPerRegisterNodes(limits.getMaxNodesPerRegisterNodes());
          limitsNode.setMaxNodesPerTranslateBrowsePathsToNodeIds(limits.getMaxNodesPerTranslateBrowsePathsToNodeIds());
          limitsNode.setMaxNodesPerWrite(limits.getMaxNodesPerWrite());
  
          serverTypeNode.getServerRedundancyNode().setRedundancySupport(RedundancySupport.None);
  
          configureGetMonitoredItems();
          configureResendData();
      }
  
      private void configureGetMonitoredItems() {
          UaNode node = getNodeManager().get(Identifiers.Server_GetMonitoredItems);
  
          if (node instanceof UaMethodNode) {
              UaMethodNode methodNode = (UaMethodNode) node;
  
              configureMethodNode(methodNode, GetMonitoredItemsMethodImpl::new);
          } else {
              logger.warn("GetMonitoredItems UaMethodNode not found.");
          }
      }
  
      private void configureResendData() {
          UaNode node = getNodeManager().get(Identifiers.Server_ResendData);
  
          if (node instanceof UaMethodNode) {
              UaMethodNode resendDataNode = (UaMethodNode) node;
  
              configureMethodNode(resendDataNode, ResendDataMethodImpl::new);
          } else {
              logger.warn("ResendData UaMethodNode not found.");
          }
      }
  
      private void configureConditionRefresh() {
          UaNode node = getNodeManager().get(Identifiers.ConditionType_ConditionRefresh);
  
          if (node instanceof UaMethodNode) {
              UaMethodNode conditionRefreshNode = (UaMethodNode) node;
  
              configureMethodNode(conditionRefreshNode, ConditionRefreshMethodImpl::new);
          } else {
              logger.warn("ConditionRefresh UaMethodNode not found.");
          }
      }
  
      private static <T extends AbstractMethodInvocationHandler> void configureMethodNode(
          UaMethodNode methodNode,
          Function<UaMethodNode, T> f
      ) {
  
          T invocationHandler = f.apply(methodNode);
  
          methodNode.setInvocationHandler(invocationHandler);
          methodNode.setInputArguments(invocationHandler.getInputArguments());
          methodNode.setOutputArguments(invocationHandler.getOutputArguments());
      }
  
      private static class ConditionRefreshMethodImpl extends ConditionRefreshMethod {
  
          private final OpcUaServer server;
  
          ConditionRefreshMethodImpl(UaMethodNode node) {
              super(node);
  
              server = node.getNodeContext().getServer();
          }
  
          @Override
          protected void invoke(InvocationContext context, UInteger subscriptionId) throws UaException {
              Session session = context.getSession().orElse(null);
  
              if (session != null) {
                  Subscription subscription = session.getSubscriptionManager().getSubscription(subscriptionId);
  
                  if (subscription != null) {
                      BaseEventTypeNode refreshStart = server.getEventFactory().createEvent(
                          new NodeId(1, UUID.randomUUID()),
                          Identifiers.RefreshStartEventType
                      );
  
                      refreshStart.setBrowseName(new QualifiedName(1, "RefreshStart"));
                      refreshStart.setDisplayName(LocalizedText.english("RefreshStart"));
                      refreshStart.setEventId(NonceUtil.generateNonce(16));
                      refreshStart.setEventType(Identifiers.RefreshStartEventType);
                      refreshStart.setSourceNode(Identifiers.Server);
                      refreshStart.setSourceName("Server");
                      refreshStart.setTime(DateTime.now());
                      refreshStart.setReceiveTime(DateTime.NULL_VALUE);
                      refreshStart.setMessage(LocalizedText.english("RefreshStart"));
                      refreshStart.setSeverity(ushort(0));
  
                      BaseEventTypeNode refreshEnd = server.getEventFactory().createEvent(
                          new NodeId(1, UUID.randomUUID()),
                          Identifiers.RefreshEndEventType
                      );
  
                      refreshEnd.setBrowseName(new QualifiedName(1, "RefreshEnd"));
                      refreshEnd.setDisplayName(LocalizedText.english("RefreshEnd"));
                      refreshEnd.setEventId(NonceUtil.generateNonce(16));
                      refreshEnd.setEventType(Identifiers.RefreshEndEventType);
                      refreshEnd.setSourceNode(Identifiers.Server);
                      refreshEnd.setSourceName("Server");
                      refreshEnd.setTime(DateTime.now());
                      refreshEnd.setReceiveTime(DateTime.NULL_VALUE);
                      refreshEnd.setMessage(LocalizedText.english("RefreshEnd"));
                      refreshEnd.setSeverity(ushort(0));
  
                      server.getEventBus().post(refreshStart);
                      server.getEventBus().post(refreshEnd);
  
                      refreshStart.delete();
                      refreshEnd.delete();
                  } else {
                      throw new UaException(StatusCodes.Bad_SubscriptionIdInvalid);
                  }
              } else {
                  throw new UaException(StatusCodes.Bad_UserAccessDenied);
              }
          }
  
      }
  
      private static class GetMonitoredItemsMethodImpl extends GetMonitoredItemsMethod {
  
          private final OpcUaServer server;
  
          GetMonitoredItemsMethodImpl(UaMethodNode node) {
              super(node);
  
              server = node.getNodeContext().getServer();
          }
  
          @Override
          protected void invoke(
              InvocationContext context,
              UInteger subscriptionId,
              Out<UInteger[]> serverHandles,
              Out<UInteger[]> clientHandles
          ) throws UaException {
  
              Session session = context.getSession().orElseThrow(
                  () ->
                      new UaException(StatusCodes.Bad_SessionIdInvalid)
              );
  
              Subscription subscription = server.getSubscriptions().get(subscriptionId);
  
              if (subscription == null) {
                  throw new UaException(StatusCodes.Bad_SubscriptionIdInvalid);
              }
  
              if (!session.getSessionId().equals(subscription.getSession().getSessionId())) {
                  throw new UaException(StatusCodes.Bad_UserAccessDenied);
              }
  
              List<UInteger> serverHandleList = Lists.newArrayList();
              List<UInteger> clientHandleList = Lists.newArrayList();
  
              for (BaseMonitoredItem<?> item : subscription.getMonitoredItems().values()) {
                  serverHandleList.add(item.getId());
                  clientHandleList.add(uint(item.getClientHandle()));
              }
  
              serverHandles.set(serverHandleList.toArray(new UInteger[0]));
              clientHandles.set(clientHandleList.toArray(new UInteger[0]));
          }
  
      }
  
      private static class ResendDataMethodImpl extends ResendDataMethod {
  
          ResendDataMethodImpl(UaMethodNode node) {
              super(node);
          }
  
          @Override
          protected void invoke(InvocationContext context, UInteger subscriptionId) throws UaException {
              Session session = context.getSession().orElse(null);
  
              if (session != null) {
                  Subscription subscription = session.getSubscriptionManager().getSubscription(subscriptionId);
  
                  if (subscription == null) {
                      if (session.getServer().getSubscriptions().containsKey(subscriptionId)) {
                          // Exists but belongs to another session
                          throw new UaException(StatusCodes.Bad_UserAccessDenied);
                      } else {
                          // Doesn't exist in any session
                          throw new UaException(StatusCodes.Bad_SubscriptionIdInvalid);
                      }
                  } else {
                      subscription.getMonitoredItems().values().stream()
                          .filter(item -> item instanceof MonitoredDataItem)
                          .map(item -> (MonitoredDataItem) item)
                          .forEach(MonitoredDataItem::maybeSendLastValue);
                  }
              } else {
                  throw new UaException(StatusCodes.Bad_UserAccessDenied);
              }
          }
  
      }
  
  }
  