code: /*
   * Copyright (c) 2021 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.nodes.factories;
  
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.ExecutionException;
  import java.util.concurrent.TimeUnit;
  
  import com.google.common.cache.Cache;
  import com.google.common.cache.CacheBuilder;
  import org.eclipse.milo.opcua.sdk.core.Reference;
  import org.eclipse.milo.opcua.sdk.core.nodes.ObjectTypeNode;
  import org.eclipse.milo.opcua.sdk.core.nodes.VariableTypeNode;
  import org.eclipse.milo.opcua.sdk.server.ObjectTypeManager;
  import org.eclipse.milo.opcua.sdk.server.VariableTypeManager;
  import org.eclipse.milo.opcua.sdk.server.api.AddressSpaceManager;
  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.UaNodeContext;
  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.UaVariableTypeNode;
  import org.eclipse.milo.opcua.stack.core.Identifiers;
  import org.eclipse.milo.opcua.stack.core.NamespaceTable;
  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.ExpandedNodeId;
  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.util.Tree;
  import org.jetbrains.annotations.Nullable;
  import org.slf4j.LoggerFactory;
  
  public class NodeFactory {
  
      private static final Cache<NodeId, InstanceDeclarationHierarchy> IDH_CACHE = CacheBuilder.newBuilder()
          .maximumSize(1024)
          .expireAfterAccess(1, TimeUnit.HOURS)
          .build();
  
      /**
       * Invalidate the cached {@link InstanceDeclarationHierarchy} for {@code typeDefinitionId}.
       *
       * @param typeDefinitionId the {@link NodeId} type definition to invalidate.
       */
      public static void invalidateCachedIdh(NodeId typeDefinitionId) {
          IDH_CACHE.invalidate(typeDefinitionId);
      }
  
      private final UaNodeContext context;
      private final ObjectTypeManager objectTypeManager;
      private final VariableTypeManager variableTypeManager;
  
      public NodeFactory(UaNodeContext context) {
          this(
              context,
              context.getServer().getObjectTypeManager(),
              context.getServer().getVariableTypeManager()
          );
      }
  
      public NodeFactory(
          UaNodeContext context,
          ObjectTypeManager objectTypeManager,
          VariableTypeManager variableTypeManager
      ) {
  
          this.context = context;
          this.objectTypeManager = objectTypeManager;
          this.variableTypeManager = variableTypeManager;
      }
  
      public UaNode createNode(
          NodeId rootNodeId,
          NodeId typeDefinitionId
      ) throws UaException {
  
          return createNode(rootNodeId, typeDefinitionId, new InstantiationCallback() {});
      }
  
      public UaNode createNode(
          NodeId rootNodeId,
          NodeId typeDefinitionId,
          InstantiationCallback instantiationCallback
      ) throws UaException {
  
          Tree<UaNode> nodeTree = createNodeTree(
              rootNodeId,
              typeDefinitionId,
              instantiationCallback
          );
  
          return nodeTree.getValue();
      }
  
      public UaNode createNode(
          NodeId rootNodeId,
          ExpandedNodeId typeDefinitionId
      ) throws UaException {
  
          return createNode(rootNodeId, typeDefinitionId, new InstantiationCallback() {});
      }
  
      public UaNode createNode(
          NodeId rootNodeId,
          ExpandedNodeId typeDefinitionId,
          InstantiationCallback instantiationCallback
      ) throws UaException {
  
          NodeId localTypeDefinitionId = typeDefinitionId.toNodeId(context.getNamespaceTable())
              .orElseThrow(
                  () ->
                      new UaException(
                          StatusCodes.Bad_NodeIdUnknown,
                          "typeDefinitionId not local: " + typeDefinitionId)
              );
  
          return createNode(rootNodeId, localTypeDefinitionId, instantiationCallback);
      }
  
      public Tree<UaNode> createNodeTree(
          NodeId rootNodeId,
          NodeId typeDefinitionId,
          InstantiationCallback instantiationCallback
      ) throws UaException {
  
          AddressSpaceManager addressSpaceManager = context.getServer().getAddressSpaceManager();
  
          if (!addressSpaceManager.getManagedNode(typeDefinitionId).isPresent()) {
              throw new UaException(
                  StatusCodes.Bad_NodeIdUnknown,
                  "unknown type definition: " + typeDefinitionId);
          }
  
          NamespaceTable namespaceTable = context.getServer().getNamespaceTable();
  
          InstanceDeclarationHierarchy idh;
  
          try {
              idh = IDH_CACHE.get(
                  typeDefinitionId,
                  () -> {
                      LoggerFactory.getLogger(NodeFactory.class).debug(
                          "InstanceDeclarationHierarchy cache " +
                              "miss for typeDefinitionId={}", typeDefinitionId
                      );
  
                      return InstanceDeclarationHierarchy.create(
                          addressSpaceManager,
                          namespaceTable,
                          typeDefinitionId
                      );
                  }
              );
          } catch (ExecutionException e) {
              throw new UaException(StatusCodes.Bad_InternalError, e);
          }
  
          NodeTable nodeTable = idh.getNodeTable();
          ReferenceTable referenceTable = idh.getReferenceTable();
  
          Map<BrowsePath, UaNode> nodes = new HashMap<>();
  
          for (Map.Entry<BrowsePath, NodeId> entry : nodeTable.nodes.entrySet()) {
              BrowsePath browsePath = entry.getKey();
              NodeId nodeId = entry.getValue();
  
              UaNode node = addressSpaceManager.getManagedNode(nodeId).orElse(null);
  
              if (browsePath.parent == null) {
                  // Root Node of hierarchy will be the ObjectType or VariableType to be instantiated
  
                  if (node instanceof UaObjectTypeNode) {
                      UaNode instance = instanceFromTypeDefinition(rootNodeId, (UaObjectTypeNode) node);
  
                      nodes.put(browsePath, instance);
                  } else if (node instanceof UaVariableTypeNode) {
                      UaNode instance = instanceFromTypeDefinition(rootNodeId, (UaVariableTypeNode) node);
  
                      nodes.put(browsePath, instance);
                  } else {
                      throw new UaException(StatusCodes.Bad_InternalError);
                  }
              } else {
                  // Non-root Nodes are all instance declarations
                  NodeId instanceNodeId = instanceNodeId(rootNodeId, browsePath);
  
                  if (node instanceof UaMethodNode) {
                      UaMethodNode declaration = (UaMethodNode) node;
  
                      UaMethodNode instance = new UaMethodNode(
                          context,
                          instanceNodeId,
                          declaration.getBrowseName(),
                          declaration.getDisplayName(),
                          declaration.getDescription(),
                          declaration.getWriteMask(),
                          declaration.getUserWriteMask(),
                          declaration.isExecutable(),
                          declaration.isUserExecutable()
                      );
  
                      nodes.put(browsePath, instance);
                  } else if (node instanceof UaObjectNode) {
                      UaObjectNode declaration = (UaObjectNode) node;
  
                      ExpandedNodeId instanceTypeDefinitionId =
                          getTypeDefinition(referenceTable, browsePath);
  
                      UaNode typeDefinitionNode = addressSpaceManager
                          .getManagedNode(instanceTypeDefinitionId)
                          .orElse(null);
  
                      if (typeDefinitionNode instanceof ObjectTypeNode) {
                          boolean optional = isOptionalDeclaration(declaration);
  
                          if (!optional || instantiationCallback.includeOptionalNode(
                              typeDefinitionNode.getNodeId(), declaration.getBrowseName())) {
  
                              UaObjectNode instance = instanceFromTypeDefinition(
                                  instanceNodeId, (ObjectTypeNode) typeDefinitionNode);
  
                              instance.setBrowseName(declaration.getBrowseName());
                              instance.setDisplayName(declaration.getDisplayName());
                              instance.setDescription(declaration.getDescription());
                              instance.setWriteMask(declaration.getWriteMask());
                              instance.setUserWriteMask(declaration.getUserWriteMask());
                              instance.setEventNotifier(declaration.getEventNotifier());
  
                              nodes.put(browsePath, instance);
                          }
                      } else {
                          throw new UaException(
                              StatusCodes.Bad_InternalError,
                              "expected type definition for " + instanceTypeDefinitionId);
                      }
                  } else if (node instanceof UaVariableNode) {
                      UaVariableNode declaration = (UaVariableNode) node;
  
                      ExpandedNodeId instanceTypeDefinitionId =
                          getTypeDefinition(referenceTable, browsePath);
  
                      UaNode typeDefinitionNode = addressSpaceManager
                          .getManagedNode(instanceTypeDefinitionId)
                          .orElse(null);
  
                      if (typeDefinitionNode instanceof VariableTypeNode) {
                          boolean optional = isOptionalDeclaration(declaration);
  
                          if (!optional || instantiationCallback.includeOptionalNode(
                              typeDefinitionNode.getNodeId(), declaration.getBrowseName())) {
                              UaVariableNode instance = instanceFromTypeDefinition(
                                  instanceNodeId, (VariableTypeNode) typeDefinitionNode);
  
                              instance.setBrowseName(declaration.getBrowseName());
                              instance.setDisplayName(declaration.getDisplayName());
                              instance.setDescription(declaration.getDescription());
                              instance.setWriteMask(declaration.getWriteMask());
                              instance.setUserWriteMask(declaration.getUserWriteMask());
                              instance.setValue(declaration.getValue());
                              instance.setDataType(declaration.getDataType());
                              instance.setValueRank(declaration.getValueRank());
                              instance.setArrayDimensions(declaration.getArrayDimensions());
                              instance.setAccessLevel(declaration.getAccessLevel());
                              instance.setUserAccessLevel(declaration.getUserAccessLevel());
  
                              nodes.put(browsePath, instance);
                          }
                      } else {
                          throw new UaException(
                              StatusCodes.Bad_InternalError,
                              "expected type definition for " + instanceTypeDefinitionId);
                      }
                  } else {
                      throw new UaException(
                          StatusCodes.Bad_InternalError,
                          "not an instance declaration: " + node);
                  }
              }
          }
  
          nodes.forEach((browsePath, node) -> {
              List<ReferenceTable.RefRow> references = referenceTable.getReferences(browsePath);
  
              references.forEach(t -> {
                  NodeId referenceTypeId = t.nodeId;
                  ReferenceTable.RefTarget target = t.target;
  
                  if (!Identifiers.HasModellingRule.equals(referenceTypeId)) {
                      if (target.targetNodeId != null) {
                          node.addReference(new Reference(
                              node.getNodeId(),
                              referenceTypeId,
                              target.targetNodeId,
                              true
                          ));
                      } else {
                          BrowsePath targetPath = target.targetPath;
  
                          UaNode targetNode = nodes.get(targetPath);
  
                          if (targetNode != null) {
                              node.addReference(new Reference(
                                  node.getNodeId(),
                                  referenceTypeId,
                                  targetNode.getNodeId().expanded(),
                                  true
                              ));
                          }
                      }
                  }
              });
  
              context.getNodeManager().addNode(node);
          });
  
          Tree<UaNode> nodeTree = nodeTable.getBrowsePathTree().map(nodes::get);
  
          notifyInstantiationCallback(nodeTree, instantiationCallback);
  
          return nodeTree;
      }
  
      protected void notifyInstantiationCallback(Tree<UaNode> nodeTree, InstantiationCallback instantiationCallback) {
          nodeTree.traverseWithParent((node, parentNode) -> {
              if (parentNode instanceof UaObjectNode && node instanceof UaMethodNode) {
                  UaMethodNode methodNode = (UaMethodNode) node;
  
                  instantiationCallback.onMethodAdded((UaObjectNode) parentNode, methodNode);
              } else if (node instanceof UaObjectNode) {
                  UaObjectNode objectNode = (UaObjectNode) node;
                  ObjectTypeNode objectTypeNode = objectNode.getTypeDefinitionNode();
  
                  instantiationCallback.onObjectAdded(parentNode, objectNode, objectTypeNode.getNodeId());
              } else if (node instanceof UaVariableNode) {
                  UaVariableNode variableNode = (UaVariableNode) node;
                  VariableTypeNode variableTypeNode = variableNode.getTypeDefinitionNode();
  
                  instantiationCallback.onVariableAdded(parentNode, variableNode, variableTypeNode.getNodeId());
              }
          });
      }
  
      /**
       * Return an appropriate {@link NodeId} for the instance being created.
       *
       * @param rootNodeId the root {@link NodeId}.
       * @param browsePath the relative {@link BrowsePath} to the instance being created.
       * @return a {@link NodeId} for the instance being created.
       */
      protected NodeId instanceNodeId(NodeId rootNodeId, BrowsePath browsePath) {
          Object rootIdentifier = rootNodeId.getIdentifier();
  
          String instanceIdentifier = String.format("%s%s", rootIdentifier, browsePath.join());
  
          return new NodeId(rootNodeId.getNamespaceIndex(), instanceIdentifier);
      }
  
      protected UaObjectNode instanceFromTypeDefinition(
          NodeId nodeId,
          ObjectTypeNode typeDefinitionNode
      ) {
  
          NodeId typeDefinitionId = typeDefinitionNode.getNodeId();
  
          // Use a specialized instance if one is registered, otherwise fallback to UaObjectNode.
          ObjectTypeManager.ObjectNodeConstructor ctor = objectTypeManager
              .getNodeFactory(typeDefinitionId)
              .orElse(UaObjectNode::new);
  
          return ctor.apply(
              context,
              nodeId,
              typeDefinitionNode.getBrowseName(),
              typeDefinitionNode.getDisplayName(),
              typeDefinitionNode.getDescription(),
              typeDefinitionNode.getWriteMask(),
              typeDefinitionNode.getUserWriteMask()
          );
      }
  
      protected UaVariableNode instanceFromTypeDefinition(
          NodeId nodeId,
          VariableTypeNode typeDefinitionNode
      ) {
  
          NodeId typeDefinitionId = typeDefinitionNode.getNodeId();
  
          // Use a specialized instance if one is registered, otherwise fallback to UaVariableNode.
          VariableTypeManager.VariableNodeConstructor ctor = variableTypeManager
              .getNodeFactory(typeDefinitionId)
              .orElse(UaVariableNode::new);
  
          return ctor.apply(
              context,
              nodeId,
              typeDefinitionNode.getBrowseName(),
              typeDefinitionNode.getDisplayName(),
              typeDefinitionNode.getDescription(),
              typeDefinitionNode.getWriteMask(),
              typeDefinitionNode.getUserWriteMask()
          );
      }
  
      protected boolean isOptionalDeclaration(UaNode node) {
          return node.getReferences()
              .stream()
              .filter(r -> Identifiers.HasModellingRule.equals(r.getReferenceTypeId()))
              .anyMatch(r -> Identifiers.ModellingRule_Optional.equalTo(r.getTargetNodeId()));
      }
  
      private static ExpandedNodeId getTypeDefinition(ReferenceTable referenceTable, BrowsePath browsePath) {
          return referenceTable
              .getReferences(browsePath)
              .stream()
              .filter(t -> t.nodeId.equals(Identifiers.HasTypeDefinition))
              .map(t -> t.target.targetNodeId)
              .findFirst()
              .orElse(ExpandedNodeId.NULL_VALUE);
      }
  
      public interface InstantiationCallback {
  
          /**
           * Called determine whether the optional member named {@code browseName} should be added to an instantiated
           * Node of the type identified by {@code typeDefinitionId}
           *
           * @param typeDefinitionId the type definition id of the {@link UaNode} being instantiated.
           * @param browseName       the {@link QualifiedName} of the optional member.
           * @return {@code true} if the optional member named {@code browseName} should be added.
           */
          default boolean includeOptionalNode(NodeId typeDefinitionId, QualifiedName browseName) {
              return false;
          }
  
          /**
           * Called when a {@link UaMethodNode} has been added to a {@link UaObjectNode} somewhere in the instance
           * hierarchy.
           *
           * @param parent   the {@link UaObjectNode} the method was added to.
           * @param instance the {@link UaMethodNode} instance.
           */
          default void onMethodAdded(@Nullable UaObjectNode parent, UaMethodNode instance) {}
  
          /**
           * Called when a {@link UaObjectNode} has been added to a parent {@link UaNode} by a hierarchical
           * reference somewhere in the instance hierarchy.
           * <p>
           * If {@code parent} is {@code null} then {@code instance} is the root of the instance hierarchy.
           *
           * @param parent           the parent {@link UaNode}.
           * @param instance         the {@link UaObjectNode} instance.
           * @param typeDefinitionId the {@link NodeId} of the ObjectTypeDefinition.
           */
          default void onObjectAdded(@Nullable UaNode parent, UaObjectNode instance, NodeId typeDefinitionId) {}
  
          /**
           * Called when a {@link UaVariableNode} has been added to a parent {@link UaNode} by a hierarchical
           * reference somewhere in the instance hierarchy.
           * <p>
           * If {@code parent} is {@code null} then {@code instance} is the root of the instance hierarchy.
           *
           * @param parent           the parent {@link UaNode}.
           * @param instance         the {@link UaVariableNode} instance.
           * @param typeDefinitionId the {@link NodeId} of the VariableTypeDefinition.
           */
          default void onVariableAdded(@Nullable UaNode parent, UaVariableNode instance, NodeId typeDefinitionId) {}
  
      }
  
  }
  