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;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Objects;
  import java.util.function.Function;
  import java.util.function.Supplier;
  import java.util.stream.Collectors;
  
  import com.google.common.base.Preconditions;
  import org.eclipse.milo.opcua.sdk.core.Reference;
  import org.eclipse.milo.opcua.sdk.core.nodes.Node;
  import org.eclipse.milo.opcua.sdk.core.nodes.ObjectNode;
  import org.eclipse.milo.opcua.sdk.core.nodes.ObjectNodeProperties;
  import org.eclipse.milo.opcua.sdk.core.nodes.ObjectTypeNode;
  import org.eclipse.milo.opcua.sdk.server.api.AddressSpaceManager;
  import org.eclipse.milo.opcua.sdk.server.api.NodeManager;
  import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilter;
  import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilterChain;
  import org.eclipse.milo.opcua.stack.core.AttributeId;
  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.ByteString;
  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.unsigned.UByte;
  import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
  import org.eclipse.milo.opcua.stack.core.types.enumerated.NamingRuleType;
  import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass;
  import org.jetbrains.annotations.Nullable;
  
  import static org.eclipse.milo.opcua.sdk.core.Reference.HAS_COMPONENT_PREDICATE;
  import static org.eclipse.milo.opcua.sdk.core.Reference.HAS_EVENT_SOURCE_PREDICATE;
  import static org.eclipse.milo.opcua.sdk.core.Reference.HAS_NOTIFIER_PREDICATE;
  import static org.eclipse.milo.opcua.sdk.core.Reference.HAS_ORDERED_COMPONENT_PREDICATE;
  import static org.eclipse.milo.opcua.sdk.core.Reference.HAS_PROPERTY_PREDICATE;
  import static org.eclipse.milo.opcua.sdk.core.Reference.HAS_TYPE_DEFINITION_PREDICATE;
  import static org.eclipse.milo.opcua.sdk.core.Reference.ORGANIZES_PREDICATE;
  import static org.eclipse.milo.opcua.sdk.core.util.StreamUtil.opt2stream;
  import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte;
  
  public class UaObjectNode extends UaNode implements ObjectNode {
  
      private UByte eventNotifier = ubyte(0);
  
      public UaObjectNode(
          UaNodeContext context,
          NodeId nodeId,
          ObjectTypeNode objectTypeNode) {
  
          this(context, nodeId, objectTypeNode.getBrowseName(), objectTypeNode.getDisplayName());
  
          setDescription(objectTypeNode.getDescription());
          setWriteMask(objectTypeNode.getWriteMask());
          setUserWriteMask(objectTypeNode.getUserWriteMask());
      }
  
      public UaObjectNode(
          UaNodeContext context,
          NodeId nodeId,
          QualifiedName browseName,
          LocalizedText displayName) {
  
          super(context, nodeId, NodeClass.Object, browseName, displayName);
      }
  
      public UaObjectNode(
          UaNodeContext context,
          NodeId nodeId,
          QualifiedName browseName,
          LocalizedText displayName,
          LocalizedText description,
          UInteger writeMask,
          UInteger userWriteMask) {
  
          super(context, nodeId, NodeClass.Object, browseName, displayName, description, writeMask, userWriteMask);
      }
  
      public UaObjectNode(
          UaNodeContext context,
          NodeId nodeId,
          QualifiedName browseName,
          LocalizedText displayName,
          LocalizedText description,
          UInteger writeMask,
          UInteger userWriteMask,
          UByte eventNotifier) {
  
          super(context, nodeId, NodeClass.Object,
              browseName, displayName, description, writeMask, userWriteMask);
  
          this.eventNotifier = eventNotifier;
      }
  
      @Override
      public UByte getEventNotifier() {
          return (UByte) filterChain.getAttribute(this, AttributeId.EventNotifier);
      }
  
      @Override
      public void setEventNotifier(UByte eventNotifier) {
          filterChain.setAttribute(this, AttributeId.EventNotifier, eventNotifier);
      }
  
      @Override
      public synchronized Object getAttribute(AttributeId attributeId) {
          if (attributeId == AttributeId.EventNotifier) {
              return eventNotifier;
          } else {
              return super.getAttribute(attributeId);
          }
      }
  
      @Override
      public synchronized void setAttribute(AttributeId attributeId, Object value) {
          if (attributeId == AttributeId.EventNotifier) {
              eventNotifier = (UByte) value;
              fireAttributeChanged(attributeId, value);
          } else {
              super.setAttribute(attributeId, value);
          }
      }
  
      @Nullable
      public UaMethodNode findMethodNode(NodeId methodId) {
          List<UaMethodNode> methodNodes = getMethodNodes();
  
          for (UaMethodNode methodNode : methodNodes) {
              if (methodId.equals(methodNode.getNodeId())) {
                  return methodNode;
              }
  
              NodeId typeDefinitionId = getTypeDefinitionNode().getNodeId();
  
              NodeId methodDeclarationId = findMethodDeclarationId(
                  typeDefinitionId,
                  methodNode.getBrowseName()
              );
  
              if (methodId.equals(methodDeclarationId)) {
                  return methodNode;
              }
          }
  
          return null;
      }
  
      /**
       * Find the NodeId of the MethodNode on the type definition with the same name as {@code methodName}.
       *
       * @param methodName the name of the MethodNode to search for.
       * @return the NodeId of the MethodNode on the type definition with the same name as {@code methodName}. Will be
       * {@link NodeId#NULL_VALUE} if not found.
       */
      private NodeId findMethodDeclarationId(NodeId typeDefinitionId, QualifiedName methodName) {
          AddressSpaceManager asm = getNodeContext()
              .getServer()
              .getAddressSpaceManager();
  
          NamespaceTable namespaceTable = getNodeContext()
              .getServer()
              .getNamespaceTable();
  
          NodeId nodeId = asm.getManagedReferences(typeDefinitionId)
              .stream()
              .filter(HAS_COMPONENT_PREDICATE.or(HAS_ORDERED_COMPONENT_PREDICATE))
              .flatMap(r -> opt2stream(getManagedNode(r.getTargetNodeId())))
              .filter(n ->
                  (n instanceof UaMethodNode) &&
                      Objects.equals(n.getBrowseName(), methodName))
              .findFirst()
              .map(Node::getNodeId)
              .orElse(NodeId.NULL_VALUE);
  
          if (nodeId.isNull()) {
              NodeId parentTypeId = asm.getManagedReferences(typeDefinitionId)
                  .stream()
                  .filter(Reference.SUBTYPE_OF)
                  .flatMap(r -> opt2stream(r.getTargetNodeId().toNodeId(namespaceTable)))
                  .findFirst()
                  .orElse(null);
  
              if (parentTypeId != null) {
                  return findMethodDeclarationId(parentTypeId, methodName);
              } else {
                  return nodeId;
              }
          } else {
              return nodeId;
          }
      }
  
      public List<UaNode> getComponentNodes() {
          return getReferences().stream()
              .filter(HAS_COMPONENT_PREDICATE)
              .flatMap(r -> opt2stream(getManagedNode(r.getTargetNodeId())))
              .collect(Collectors.toList());
      }
  
      public List<UaNode> getPropertyNodes() {
          return getReferences().stream()
              .filter(HAS_PROPERTY_PREDICATE)
              .flatMap(r -> opt2stream(getManagedNode(r.getTargetNodeId())))
              .collect(Collectors.toList());
      }
  
      public List<UaMethodNode> getMethodNodes() {
          return getReferences().stream()
              .filter(HAS_COMPONENT_PREDICATE.or(HAS_ORDERED_COMPONENT_PREDICATE))
              .flatMap(r -> opt2stream(getManagedNode(r.getTargetNodeId())))
              .filter(n -> (n instanceof UaMethodNode))
              .map(UaMethodNode.class::cast)
              .collect(Collectors.toList());
      }
  
      public ObjectTypeNode getTypeDefinitionNode() {
          Node node = getReferences().stream()
              .filter(HAS_TYPE_DEFINITION_PREDICATE)
              .findFirst()
              .flatMap(r -> getManagedNode(r.getTargetNodeId()))
              .orElse(null);
  
          return (node instanceof ObjectTypeNode) ? (ObjectTypeNode) node : null;
      }
  
      public List<Node> getEventSourceNodes() {
          return getReferences().stream()
              .filter(HAS_EVENT_SOURCE_PREDICATE)
              .flatMap(r -> opt2stream(getManagedNode(r.getTargetNodeId())))
              .collect(Collectors.toList());
      }
  
      public List<Node> getNotifierNodes() {
          return getReferences().stream()
              .filter(HAS_NOTIFIER_PREDICATE)
              .flatMap(r -> opt2stream(getManagedNode(r.getTargetNodeId())))
              .collect(Collectors.toList());
      }
  
      public List<Node> getOrganizesNodes() {
          return getReferences().stream()
              .filter(ORGANIZES_PREDICATE)
              .flatMap(r -> opt2stream(getManagedNode(r.getTargetNodeId())))
              .collect(Collectors.toList());
      }
  
      /**
       * Add a 'HasComponent' reference from this Object to {@code node} and an inverse 'ComponentOf' reference from
       * {@code node} back to this Object.
       *
       * @param node the node to add as a component of this Object.
       */
      public void addComponent(UaNode node) {
          addReference(new Reference(
              getNodeId(),
              Identifiers.HasComponent,
              node.getNodeId().expanded(),
              true
          ));
      }
  
      /**
       * Remove the 'HasComponent' reference from this Object to {@code node} and the inverse 'ComponentOf' reference
       * from {@code node} back to this Object.
       *
       * @param node the node to remove as a component of this Object.
       */
      public void removeComponent(UaNode node) {
          removeReference(new Reference(
              getNodeId(),
              Identifiers.HasComponent,
              node.getNodeId().expanded(),
              true
          ));
      }
  
      /**
       * Get the value of the NodeVersion Property, if it exists.
       *
       * @return the value of the NodeVersion Property, if it exists.
       * @see ObjectNodeProperties#NodeVersion
       */
      @Nullable
      public String getNodeVersion() {
          return getProperty(ObjectNodeProperties.NodeVersion).orElse(null);
      }
  
      /**
       * Get the value of the Icon Property, if it exists.
       *
       * @return the value of the Icon Property, if it exists.
       * @see ObjectNodeProperties#Icon
       */
      @Nullable
      public ByteString getIcon() {
          return getProperty(ObjectNodeProperties.Icon).orElse(null);
      }
  
      /**
       * Get the value of the NamingRule Property, if it exists.
       *
       * @return the value of the NamingRule Property, if it exists.
       * @see ObjectNodeProperties#NamingRule
       */
      @Nullable
      public NamingRuleType getNamingRule() {
          return getProperty(ObjectNodeProperties.NamingRule).orElse(null);
      }
  
      /**
       * Set the value of the NodeVersion Property.
       * <p>
       * A PropertyNode will be created if it does not already exist.
       *
       * @param nodeVersion the value to set.
       * @see ObjectNodeProperties#NodeVersion
       */
      public void setNodeVersion(String nodeVersion) {
          setProperty(ObjectNodeProperties.NodeVersion, nodeVersion);
      }
  
      /**
       * Set the value of the Icon Property.
       * <p>
       * A PropertyNode will be created if it does not already exist.
       *
       * @param icon the value to set.
       * @see ObjectNodeProperties#Icon
       */
      public void setIcon(ByteString icon) {
          setProperty(ObjectNodeProperties.Icon, icon);
      }
  
      /**
       * Set the value of the NamingRule Property.
       * <p>
       * A PropertyNode will be created if it does not already exist.
       *
       * @param namingRule the value to set.
       * @see ObjectNodeProperties#NamingRule
       */
      public void setNamingRule(NamingRuleType namingRule) {
          setProperty(ObjectNodeProperties.NamingRule, namingRule);
      }
  
      /**
       * @deprecated use {@link UaObjectNodeBuilder#UaObjectNodeBuilder(UaNodeContext)} or
       * {@link #build(UaNodeContext, Function)}.
       */
      @Deprecated
      public static UaObjectNodeBuilder builder(UaNodeContext context) {
          return new UaObjectNodeBuilder(context);
      }
  
      /**
       * Build a {@link UaObjectNode} using the {@link UaObjectNodeBuilder} supplied to the
       * {@code build} function.
       *
       * @param context a {@link UaNodeContext}.
       * @param build   a function that accepts a {@link UaObjectNodeBuilder} and uses it to build
       *                and return a {@link UaObjectNode}.
       * @return a {@link UaObjectNode} built using the supplied {@link UaObjectNodeBuilder}.
       */
      public static UaObjectNode build(
          UaNodeContext context,
          Function<UaObjectNodeBuilder, UaObjectNode> build
      ) {
  
          UaObjectNodeBuilder builder = new UaObjectNodeBuilder(context);
  
          return build.apply(builder);
      }
  
      public static class UaObjectNodeBuilder implements Supplier<UaObjectNode> {
  
          private final List<AttributeFilter> attributeFilters = new ArrayList<>();
  
          private final List<Reference> references = new ArrayList<>();
  
          private NodeId nodeId;
          private QualifiedName browseName;
          private LocalizedText displayName;
          private LocalizedText description = LocalizedText.NULL_VALUE;
          private UInteger writeMask = UInteger.MIN;
          private UInteger userWriteMask = UInteger.MIN;
          private UByte eventNotifier = ubyte(0);
  
          private final UaNodeContext context;
  
          public UaObjectNodeBuilder(UaNodeContext context) {
              this.context = context;
          }
  
          /**
           * @see #build()
           */
          @Override
          public UaObjectNode get() {
              return build();
          }
  
          /**
           * Build and return the {@link UaObjectNode}.
           * <p>
           * The following fields are required: NodeId, BrowseName, DisplayName.
           *
           * @return a {@link UaObjectNode}.
           * @throws NullPointerException  if any of the required fields are null.
           * @throws IllegalStateException if exactly one HasTypeDefinition reference is not present.
           */
          public UaObjectNode build() {
              Preconditions.checkNotNull(nodeId, "NodeId cannot be null");
              Preconditions.checkNotNull(browseName, "BrowseName cannot be null");
              Preconditions.checkNotNull(displayName, "DisplayName cannot be null");
  
              long hasTypeDefinitionCount = references.stream()
                  .filter(r -> Identifiers.HasTypeDefinition.equals(r.getReferenceTypeId())).count();
  
              if (hasTypeDefinitionCount == 0) {
                  setTypeDefinition(Identifiers.BaseObjectType);
              } else {
                  Preconditions.checkState(
                      hasTypeDefinitionCount == 1,
                      "Object Node must have exactly one HasTypeDefinition reference.");
              }
  
              UaObjectNode node = new UaObjectNode(
                  context,
                  nodeId,
                  browseName,
                  displayName,
                  description,
                  writeMask,
                  userWriteMask,
                  eventNotifier
              );
  
              references.forEach(node::addReference);
  
              node.getFilterChain().addLast(attributeFilters);
  
              return node;
          }
  
          /**
           * Build the {@link UaObjectNode} using the configured values and add it to the
           * {@link NodeManager} from the {@link UaNodeContext}.
           *
           * @return a {@link UaObjectNode} built from the configured values.
           * @see #build()
           */
          public UaObjectNode buildAndAdd() {
              UaObjectNode node = build();
              context.getNodeManager().addNode(node);
              return node;
          }
  
          public UaObjectNodeBuilder setNodeId(NodeId nodeId) {
              this.nodeId = nodeId;
              return this;
          }
  
          public UaObjectNodeBuilder setBrowseName(QualifiedName browseName) {
              this.browseName = browseName;
              return this;
          }
  
          public UaObjectNodeBuilder setDisplayName(LocalizedText displayName) {
              this.displayName = displayName;
              return this;
          }
  
          public UaObjectNodeBuilder setDescription(LocalizedText description) {
              this.description = description;
              return this;
          }
  
          public UaObjectNodeBuilder setWriteMask(UInteger writeMask) {
              this.writeMask = writeMask;
              return this;
          }
  
          public UaObjectNodeBuilder setUserWriteMask(UInteger userWriteMask) {
              this.userWriteMask = userWriteMask;
              return this;
          }
  
          public UaObjectNodeBuilder setEventNotifier(UByte eventNotifier) {
              this.eventNotifier = eventNotifier;
              return this;
          }
  
          public NodeId getNodeId() {
              return nodeId;
          }
  
          public QualifiedName getBrowseName() {
              return browseName;
          }
  
          public LocalizedText getDisplayName() {
              return displayName;
          }
  
          public LocalizedText getDescription() {
              return description;
          }
  
          public UInteger getWriteMask() {
              return writeMask;
          }
  
          public UInteger getUserWriteMask() {
              return userWriteMask;
          }
  
          public UByte getEventNotifier() {
              return eventNotifier;
          }
  
          /**
           * Add an {@link AttributeFilter} that will be added to the node's
           * {@link AttributeFilterChain} when it's built.
           * <p>
           * The order filters are added in this builder is maintained.
           *
           * @param attributeFilter the {@link AttributeFilter} to add.
           * @return this {@link UaObjectNodeBuilder}.
           */
          public UaObjectNodeBuilder addAttributeFilter(AttributeFilter attributeFilter) {
              attributeFilters.add(attributeFilter);
              return this;
          }
  
          /**
           * Add a {@link Reference} to the node when it's built.
           *
           * @param reference the {@link Reference} to add.
           * @return this {@link UaObjectNodeBuilder}.
           */
          public UaObjectNodeBuilder addReference(Reference reference) {
              references.add(reference);
              return this;
          }
  
          /**
           * Convenience method for adding the required HasTypeDefinition reference.
           * <p>
           * {@link #setNodeId(NodeId)} must have already been called before invoking this method.
           *
           * @param typeDefinition The {@link NodeId} of the TypeDefinition.
           * @return this {@link UaObjectNodeBuilder}.
           */
          public UaObjectNodeBuilder setTypeDefinition(NodeId typeDefinition) {
              Objects.requireNonNull(nodeId, "NodeId cannot be null");
  
              references.add(new Reference(
                  nodeId,
                  Identifiers.HasTypeDefinition,
                  typeDefinition.expanded(),
                  true
              ));
  
              return this;
          }
  
      }
  
  }
  