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.core;
  
  import java.util.Map;
  import java.util.Objects;
  
  import com.google.common.collect.Maps;
  import org.eclipse.milo.opcua.stack.core.BuiltinDataType;
  import org.eclipse.milo.opcua.stack.core.Identifiers;
  import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject;
  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.builtin.unsigned.UNumber;
  import org.eclipse.milo.opcua.stack.core.util.Tree;
  import org.jetbrains.annotations.Nullable;
  
  /**
   * A tree-based representation of a DataType hierarchy.
   * <p>
   * Allows for convenient operations such as:
   * <ul>
   *     <li>resolving the backing Class for a value of a given DataType</li>
   *     <li>reducing a DataType to its inherited builtin type</li>
   *     <li>checking if a Class would be assignable to a value of some DataType</li>
   *     <li>retrieving the encoding ids for structured DataTypes</li>
   * </ul>
   */
  public class DataTypeTree {
  
      private final Map<NodeId, Tree<DataType>> dataTypes = Maps.newConcurrentMap();
  
      private final Tree<DataType> tree;
  
      public DataTypeTree(Tree<DataType> tree) {
          this.tree = tree;
  
          tree.traverseNodes(
              treeNode ->
                  dataTypes.put(treeNode.getValue().getNodeId(), treeNode)
          );
      }
  
      /**
       * Get the backing Class a value of DataType {@code dataTypeId} would have.
       * <p>
       * Builtin DataTypes are backed by their intrinsic backing class.
       * <p>
       * Abstract types {@link Identifiers#Number}, {@link Identifiers#Integer}, and {@link Identifiers#UInteger}
       * are backed by {@link Number}, {@link Integer}, and {@link UInteger} respectively.
       * <p>
       * Enumerations are backed by {@link Integer}.
       * <p>
       * Structures are backed by {@link ExtensionObject}.
       *
       * @param dataTypeId the {@link NodeId} of a DataType Node.
       * @return the backing Class a value of DataType {@code dataTypeId} would have.
       * @see BuiltinDataType
       * @see BuiltinDataType#getBackingClass()
       */
      public Class<?> getBackingClass(NodeId dataTypeId) {
          if (BuiltinDataType.isBuiltin(dataTypeId)) {
              return BuiltinDataType.getBackingClass(dataTypeId);
          } else {
              if (Identifiers.Enumeration.equals(dataTypeId)) {
                  return Integer.class;
              } else if (Identifiers.Number.equals(dataTypeId)) {
                  return Number.class;
              } else if (Identifiers.Integer.equals(dataTypeId)) {
                  return Number.class;
              } else if (Identifiers.UInteger.equals(dataTypeId)) {
                  return UNumber.class;
              } else {
                  Tree<DataType> node = dataTypes.get(dataTypeId);
                  Tree<DataType> parent = node != null ? node.getParent() : null;
  
                  if (parent != null) {
                      return getBackingClass(parent.getValue().getNodeId());
                  } else {
                      return Object.class;
                  }
              }
          }
      }
  
      /**
       * Get the {@link BuiltinDataType} {@code dataTypeId} inherits from, following references to the parent
       * as necessary until a {@link BuiltinDataType} is found.
       *
       * @param dataTypeId the {@link NodeId} of a DataType Node.
       * @return the {@link BuiltinDataType} this DataType inherits from.
       */
      public BuiltinDataType getBuiltinType(NodeId dataTypeId) {
          if (BuiltinDataType.isBuiltin(dataTypeId)) {
              return BuiltinDataType.fromNodeId(dataTypeId);
          } else {
              Tree<DataType> node = dataTypes.get(dataTypeId);
              Tree<DataType> parent = node != null ? node.getParent() : null;
  
              if (parent != null) {
                  return getBuiltinType(parent.getValue().getNodeId());
              } else {
                  return BuiltinDataType.Variant;
              }
          }
      }
  
      /**
       * Get the {@link DataType} info for the DataType identified by {@code dataTypeId}, if it exists.
       *
       * @param dataTypeId the {@link NodeId} of a DataType Node.
       * @return the {@link DataType} info for the DataType identified by {@code dataTypeId}, if it exists.
       */
      @Nullable
      public DataType getDataType(NodeId dataTypeId) {
          Tree<DataType> node = dataTypes.get(dataTypeId);
  
          return node != null ? node.getValue() : null;
      }
  
      /**
       * Get the {@link NodeId} of the Binary Encoding Node for the DataType identified by {@code dataTypeId}.
       *
       * @param dataTypeId the {@link NodeId} of a DataType Node.
       * @return the {@link NodeId} of the Binary Encoding Node, or {@code null} if none exists.
       */
      @Nullable
      public NodeId getBinaryEncodingId(NodeId dataTypeId) {
          DataType dataType = getDataType(dataTypeId);
  
          return dataType != null ? dataType.getBinaryEncodingId() : null;
      }
  
      /**
       * Get the {@link NodeId} of the XML Encoding Node for the DataType identified by {@code dataTypeId}.
       *
       * @param dataTypeId the {@link NodeId} of a DataType Node.
       * @return the {@link NodeId} of the XML Encoding Node, or {@code null} if none exists.
       */
      @Nullable
      public NodeId getXmlEncodingId(NodeId dataTypeId) {
          DataType dataType = getDataType(dataTypeId);
  
          return dataType != null ? dataType.getXmlEncodingId() : null;
      }
  
      /**
       * Check if a value of type {@code clazz} is assignable to a value of DataType {@code dataTypeId}, i.e. it is
       * equal to or a subtype of the backing class for {@code dataTypeId}.
       *
       * @param dataTypeId the {@link NodeId} of a DataType Node.
       * @param clazz      the backing Class to check.
       * @return {@code true} if {@code clazz} is equal to or a subtype of the backing class for {@code dataTypeId}.
       */
      public boolean isAssignable(NodeId dataTypeId, Class<?> clazz) {
          Class<?> backingClass = getBackingClass(dataTypeId);
  
          if (Identifiers.Integer.equals(dataTypeId)) {
              // Can't just check that it's assignable from Number.class because
              // UNumber extends Number rather than the two sharing a common
              // superclass.
              return clazz == byte.class || clazz == Byte.class ||
                  clazz == short.class || clazz == Short.class ||
                  clazz == int.class || clazz == Integer.class ||
                  clazz == long.class || clazz == Long.class;
          } else if (Identifiers.UInteger.equals(dataTypeId)) {
              return UNumber.class.isAssignableFrom(clazz);
          } else {
              return backingClass.isAssignableFrom(clazz);
          }
      }
  
      /**
       * Get the underlying {@link Tree} structure.
       *
       * @return the underlying {@link Tree} structure.
       */
      public Tree<DataType> getTree() {
          return tree;
      }
  
      /**
       * Get the underlying {@link Tree} node for the DataType identified by {@code dataTypeId}.
       *
       * @param dataTypeId the {@link NodeId} of a DataType Node.
       * @return the underlying {@link Tree} node for the DataType identified by {@code dataTypeId}.
       */
      @Nullable
      public Tree<DataType> getTreeNode(NodeId dataTypeId) {
          return dataTypes.get(dataTypeId);
      }
  
      /**
       * Data object that holds details of a DataType:
       * <ul>
       *     <li>Browse Name of the DataType Node</li>
       *     <li>NodeId of the DataType Node</li>
       *     <li>NodeId of the Binary Encoding Node</li>
       *     <li>NodeId of the XML Encoding Node</li>
       * </ul>
       */
      public static class DataType {
  
          private final QualifiedName browseName;
          private final NodeId nodeId;
          private final NodeId binaryEncodingId;
          private final NodeId xmlEncodingId;
  
          public DataType(QualifiedName browseName, NodeId nodeId, NodeId binaryEncodingId, NodeId xmlEncodingId) {
              this.browseName = browseName;
              this.nodeId = nodeId;
              this.binaryEncodingId = binaryEncodingId;
              this.xmlEncodingId = xmlEncodingId;
          }
  
          /**
           * Get the Browse Name of this DataType.
           *
           * @return the Browse Name of this DataType.
           */
          public QualifiedName getBrowseName() {
              return browseName;
          }
  
          /**
           * Get the {@link NodeId} of this DataType.
           *
           * @return the {@link NodeId} of this DataType.
           */
          public NodeId getNodeId() {
              return nodeId;
          }
  
          /**
           * Get the {@link NodeId} of the Binary Encoding Node for this DataType, if it exists.
           * <p>
           * Only Structured DataTypes have encoding ids.
           *
           * @return the NodeId of the Binary Encoding Node for this DataType, if it exists.
           */
          @Nullable
          public NodeId getBinaryEncodingId() {
              return binaryEncodingId;
          }
  
          /**
           * Get the {@link NodeId} of the XML Encoding Node for this DataType, if it exists.
           * <p>
           * Only Structured DataTypes have encoding ids.
           *
           * @return the NodeId of the XML Encoding Node for this DataType, if it exists.
           */
          @Nullable
          public NodeId getXmlEncodingId() {
              return xmlEncodingId;
          }
  
          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (o == null || getClass() != o.getClass()) return false;
              DataType dataType = (DataType) o;
              return browseName.equals(dataType.browseName) &&
                  nodeId.equals(dataType.nodeId) &&
                  Objects.equals(binaryEncodingId, dataType.binaryEncodingId) &&
                  Objects.equals(xmlEncodingId, dataType.xmlEncodingId);
          }
  
          @Override
          public int hashCode() {
              return Objects.hash(browseName, nodeId, binaryEncodingId, xmlEncodingId);
          }
  
          @Override
          public String toString() {
              return "DataType{" +
                  "browseName=" + browseName +
                  ", nodeId=" + nodeId +
                  ", binaryEncodingId=" + binaryEncodingId +
                  ", xmlEncodingId=" + xmlEncodingId +
                  '}';
          }
  
      }
  
  }
  