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.ObjectTypeNode;
  import org.eclipse.milo.opcua.sdk.core.nodes.ObjectTypeNodeProperties;
  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.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.UInteger;
  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_ORDERED_COMPONENT_PREDICATE;
  import static org.eclipse.milo.opcua.sdk.core.util.StreamUtil.opt2stream;
  
  public class UaObjectTypeNode extends UaNode implements ObjectTypeNode {
  
      private Boolean isAbstract;
  
      public UaObjectTypeNode(
          UaNodeContext context,
          NodeId nodeId,
          QualifiedName browseName,
          LocalizedText displayName,
          LocalizedText description,
          UInteger writeMask,
          UInteger userWriteMask,
          Boolean isAbstract) {
  
          super(context, nodeId, NodeClass.ObjectType,
              browseName, displayName, description, writeMask, userWriteMask);
  
          this.isAbstract = isAbstract;
      }
  
      @Override
      public Boolean getIsAbstract() {
          return (Boolean) filterChain.getAttribute(this, AttributeId.IsAbstract);
      }
  
      @Override
      public void setIsAbstract(Boolean isAbstract) {
          filterChain.setAttribute(this, AttributeId.IsAbstract, isAbstract);
      }
  
      @Override
      public synchronized Object getAttribute(AttributeId attributeId) {
          if (attributeId == AttributeId.IsAbstract) {
              return isAbstract;
          } else {
              return super.getAttribute(attributeId);
          }
      }
  
      @Override
      public synchronized void setAttribute(AttributeId attributeId, Object value) {
          if (attributeId == AttributeId.IsAbstract) {
              isAbstract = (Boolean) value;
              fireAttributeChanged(attributeId, value);
          } else {
              super.setAttribute(attributeId, value);
          }
      }
  
      @Nullable
      public UaMethodNode findMethodNode(NodeId methodId) {
          return getReferences().stream()
              .filter(HAS_COMPONENT_PREDICATE.or(HAS_ORDERED_COMPONENT_PREDICATE))
              .flatMap(r -> opt2stream(getManagedNode(r.getTargetNodeId())))
              .filter(n -> (n instanceof UaMethodNode) && Objects.equals(n.getNodeId(), methodId))
              .map(UaMethodNode.class::cast)
              .findFirst()
              .orElse(null);
      }
  
      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());
      }
  
      /**
       * Add a 'HasComponent' reference from this node to {@code node} and an inverse 'ComponentOf' reference from
       * {@code node} back to this node.
       *
       * @param node the node to add as a component of this node.
       */
      public void addComponent(UaNode node) {
          addReference(new Reference(
              getNodeId(),
              Identifiers.HasComponent,
              node.getNodeId().expanded(),
              true
          ));
  
          node.addReference(new Reference(
              node.getNodeId(),
              Identifiers.HasComponent,
              getNodeId().expanded(),
              false
          ));
      }
  
      /**
       * Add a 'HasSubtype' reference from this node to {@code node} and an inverse 'SubtypeOf' reference from
       * {@code node} back to this node.
       *
       * @param node the node to add as a subtype of this node.
       */
      public void addSubtype(UaObjectTypeNode node) {
          addReference(new Reference(
              getNodeId(),
              Identifiers.HasSubtype,
              node.getNodeId().expanded(),
              true
          ));
  
          node.addReference(new Reference(
              node.getNodeId(),
              Identifiers.HasSubtype,
              getNodeId().expanded(),
              false
          ));
      }
  
      /**
       * Get the value of the NodeVersion Property, if it exists.
       *
       * @return the value of the NodeVersion Property, if it exists.
       * @see ObjectTypeNodeProperties#NodeVersion
       */
      @Nullable
      public String getNodeVersion() {
          return getProperty(ObjectTypeNodeProperties.NodeVersion).orElse(null);
      }
  
      /**
       * Get the value of the Icon Property, if it exists.
       *
       * @return the value of the Icon Property, if it exists.
       * @see ObjectTypeNodeProperties#Icon
       */
      @Nullable
      public ByteString getIcon() {
          return getProperty(ObjectTypeNodeProperties.Icon).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 ObjectTypeNodeProperties#NodeVersion
       */
      public void setNodeVersion(String nodeVersion) {
          setProperty(ObjectTypeNodeProperties.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 ObjectTypeNodeProperties#NodeVersion
       */
      public void setIcon(ByteString icon) {
          setProperty(ObjectTypeNodeProperties.Icon, icon);
      }
  
      public static UaObjectTypeNodeBuilder builder(UaNodeContext context) {
          return new UaObjectTypeNodeBuilder(context);
      }
  
      /**
       * Build a {@link UaObjectTypeNode} using the {@link UaObjectTypeNodeBuilder} supplied to the
       * {@code build} function.
       *
       * @param context a {@link UaNodeContext}.
       * @param build   a function that accepts a {@link UaObjectTypeNodeBuilder} and uses it to build
       *                and return a {@link UaObjectTypeNode}.
       * @return a {@link UaObjectTypeNode} built using the supplied {@link UaObjectTypeNodeBuilder}.
       */
      public static UaObjectTypeNode build(
          UaNodeContext context,
          Function<UaObjectTypeNodeBuilder, UaObjectTypeNode> build
      ) {
  
          UaObjectTypeNodeBuilder builder = new UaObjectTypeNodeBuilder(context);
  
          return build.apply(builder);
      }
  
      public static class UaObjectTypeNodeBuilder implements Supplier<UaObjectTypeNode> {
  
          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 boolean isAbstract = false;
  
          private final UaNodeContext context;
  
          public UaObjectTypeNodeBuilder(UaNodeContext context) {
              this.context = context;
          }
  
          /**
           * @see #build()
           */
          @Override
          public UaObjectTypeNode get() {
              return build();
          }
  
          /**
           * Build and return the {@link UaObjectTypeNode}.
           * <p>
           * The following fields are required: NodeId, BrowseName, DisplayName.
           *
           * @return a {@link UaObjectTypeNode} built from the configuration of this builder.
           */
          public UaObjectTypeNode build() {
              Preconditions.checkNotNull(nodeId, "NodeId cannot be null");
              Preconditions.checkNotNull(browseName, "BrowseName cannot be null");
              Preconditions.checkNotNull(displayName, "DisplayName cannot be null");
  
              UaObjectTypeNode node = new UaObjectTypeNode(
                  context,
                  nodeId,
                  browseName,
                  displayName,
                  description,
                  writeMask,
                  userWriteMask,
                  isAbstract
              );
  
              references.forEach(node::addReference);
  
              node.getFilterChain().addLast(attributeFilters);
  
              return node;
          }
  
          /**
           * Build the {@link UaObjectTypeNode} using the configured values and add it to the
           * {@link NodeManager} from the {@link UaNodeContext}.
           *
           * @return a {@link UaObjectTypeNode} built from the configured values.
           * @see #build()
           */
          public UaObjectTypeNode buildAndAdd() {
              UaObjectTypeNode node = build();
              context.getNodeManager().addNode(node);
              return node;
          }
  
          public UaObjectTypeNodeBuilder setNodeId(NodeId nodeId) {
              this.nodeId = nodeId;
              return this;
          }
  
          public UaObjectTypeNodeBuilder setBrowseName(QualifiedName browseName) {
              this.browseName = browseName;
              return this;
          }
  
          public UaObjectTypeNodeBuilder setDisplayName(LocalizedText displayName) {
              this.displayName = displayName;
              return this;
          }
  
          public UaObjectTypeNodeBuilder setDescription(LocalizedText description) {
              this.description = description;
              return this;
          }
  
          public UaObjectTypeNodeBuilder setWriteMask(UInteger writeMask) {
              this.writeMask = writeMask;
              return this;
          }
  
          public UaObjectTypeNodeBuilder setUserWriteMask(UInteger userWriteMask) {
              this.userWriteMask = userWriteMask;
              return this;
          }
  
          public UaObjectTypeNodeBuilder setIsAbstract(boolean isAbstract) {
              this.isAbstract = isAbstract;
              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 boolean isAbstract() {
              return isAbstract;
          }
  
          /**
           * 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 UaObjectTypeNodeBuilder}.
           */
          public UaObjectTypeNodeBuilder 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 UaObjectTypeNodeBuilder}.
           */
          public UaObjectTypeNodeBuilder addReference(Reference reference) {
              references.add(reference);
              return this;
          }
  
      }
  
  }
  