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.stack.core.types.builtin;
  
  import java.util.Optional;
  import java.util.UUID;
  import java.util.function.Function;
  import javax.xml.bind.DatatypeConverter;
  
  import com.google.common.base.MoreObjects;
  import com.google.common.base.Objects;
  import org.eclipse.milo.opcua.stack.core.NamespaceTable;
  import org.eclipse.milo.opcua.stack.core.StatusCodes;
  import org.eclipse.milo.opcua.stack.core.UaRuntimeException;
  import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
  import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
  import org.eclipse.milo.opcua.stack.core.types.enumerated.IdType;
  import org.eclipse.milo.opcua.stack.core.util.Namespaces;
  import org.jetbrains.annotations.NotNull;
  import org.jetbrains.annotations.Nullable;
  
  import static com.google.common.base.Preconditions.checkNotNull;
  import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
  import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort;
  
  public final class NodeId {
  
      public static final NodeId NULL_NUMERIC = new NodeId(ushort(0), uint(0));
      public static final NodeId NULL_STRING = new NodeId(ushort(0), "");
      public static final NodeId NULL_GUID = new NodeId(ushort(0), new UUID(0, 0));
      public static final NodeId NULL_OPAQUE = new NodeId(ushort(0), ByteString.NULL_VALUE);
  
      public static final NodeId NULL_VALUE = NULL_NUMERIC;
  
      private final UShort namespaceIndex;
      private final Object identifier;
  
      /**
       * @param namespaceIndex the index for a namespace URI. An index of 0 is used for OPC UA defined NodeIds.
       * @param identifier     the identifier for a node in the address space of an OPC UA Server.
       */
      public NodeId(int namespaceIndex, int identifier) {
          this(ushort(namespaceIndex), uint(identifier));
      }
  
      /**
       * @param namespaceIndex the index for a namespace URI. An index of 0 is used for OPC UA defined NodeIds.
       * @param identifier     the identifier for a node in the address space of an OPC UA Server.
       */
      public NodeId(int namespaceIndex, UInteger identifier) {
          this(ushort(namespaceIndex), identifier);
      }
  
      /**
       * @param namespaceIndex the index for a namespace URI. An index of 0 is used for OPC UA defined NodeIds.
       * @param identifier     the identifier for a node in the address space of an OPC UA Server.
       */
      public NodeId(int namespaceIndex, String identifier) {
          this(ushort(namespaceIndex), identifier);
      }
  
      /**
       * @param namespaceIndex the index for a namespace URI. An index of 0 is used for OPC UA defined NodeIds.
       * @param identifier     the identifier for a node in the address space of an OPC UA Server.
       */
      public NodeId(int namespaceIndex, UUID identifier) {
          this(ushort(namespaceIndex), identifier);
      }
  
      /**
       * @param namespaceIndex the index for a namespace URI. An index of 0 is used for OPC UA defined NodeIds.
       * @param identifier     the identifier for a node in the address space of an OPC UA Server.
       */
      public NodeId(int namespaceIndex, ByteString identifier) {
          this(ushort(namespaceIndex), identifier);
      }
  
      /**
       * @param namespaceIndex the index for a namespace URI. An index of 0 is used for OPC UA defined NodeIds.
       * @param identifier     the identifier for a node in the address space of an OPC UA Server.
       */
      public NodeId(UShort namespaceIndex, UInteger identifier) {
          checkNotNull(namespaceIndex);
          checkNotNull(identifier);
  
          this.namespaceIndex = namespaceIndex;
          this.identifier = identifier;
      }
  
      /**
       * @param namespaceIndex the index for a namespace URI. An index of 0 is used for OPC UA defined NodeIds.
       * @param identifier     the identifier for a node in the address space of an OPC UA Server.
       */
      public NodeId(UShort namespaceIndex, int identifier) {
          checkNotNull(namespaceIndex);
  
          this.namespaceIndex = namespaceIndex;
          this.identifier = uint(identifier);
      }
  
      /**
       * @param namespaceIndex the index for a namespace URI. An index of 0 is used for OPC UA defined NodeIds.
       * @param identifier     the identifier for a node in the address space of an OPC UA Server.
       */
      public NodeId(UShort namespaceIndex, String identifier) {
          checkNotNull(namespaceIndex);
  
          if (identifier == null) identifier = "";
  
          this.namespaceIndex = namespaceIndex;
          this.identifier = identifier;
      }
  
      /**
       * @param namespaceIndex the index for a namespace URI. An index of 0 is used for OPC UA defined NodeIds.
       * @param identifier     the identifier for a node in the address space of an OPC UA Server.
       */
      public NodeId(UShort namespaceIndex, UUID identifier) {
          checkNotNull(namespaceIndex);
          checkNotNull(identifier);
  
          this.namespaceIndex = namespaceIndex;
          this.identifier = identifier;
      }
  
      /**
       * @param namespaceIndex the index for a namespace URI. An index of 0 is used for OPC UA defined NodeIds.
       * @param identifier     the identifier for a node in the address space of an OPC UA Server.
       */
      public NodeId(UShort namespaceIndex, ByteString identifier) {
          checkNotNull(namespaceIndex);
          checkNotNull(identifier);
  
          this.namespaceIndex = namespaceIndex;
          this.identifier = identifier;
      }
  
      NodeId(@NotNull UShort namespaceIndex, @NotNull Object identifier) {
          checkNotNull(namespaceIndex);
          checkNotNull(identifier);
  
          this.namespaceIndex = namespaceIndex;
          this.identifier = identifier;
      }
  
      public UShort getNamespaceIndex() {
          return namespaceIndex;
      }
  
      public Object getIdentifier() {
          return identifier;
      }
  
      public IdType getType() {
          if (identifier instanceof UInteger) {
              return IdType.Numeric;
          } else if (identifier instanceof String) {
              return IdType.String;
          } else if (identifier instanceof UUID) {
              return IdType.Guid;
          } else {
              return IdType.Opaque;
          }
      }
  
      /**
       * Convert this NodeId to a relative {@link ExpandedNodeId}.
       *
       * @return a relative {@link ExpandedNodeId}.
       * @see ExpandedNodeId#isRelative()
       */
      public ExpandedNodeId expanded() {
          return new ExpandedNodeId(namespaceIndex, null, identifier, UInteger.MIN);
      }
  
      /**
       * Convert this NodeId to an absolute {@link ExpandedNodeId}, using {@code namespaceTable} to convert the
       * namespace index to a namespace URI.
       *
       * @param namespaceTable a {@link NamespaceTable} to look up the namespace URI in.
       * @return an absolute {@link ExpandedNodeId}.
       * @see ExpandedNodeId#isAbsolute()
       */
      public ExpandedNodeId expanded(NamespaceTable namespaceTable) {
          String namespaceUri = namespaceTable.getUri(namespaceIndex);
          return new ExpandedNodeId(namespaceIndex, namespaceUri, identifier, UInteger.MIN);
      }
  
      public boolean isNull() {
          return namespaceIndex.intValue() == 0 &&
              (NULL_NUMERIC.equals(this) ||
                  NULL_STRING.equals(this) ||
                  NULL_GUID.equals(this) ||
                  NULL_OPAQUE.equals(this));
      }
  
      public boolean isNotNull() {
          return !isNull();
      }
  
      /**
       * Re-index this {@link NodeId} from its current namespace index to the index for {@code namespaceUri}.
       * <p>
       * If the target namespace URI is not present in the namespace table this {@link NodeId} is returned.
       *
       * @param namespaceTable the {@link NamespaceTable}.
       * @param namespaceUri   the target namespace URI.
       * @return a new {@link NodeId} in the namespace index indicated by {@code namespaceUri}.
       */
      public NodeId reindex(NamespaceTable namespaceTable, String namespaceUri) {
          UShort namespaceIndex = namespaceTable.getIndex(namespaceUri);
  
          if (namespaceIndex != null && !namespaceIndex.equals(getNamespaceIndex())) {
              return withNamespaceIndex(namespaceIndex);
          } else {
              return this;
          }
      }
  
      /**
       * Return a new {@link NodeId} with {@code id}.
       *
       * @param id the new identifier.
       * @return a new {@link NodeId} with {@code id}.
       */
      public NodeId withId(UInteger id) {
          return new NodeId(namespaceIndex, id);
      }
  
      /**
       * Return a new {@link NodeId} with {@code id}.
       *
       * @param id the new identifier.
       * @return a new {@link NodeId} with {@code id}.
       */
      public NodeId withId(String id) {
          return new NodeId(namespaceIndex, id);
      }
  
      /**
       * Return a new {@link NodeId} with {@code id}.
       *
       * @param id the new identifier.
       * @return a new {@link NodeId} with {@code id}.
       */
      public NodeId withId(UUID id) {
          return new NodeId(namespaceIndex, id);
      }
  
      /**
       * Return a new {@link NodeId} with {@code id}.
       *
       * @param id the new identifier.
       * @return a new {@link NodeId} with {@code id}.
       */
      public NodeId withId(ByteString id) {
          return new NodeId(namespaceIndex, id);
      }
  
      /**
       * Return a new {@link NodeId} with {@code namespaceIndex}.
       *
       * @param namespaceIndex the new namespace index.
       * @return a new {@link NodeId} with {@code namespaceIndex}.
       */
      public NodeId withNamespaceIndex(UShort namespaceIndex) {
          return new NodeId(namespaceIndex, this.identifier);
      }
  
      /**
       * Check if this {@link NodeId} is equal to {@code xni}.
       * <p>
       * To be considered equal {@code xni} must be in serverIndex == 0, have a namespace index that is equal this
       * namespace index, or have a namespace URI at the same index in the default namespace table, and an equal
       * identifier.
       *
       * @param xni the {@link ExpandedNodeId} to check equality against.
       * @return {@code true} if this {@link NodeId} is equal to {@code xni}.
       * @deprecated use {@link #equalTo(ExpandedNodeId)} instead.
       */
      @Deprecated
      public boolean equals(ExpandedNodeId xni) {
          return equalTo(xni);
      }
  
      /**
       * Check if this {@link NodeId} is equal to {@code xni}.
       * <p>
       * To be considered equal {@code xni} must be in serverIndex == 0, have a namespace index that is equal this
       * namespace index, or have a namespace URI at the same index in the default namespace table, and an equal
       * identifier.
       *
       * @param xni            the {@link ExpandedNodeId} to check equality against.
       * @param namespaceTable the {@link NamespaceTable} used to look up the index of a namespace URI.
       * @return {@code true} if this {@link NodeId} is equal to {@code xni}.
       * @deprecated use {@link #equalTo(ExpandedNodeId, NamespaceTable)} instead.
       */
      @Deprecated
      public boolean equals(ExpandedNodeId xni, NamespaceTable namespaceTable) {
          return equalTo(xni, namespaceTable);
      }
  
      /**
       * Check if this {@link NodeId} is equal to {@code xni}.
       * <p>
       * To be considered equal {@code xni} must be in serverIndex == 0, have a namespace index that is equal this
       * namespace index, or have a namespace URI at the same index in the default namespace table, and an equal
       * identifier.
       *
       * @param xni the {@link ExpandedNodeId} to check equality against.
       * @return {@code true} if this {@link NodeId} is equal to {@code xni}.
       */
      public boolean equalTo(ExpandedNodeId xni) {
          return equalTo(xni, uri -> {
              if (Namespaces.OPC_UA.equals(uri)) {
                  return UShort.MIN;
              } else {
                  return null;
              }
          });
      }
  
      /**
       * Check if this {@link NodeId} is equal to {@code xni}.
       * <p>
       * To be considered equal {@code xni} must be in serverIndex == 0, have a namespace index that is equal this
       * namespace index, or have a namespace URI at the same index in the default namespace table, and an equal
       * identifier.
       *
       * @param xni            the {@link ExpandedNodeId} to check equality against.
       * @param namespaceTable the {@link NamespaceTable} used to look up the index of a namespace URI.
       * @return {@code true} if this {@link NodeId} is equal to {@code xni}.
       */
      public boolean equalTo(ExpandedNodeId xni, NamespaceTable namespaceTable) {
          return equalTo(xni, namespaceTable::getIndex);
      }
  
      private boolean equalTo(ExpandedNodeId xni, Function<String, UShort> getIndex) {
          if (!xni.isLocal()) {
              return false;
          }
  
          UShort otherNamespaceIndex = xni.isAbsolute() ?
              getIndex.apply(xni.getNamespaceUri()) :
              xni.getNamespaceIndex();
  
          return Objects.equal(namespaceIndex, otherNamespaceIndex) && Objects.equal(identifier, xni.getIdentifier());
      }
  
      @Override
      public boolean equals(Object o) {
          if (this == o) return true;
          if (o == null || getClass() != o.getClass()) return false;
  
          NodeId nodeId = (NodeId) o;
  
          return identifier.equals(nodeId.identifier) &&
              namespaceIndex.equals(nodeId.namespaceIndex);
      }
  
      @Override
      public int hashCode() {
          int result = namespaceIndex.hashCode();
          result = 31 * result + identifier.hashCode();
          return result;
      }
  
      @Override
      public String toString() {
          return MoreObjects.toStringHelper(this)
              .add("ns", namespaceIndex)
              .add("id", identifier)
              .toString();
      }
  
      public String toParseableString() {
          StringBuilder sb = new StringBuilder();
  
          sb.append("ns=").append(namespaceIndex).append(";");
  
          switch (getType()) {
              case Numeric:
                  sb.append("i=").append(identifier);
                  break;
              case String:
                  sb.append("s=").append(identifier);
                  break;
              case Guid:
                  sb.append("g=").append(identifier);
                  break;
              case Opaque:
                  ByteString bs = (ByteString) identifier;
                  if (bs.isNull()) sb.append("b=");
                  else sb.append("b=").append(DatatypeConverter.printBase64Binary(bs.bytes()));
                  break;
  
              default:
                  throw new IllegalStateException("unknown IdType: " + getType());
          }
  
          return sb.toString();
      }
  
      public static NodeId parse(@NotNull String s) throws UaRuntimeException {
          if (s.startsWith("ns=")) {
              int index = s.indexOf(";");
  
              if (index == -1) {
                  throw new UaRuntimeException(StatusCodes.Bad_NodeIdInvalid);
              }
  
              try {
                  int namespaceIndex = Integer.parseInt(s.substring(3, index));
  
                  return parse(s.substring(index + 1), namespaceIndex);
              } catch (NumberFormatException e) {
                  throw new UaRuntimeException(StatusCodes.Bad_NodeIdInvalid, e);
              }
          } else {
              return parse(s, 0);
          }
      }
  
      private static NodeId parse(String s, int namespaceIndex) throws UaRuntimeException {
          if (s.length() < 2) {
              throw new UaRuntimeException(StatusCodes.Bad_NodeIdInvalid);
          }
  
          String type = s.substring(0, 2);
          String id = s.substring(2);
  
          switch (type) {
              case "i=":
                  try {
                      return new NodeId(namespaceIndex, uint(Long.parseLong(id)));
                  } catch (NumberFormatException e) {
                      throw new UaRuntimeException(StatusCodes.Bad_NodeIdInvalid, e);
                  }
              case "s=":
                  return new NodeId(namespaceIndex, id);
              case "g=":
                  try {
                      return new NodeId(namespaceIndex, UUID.fromString(id));
                  } catch (IllegalArgumentException e) {
                      throw new UaRuntimeException(StatusCodes.Bad_NodeIdInvalid, e);
                  }
              case "b=":
                  try {
                      return new NodeId(namespaceIndex, ByteString.of(DatatypeConverter.parseBase64Binary(id)));
                  } catch (IllegalArgumentException e) {
                      throw new UaRuntimeException(StatusCodes.Bad_NodeIdInvalid, e);
                  }
              default:
                  throw new UaRuntimeException(StatusCodes.Bad_NodeIdInvalid);
          }
      }
  
      @Nullable
      public static NodeId parseOrNull(@NotNull String s) {
          try {
              return NodeId.parse(s);
          } catch (UaRuntimeException t) {
              return null;
          }
      }
  
      public static Optional<NodeId> parseSafe(@NotNull String s) {
          return Optional.ofNullable(parseOrNull(s));
      }
  
  }
  