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.stack.core.types.builtin;
  
  import java.util.Objects;
  import java.util.Optional;
  import java.util.UUID;
  import javax.xml.bind.DatatypeConverter;
  
  import com.google.common.base.MoreObjects;
  import com.google.common.base.MoreObjects.ToStringHelper;
  import com.google.common.base.Preconditions;
  import com.google.common.base.Strings;
  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.jetbrains.annotations.NotNull;
  import org.jetbrains.annotations.Nullable;
  
  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 ExpandedNodeId {
  
      public static final ExpandedNodeId NULL_VALUE = new ExpandedNodeId(
          UShort.MIN,
          null,
          UInteger.MIN,
          UInteger.MIN
      );
  
      private final UShort namespaceIndex;
      private final Object identifier;
      private final String namespaceUri;
      private final UInteger serverIndex;
  
      public ExpandedNodeId(
          UShort namespaceIndex,
          String namespaceUri,
          UInteger identifier
      ) {
  
          this(namespaceIndex, namespaceUri, identifier, UInteger.MIN);
      }
  
      public ExpandedNodeId(
          UShort namespaceIndex,
          String namespaceUri,
          UInteger identifier,
          UInteger serverIndex
      ) {
  
          this(namespaceIndex, namespaceUri, (Object) identifier, serverIndex);
      }
  
      public ExpandedNodeId(
          UShort namespaceIndex,
          String namespaceUri,
          String identifier
      ) {
  
          this(namespaceIndex, namespaceUri, identifier, UInteger.MIN);
      }
  
      public ExpandedNodeId(
          UShort namespaceIndex,
          String namespaceUri,
          String identifier,
          UInteger serverIndex
      ) {
  
          this(namespaceIndex, namespaceUri, (Object) identifier, serverIndex);
      }
  
      public ExpandedNodeId(
          UShort namespaceIndex,
          String namespaceUri,
          ByteString identifier
      ) {
  
          this(namespaceIndex, namespaceUri, identifier, UInteger.MIN);
      }
  
      public ExpandedNodeId(
          UShort namespaceIndex,
          String namespaceUri,
          ByteString identifier,
          UInteger serverIndex
      ) {
  
          this(namespaceIndex, namespaceUri, (Object) identifier, serverIndex);
      }
  
      public ExpandedNodeId(
          UShort namespaceIndex,
          String namespaceUri,
          UUID identifier
      ) {
  
          this(namespaceIndex, namespaceUri, identifier, UInteger.MIN);
      }
  
      public ExpandedNodeId(
          UShort namespaceIndex,
          String namespaceUri,
          UUID identifier,
          UInteger serverIndex
      ) {
  
          this(namespaceIndex, namespaceUri, (Object) identifier, serverIndex);
      }
  
      public ExpandedNodeId(
          @NotNull UShort namespaceIndex,
          @Nullable String namespaceUri,
          @Nullable Object identifier,
          @NotNull UInteger serverIndex
      ) {
  
          Preconditions.checkNotNull(namespaceIndex);
          Preconditions.checkNotNull(serverIndex);
  
          this.namespaceIndex = namespaceIndex;
          this.namespaceUri = namespaceUri;
          this.identifier = identifier;
          this.serverIndex = serverIndex;
      }
  
      /**
       * Get the identifier object of this {@link ExpandedNodeId}.
       *
       * @return the identifier object of this {@link ExpandedNodeId}.
       */
      public Object getIdentifier() {
          return identifier;
      }
  
      /**
       * Get the {@link IdType} of this {@link ExpandedNodeId}.
       *
       * @return the {@link IdType} of this {@link ExpandedNodeId}.
       */
      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;
          }
      }
  
      /**
       * Get the namespace index of this {@link ExpandedNodeId}.
       * <p>
       * This value shall be ignored if {@link #getNamespaceUri()} is non-null.
       *
       * @return the namespace index of this {@link ExpandedNodeId}.
       */
      public UShort getNamespaceIndex() {
          return namespaceIndex;
      }
  
      /**
       * Get the namespace URI of this {@link ExpandedNodeId}.
       *
       * @return the namespace URI of this {@link ExpandedNodeId}.
       */
      @Nullable
      public String getNamespaceUri() {
          return namespaceUri;
      }
  
      /**
       * Get the server index of this {@link ExpandedNodeId}.
       *
       * @return the server index of this {@link ExpandedNodeId}.
       */
      public UInteger getServerIndex() {
          return serverIndex;
      }
  
      /**
       * @return {@code true} if this {@link ExpandedNodeId} is absolute, i.e. it specifies a namespace URI instead of a
       * namespace index.
       */
      public boolean isAbsolute() {
          return namespaceUri != null;
      }
  
      /**
       * @return {@code true} if this {@link ExpandedNodeId} is relative, i.e. it specifies a namespace index instead of a
       * namespace URI, and is therefore defined relative to some {@link NamespaceTable}.
       */
      public boolean isRelative() {
          return !isAbsolute();
      }
  
      /**
       * @return {@code true} if this {@link ExpandedNodeId} is local (serverIndex == 0).
       */
      public boolean isLocal() {
          return serverIndex.longValue() == 0;
      }
  
      /**
       * @return {@code true} if this {@link ExpandedNodeId} is null.
       */
      public boolean isNull() {
          if (namespaceIndex.intValue() > 0) {
              return false;
          }
  
          switch (getType()) {
              case Numeric: {
                  UInteger id = (UInteger) this.identifier;
                  return id.intValue() == 0;
              }
              case String: {
                  String id = (String) this.identifier;
                  return Strings.isNullOrEmpty(id);
              }
              case Guid: {
                  UUID id = (UUID) this.identifier;
                  return id.getLeastSignificantBits() == 0 && id.getMostSignificantBits() == 0;
              }
              case Opaque: {
                  ByteString id = (ByteString) this.identifier;
                  return id == null || id.isNullOrEmpty();
              }
  
              default:
                  return true;
          }
      }
  
      public boolean isNotNull() {
          return !isNull();
      }
  
      /**
       * Convert this {@link ExpandedNodeId} to an absolute ExpandedNodeId if not already.
       * <p>
       * An absolute ExpandedNodeId is one that defines the namespace URI rather than namespace index.
       * <p>
       * Returns {@link Optional#empty()} if the URI is not found in the namespace table.
       *
       * @param namespaceTable the {@link NamespaceTable} to use when converting the index to URI.
       * @return an absolute ExpandedNodeId, or this ExpandedNodeId if it's already absolute.
       * @see #isAbsolute()
       */
      public Optional<ExpandedNodeId> absolute(NamespaceTable namespaceTable) {
          if (isAbsolute()) {
              return Optional.of(this);
          } else {
              String namespaceUri = namespaceTable.getUri(namespaceIndex);
  
              if (namespaceUri != null) {
                  ExpandedNodeId xni = new ExpandedNodeId(
                      UShort.MIN,
                      namespaceUri,
                      identifier,
                      serverIndex
                  );
  
                  return Optional.of(xni);
              } else {
                  return Optional.empty();
              }
          }
      }
  
      /**
       * Convert this {@link ExpandedNodeId} to a relative ExpandedNodeId if not already.
       * <p>
       * A relative ExpandedNodeId is one that defines the namespace index rather than namespace URI.
       * <p>
       * Returns {@link Optional#empty()} if the URI is not found in the namespace table.
       *
       * @param namespaceTable the {@link NamespaceTable} to use when converting the URI to index.
       * @return a relative ExpandedNodeId, or this ExpandedNodeId if it's already relative.
       * @see #isRelative()
       */
      public Optional<ExpandedNodeId> relative(NamespaceTable namespaceTable) {
          if (isRelative()) {
              return Optional.of(this);
          } else {
              UShort namespaceIndex = namespaceTable.getIndex(namespaceUri);
  
              if (namespaceIndex != null) {
                  ExpandedNodeId xni = new ExpandedNodeId(
                      namespaceIndex,
                      null,
                      identifier,
                      serverIndex
                  );
  
                  return Optional.of(xni);
              } else {
                  return Optional.empty();
              }
          }
      }
  
      /**
       * Re-index this {@link ExpandedNodeId} from its current namespace index to the index for
       * {@code namespaceUri} in {@code namespaceTable}.
       * <p>
       * If the target namespace URI is not present in the namespace table this ExpandedNodeId
       * 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 ExpandedNodeId reindex(NamespaceTable namespaceTable, String namespaceUri) {
          if (isAbsolute()) {
              // namespaceUri is specified; namespaceIndex is ignored
              return this;
          } else {
              UShort newNamespaceIndex = namespaceTable.getIndex(namespaceUri);
  
              if (newNamespaceIndex != null &&
                  !Objects.equals(namespaceIndex, newNamespaceIndex)) {
  
                  return new ExpandedNodeId(
                      newNamespaceIndex,
                      null,
                      identifier,
                      serverIndex
                  );
              } else {
                  return this;
              }
          }
      }
  
      /**
       * If this {@link ExpandedNodeId} resides on the local server ({@code serverIndex == 0}), return its representation
       * as a local {@link NodeId}.
       * <p>
       * If this ExpandedNodeId specifies a namespace URI instead of a namespace index then the URI must exist in
       * {@code namespaceTable} or {@link Optional#empty()} is returned.
       *
       * @param namespaceTable the {@link NamespaceTable}.
       * @return a local {@link NodeId}, if {@code serverIndex == 0} and the namespace index can be determined.
       * @deprecated use {@link #toNodeId(NamespaceTable)} instead.
       */
      @Deprecated
      public Optional<NodeId> local(NamespaceTable namespaceTable) {
          return toNodeId(namespaceTable);
      }
  
      /**
       * Like {@link #local(NamespaceTable)}, but throws if the node is not local or the namespace is not registered.
       *
       * @param namespaceTable the {@link NamespaceTable}.
       * @return a local {@link NodeId}.
       * @throws Exception if the node is not local or the namespace is not registered.
       * @deprecated use {@link #toNodeIdOrThrow(NamespaceTable)} instead.
       */
      @Deprecated
      public NodeId localOrThrow(NamespaceTable namespaceTable) throws Exception {
          return toNodeIdOrThrow(namespaceTable);
      }
  
      /**
       * If this {@link ExpandedNodeId} resides on the local server ({@code serverIndex == 0}), return its representation
       * as a local {@link NodeId}.
       * <p>
       * If this ExpandedNodeId specifies a namespace URI instead of a namespace index then the URI must exist in
       * {@code namespaceTable} or {@link Optional#empty()} is returned.
       *
       * @param namespaceTable the {@link NamespaceTable}.
       * @return a local {@link NodeId}, if {@code serverIndex == 0} and the namespace index can be determined.
       */
      public Optional<NodeId> toNodeId(NamespaceTable namespaceTable) {
          if (isLocal()) {
              if (namespaceUri == null || namespaceUri.isEmpty()) {
                  NodeId nodeId = new NodeId(namespaceIndex, identifier);
  
                  return Optional.of(nodeId);
              } else {
                  UShort namespaceIndex = namespaceTable.getIndex(namespaceUri);
  
                  if (namespaceIndex == null) {
                      return Optional.empty();
                  } else {
                      NodeId nodeId = new NodeId(namespaceIndex, identifier);
  
                      return Optional.of(nodeId);
                  }
              }
          } else {
              return Optional.empty();
          }
      }
  
      /**
       * Like {@link #toNodeId(NamespaceTable)}, but throws if the node is not local or the namespace is not registered.
       *
       * @param namespaceTable the {@link NamespaceTable}.
       * @return a local {@link NodeId}.
       * @throws Exception if the node is not local or the namespace is not registered.
       */
      public NodeId toNodeIdOrThrow(NamespaceTable namespaceTable) throws Exception {
          if (isLocal()) {
              return toNodeId(namespaceTable)
                  .orElseThrow(() -> new Exception("namespace not registered: " + namespaceUri));
          } else {
              throw new Exception("not a local node (serverIndex = " + serverIndex + ")");
          }
      }
  
      /**
       * Check if this {@link ExpandedNodeId} is equal to {@code nodeId}.
       * <p>
       * To be considered equal this ExpandedNodeId must be in serverIndex == 0, have the same namespace index as
       * {@code nodeId} or have a namespace URI at the same index in the default namespace table, and an equal
       * identifier.
       *
       * @param nodeId the {@link NodeId} to check equality against.
       * @return {@code true} if this {@link ExpandedNodeId} is equal to {@code nodeId}.
       * @deprecated use {@link #equalTo(NodeId)} instead.
       */
      @Deprecated
      public boolean equals(NodeId nodeId) {
          return equalTo(nodeId);
      }
  
      /**
       * Check if this {@link ExpandedNodeId} is equal to {@code nodeId}.
       * <p>
       * To be considered equal this ExpandedNodeId must be in serverIndex == 0, have the same namespace index as
       * {@code nodeId} or have a namespace URI at the same index in the default namespace table, and an equal
       * identifier.
       *
       * @param nodeId         the {@link NodeId} 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 ExpandedNodeId} is equal to {@code nodeId}.
       * @deprecated use {@link #equalTo(NodeId, NamespaceTable)} instead.
       */
      @Deprecated
      public boolean equals(NodeId nodeId, NamespaceTable namespaceTable) {
          return equalTo(nodeId, namespaceTable);
      }
  
      /**
       * Check if this {@link ExpandedNodeId} is equal to {@code nodeId}.
       * <p>
       * To be considered equal this ExpandedNodeId must be in serverIndex == 0, have the same namespace index as
       * {@code nodeId} or have a namespace URI at the same index in the default namespace table, and an equal
       * identifier.
       *
       * @param nodeId the {@link NodeId} to check equality against.
       * @return {@code true} if this {@link ExpandedNodeId} is equal to {@code nodeId}.
       */
      public boolean equalTo(NodeId nodeId) {
          return nodeId.equalTo(this);
      }
  
      /**
       * Check if this {@link ExpandedNodeId} is equal to {@code nodeId}.
       * <p>
       * To be considered equal this ExpandedNodeId must be in serverIndex == 0, have the same namespace index as
       * {@code nodeId} or have a namespace URI at the same index in the default namespace table, and an equal
       * identifier.
       *
       * @param nodeId         the {@link NodeId} 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 ExpandedNodeId} is equal to {@code nodeId}.
       */
      public boolean equalTo(NodeId nodeId, NamespaceTable namespaceTable) {
          return nodeId.equalTo(this, namespaceTable);
      }
  
      @Override
      public boolean equals(Object o) {
          if (this == o) return true;
          if (o == null || getClass() != o.getClass()) return false;
          ExpandedNodeId that = (ExpandedNodeId) o;
          return Objects.equals(namespaceIndex, that.namespaceIndex) &&
              Objects.equals(identifier, that.identifier) &&
              Objects.equals(namespaceUri, that.namespaceUri) &&
              serverIndex.equals(that.serverIndex);
      }
  
      @Override
      public int hashCode() {
          return Objects.hash(namespaceIndex, identifier, namespaceUri, serverIndex);
      }
  
      @Override
      public String toString() {
          ToStringHelper helper = MoreObjects.toStringHelper(this);
  
          if (namespaceUri != null && namespaceUri.length() > 0) {
              helper.add("ns", namespaceUri);
          } else {
              helper.add("ns", getNamespaceIndex());
          }
  
          helper.add("id", getIdentifier());
          helper.add("serverIndex", getServerIndex());
  
          return helper.toString();
      }
  
      public String toParseableString() {
          StringBuilder sb = new StringBuilder();
  
          if (serverIndex.intValue() != 0) {
              sb.append("svr=").append(serverIndex).append(";");
          }
  
          if (namespaceUri != null) {
              sb.append("nsu=").append(namespaceUri).append(";");
          } else {
              int namespaceIndex = getNamespaceIndex().intValue();
              sb.append("ns=").append(namespaceIndex).append(";");
          }
  
          switch (getType()) {
              case Numeric:
                  sb.append("i=").append(getIdentifier());
                  break;
              case String:
                  sb.append("s=").append(getIdentifier());
                  break;
              case Guid:
                  sb.append("g=").append(getIdentifier());
                  break;
              case Opaque:
                  ByteString bs = (ByteString) getIdentifier();
                  if (bs.isNull()) sb.append("b=");
                  else sb.append("b=").append(DatatypeConverter.printBase64Binary(bs.bytes()));
                  break;
  
              default:
                  throw new IllegalStateException("IdType " + getType());
          }
  
          return sb.toString();
      }
  
      /**
       * Parse {@code s} into an {@link ExpandedNodeId}.
       *
       * @param s the String to parse.
       * @return an {@link ExpandedNodeId}.
       * @throws UaRuntimeException if parsing fails.
       */
      public static ExpandedNodeId parse(String s) {
          try {
              UInteger serverIndex = UInteger.MIN;
              if (s.startsWith("svr=")) {
                  int endIndex = s.indexOf(";");
                  serverIndex = uint(Integer.parseInt(s.substring(4, endIndex)));
                  s = s.substring(endIndex + 1);
              }
  
              UShort namespaceIndex = UShort.MIN;
              String namespaceUri = null;
              if (s.startsWith("ns=")) {
                  int endIndex = s.indexOf(";");
                  namespaceIndex = ushort(Integer.parseInt(s.substring(3, endIndex)));
                  s = s.substring(endIndex + 1);
              } else if (s.startsWith("nsu=")) {
                  int endIndex = s.indexOf(";");
                  namespaceUri = s.substring(4, endIndex);
                  s = s.substring(endIndex + 1);
              }
  
              Object identifier;
              String type = s.substring(0, 2);
              String id = s.substring(2);
  
              switch (type) {
                  case "i=":
                      try {
                          identifier = uint(Long.parseLong(id));
                      } catch (NumberFormatException e) {
                          throw new UaRuntimeException(StatusCodes.Bad_NodeIdInvalid, e);
                      }
                      break;
                  case "s=":
                      identifier = id;
                      break;
                  case "g=":
                      try {
                          identifier = UUID.fromString(id);
                      } catch (IllegalArgumentException e) {
                          throw new UaRuntimeException(StatusCodes.Bad_NodeIdInvalid, e);
                      }
                      break;
                  case "b=":
                      try {
                          identifier = ByteString.of(DatatypeConverter.parseBase64Binary(id));
                      } catch (IllegalArgumentException e) {
                          throw new UaRuntimeException(StatusCodes.Bad_NodeIdInvalid, e);
                      }
                      break;
                  default:
                      throw new UaRuntimeException(StatusCodes.Bad_NodeIdInvalid);
              }
  
              return new ExpandedNodeId(namespaceIndex, namespaceUri, identifier, serverIndex);
          } catch (Throwable t) {
              throw new UaRuntimeException(StatusCodes.Bad_NodeIdInvalid, t);
          }
      }
  
      /**
       * Parse {@code s} into an {@link ExpandedNodeId}, or return {@code null} if parsing fails.
       *
       * @param s the String to parse.
       * @return an {@link ExpandedNodeId} or {@code null} if parsing fails.
       */
      @Nullable
      public static ExpandedNodeId parseOrNull(@NotNull String s) {
          try {
              return ExpandedNodeId.parse(s);
          } catch (UaRuntimeException t) {
              return null;
          }
      }
  
      /**
       * Parse {@code s} into an Optional containing an {@link ExpandedNodeId}, or return
       * {@link Optional#empty()}} if parsing fails.
       *
       * @param s the String to parse.
       * @return an Optional containing an {@link ExpandedNodeId}, or {@link Optional#empty()} if parsing fails.
       */
      public static Optional<ExpandedNodeId> parseSafe(@NotNull String s) {
          return Optional.ofNullable(parseOrNull(s));
      }
  
      /**
       * Return a new {@link ExpandedNodeId.Builder}.
       *
       * @return a new {@link ExpandedNodeId.Builder}.
       */
      public static Builder newBuilder() {
          return new Builder();
      }
  
      public static class Builder {
  
          private UShort namespaceIndex = UShort.MIN;
          private String namespaceUri = null;
          private Object identifier = null;
          private UInteger serverIndex = UInteger.MIN;
  
          public Builder setNamespaceIndex(int namespaceIndex) {
              return setNamespaceIndex(ushort(namespaceIndex));
          }
  
          public Builder setNamespaceIndex(UShort namespaceIndex) {
              this.namespaceIndex = namespaceIndex;
              return this;
          }
  
          public Builder setNamespaceUri(String namespaceUri) {
              this.namespaceUri = namespaceUri;
              return this;
          }
  
          public Builder setIdentifier(UInteger identifier) {
              this.identifier = identifier;
              return this;
          }
  
          public Builder setIdentifier(String identifier) {
              this.identifier = identifier;
              return this;
          }
  
          public Builder setIdentifier(UUID identifier) {
              this.identifier = identifier;
              return this;
          }
  
          public Builder setIdentifier(ByteString identifier) {
              this.identifier = identifier;
              return this;
          }
  
          public Builder setServerIndex(long serverIndex) {
              return setServerIndex(uint(serverIndex));
          }
  
          public Builder setServerIndex(UInteger serverIndex) {
              this.serverIndex = serverIndex;
              return this;
          }
  
          public ExpandedNodeId build() {
              return new ExpandedNodeId(namespaceIndex, namespaceUri, identifier, serverIndex);
          }
  
      }
  
  }
  