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.List;
  import java.util.Optional;
  import java.util.stream.Collectors;
  
  import org.eclipse.milo.opcua.sdk.core.Reference;
  import org.eclipse.milo.opcua.sdk.server.api.AddressSpaceManager;
  import org.eclipse.milo.opcua.sdk.server.nodes.UaNode;
  import org.eclipse.milo.opcua.stack.core.Identifiers;
  import org.eclipse.milo.opcua.stack.core.NamespaceTable;
  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.enumerated.NodeClass;
  
  public class InstanceDeclarationHierarchy {
  
      private final NodeId typeId;
      private final NodeTable nodeTable;
      private final ReferenceTable referenceTable;
  
      private InstanceDeclarationHierarchy(
          NodeId typeId,
          NodeTable nodeTable,
          ReferenceTable referenceTable
      ) {
  
          this.typeId = typeId;
          this.nodeTable = nodeTable;
          this.referenceTable = referenceTable;
      }
  
      NodeTable getNodeTable() {
          return nodeTable;
      }
  
      ReferenceTable getReferenceTable() {
          return referenceTable;
      }
  
      private InstanceDeclarationHierarchy merge(InstanceDeclarationHierarchy parent) {
          NodeTable mergedNodeTable =
              NodeTable.merge(this.nodeTable, parent.nodeTable);
  
          ReferenceTable mergedReferenceTable =
              ReferenceTable.merge(this.referenceTable, parent.referenceTable);
  
          return new InstanceDeclarationHierarchy(typeId, mergedNodeTable, mergedReferenceTable);
      }
  
      public static InstanceDeclarationHierarchy create(
          AddressSpaceManager addressSpaceManager,
          NamespaceTable namespaceTable,
          NodeId typeDefinitionId
      ) {
  
          Builder builder = new Builder(addressSpaceManager, namespaceTable);
  
          return builder.build(typeDefinitionId);
      }
  
      static class Builder {
  
          private final NodeTable nodeTable = new NodeTable();
          private final ReferenceTable referenceTable = new ReferenceTable();
  
          private final AddressSpaceManager addressSpaceManager;
          private final NamespaceTable namespaceTable;
  
          Builder(AddressSpaceManager addressSpaceManager, NamespaceTable namespaceTable) {
              this.addressSpaceManager = addressSpaceManager;
              this.namespaceTable = namespaceTable;
          }
  
          public InstanceDeclarationHierarchy build(NodeId typeDefinitionId) {
              Optional<InstanceDeclarationHierarchy> parentIdh = addressSpaceManager
                  .getManagedReferences(typeDefinitionId)
                  .stream()
                  .filter(r -> r.isInverse() && Identifiers.HasSubtype.equals(r.getReferenceTypeId()))
                  .findFirst()
                  .flatMap(r -> r.getTargetNodeId().toNodeId(namespaceTable))
                  .map(parentTypeId -> InstanceDeclarationHierarchy
                      .create(addressSpaceManager, namespaceTable, parentTypeId));
  
              final InstanceDeclarationHierarchy idh = buildHierarchyForType(typeDefinitionId);
  
              return parentIdh.map(idh::merge).orElse(idh);
          }
  
          private InstanceDeclarationHierarchy buildHierarchyForType(NodeId typeDefinitionId) {
              BrowsePath browsePath = BrowsePath.ROOT;
  
              nodeTable.addNode(browsePath, typeDefinitionId);
              referenceTable.addReference(browsePath, Identifiers.HasTypeDefinition, typeDefinitionId.expanded());
  
              addModeledNodes(typeDefinitionId, browsePath);
  
              return new InstanceDeclarationHierarchy(typeDefinitionId, nodeTable, referenceTable);
          }
  
          private void addModeledNodes(NodeId typeDefinitionId, BrowsePath parentPath) {
              List<Reference> forwardReferences = addressSpaceManager.getManagedReferences(typeDefinitionId)
                  .stream()
                  .filter(Reference::isForward)
                  .collect(Collectors.toList());
  
              forwardReferences.forEach(reference ->
                  addressSpaceManager.getManagedNode(reference.getTargetNodeId()).ifPresent(node -> {
                      // All InstanceDeclarations of the InstanceDeclarationHierarchy and all Nodes referenced with a
                      // non-hierarchical Reference from such an InstanceDeclaration are added to the table.
                      // Hierarchical References to Nodes without a ModellingRule are not considered.
  
                      BrowsePath browsePath = new BrowsePath(parentPath, node.getBrowseName());
  
                      if (isInstanceDeclaration(node)) {
                          nodeTable.addNode(browsePath, node.getNodeId());
                          referenceTable.addReference(parentPath, reference.getReferenceTypeId(), browsePath);
  
                          node.getReferences()
                              .stream()
                              .filter(r -> r.subtypeOf(Identifiers.NonHierarchicalReferences))
                              .forEach(r -> referenceTable
                                  .addReference(browsePath, r.getReferenceTypeId(), r.getTargetNodeId()));
  
                          // Recursively add any additional instance declarations, on both this
                          // instance declaration and its type definition if applicable.
  
                          addModeledNodes(node.getNodeId(), browsePath);
  
                          Optional<ExpandedNodeId> instanceDeclarationTypeDefinitionId = node.getReferences()
                              .stream()
                              .filter(r -> Identifiers.HasTypeDefinition.equals(r.getReferenceTypeId()))
                              .findFirst()
                              .map(Reference::getTargetNodeId);
  
                          instanceDeclarationTypeDefinitionId.flatMap(xni -> xni.toNodeId(namespaceTable))
                              .ifPresent(id -> addModeledNodes(id, browsePath));
                      }
                  })
              );
          }
  
          private static boolean isInstanceDeclaration(UaNode potentialMemberNode) {
              NodeClass nodeClass = potentialMemberNode.getNodeClass();
  
              boolean methodOrObjectOrVariable =
                  nodeClass == NodeClass.Method ||
                      nodeClass == NodeClass.Object ||
                      nodeClass == NodeClass.Variable;
  
              return methodOrObjectOrVariable && hasModellingRule(potentialMemberNode);
          }
  
          private static boolean hasModellingRule(UaNode potentialMemberNode) {
              return potentialMemberNode.getReferences()
                  .stream()
                  .filter(r -> Identifiers.HasModellingRule.equals(r.getReferenceTypeId()))
                  .anyMatch(r ->
                      Identifiers.ModellingRule_Mandatory.equalTo(r.getTargetNodeId()) ||
                          Identifiers.ModellingRule_Optional.equalTo(r.getTargetNodeId())
                  );
          }
  
      }
  
  
  }
  