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 com.google.common.base.MoreObjects;
  import com.google.common.base.Objects;
  import org.eclipse.milo.opcua.stack.core.StatusCodes;
  import org.eclipse.milo.opcua.stack.core.UaSerializationException;
  import org.eclipse.milo.opcua.stack.core.serialization.SerializationContext;
  import org.eclipse.milo.opcua.stack.core.serialization.UaStructure;
  import org.eclipse.milo.opcua.stack.core.types.DataTypeEncoding;
  import org.eclipse.milo.opcua.stack.core.types.OpcUaDefaultBinaryEncoding;
  import org.eclipse.milo.opcua.stack.core.types.OpcUaDefaultXmlEncoding;
  import org.eclipse.milo.opcua.stack.core.util.Lazy;
  import org.jetbrains.annotations.NotNull;
  import org.jetbrains.annotations.Nullable;
  
  public final class ExtensionObject {
  
      public enum BodyType {
          ByteString,
          XmlElement
      }
  
      private final Lazy<Object> decoded = new Lazy<>();
  
      private final BodyType bodyType;
  
      private final Object body;
      private final NodeId encodingId;
  
      public ExtensionObject(
          @NotNull ByteString body,
          @NotNull NodeId encodingId) {
  
          this((Object) body, encodingId);
      }
  
      public ExtensionObject(
          @NotNull XmlElement body,
          @NotNull NodeId encodingId) {
  
          this((Object) body, encodingId);
      }
  
      private ExtensionObject(
          @NotNull Object body,
          @NotNull NodeId encodingId) {
  
          this.body = body;
          this.encodingId = encodingId;
  
          if (body instanceof ByteString) {
              bodyType = BodyType.ByteString;
          } else if (body instanceof XmlElement) {
              bodyType = BodyType.XmlElement;
          } else {
              throw new IllegalArgumentException("body: " + body);
          }
      }
  
      public Object getBody() {
          return body;
      }
  
      public BodyType getBodyType() {
          return bodyType;
      }
  
      public NodeId getEncodingId() {
          return encodingId;
      }
  
      public boolean isNull() {
          switch (bodyType) {
              case ByteString:
                  return ((ByteString) body).isNull();
              case XmlElement:
                  return ((XmlElement) body).isNull();
              default:
                  throw new IllegalStateException("BodyType: " + bodyType);
          }
      }
  
      public Object decode(SerializationContext context) throws UaSerializationException {
          switch (bodyType) {
              case ByteString:
                  return decode(context, OpcUaDefaultBinaryEncoding.getInstance());
              case XmlElement:
                  return decode(context, OpcUaDefaultXmlEncoding.getInstance());
              default:
                  throw new IllegalStateException("BodyType: " + bodyType);
          }
      }
  
      public Object decode(SerializationContext context, DataTypeEncoding encoding) throws UaSerializationException {
          return decoded.getOrCompute(() -> encoding.decode(context, body, encodingId));
      }
  
      @Nullable
      public Object decodeOrNull(SerializationContext context) {
          try {
              return decode(context);
          } catch (UaSerializationException e) {
              return null;
          }
      }
  
      public ExtensionObject transcode(
          SerializationContext context,
          NodeId newEncodingId,
          DataTypeEncoding newEncoding
      ) {
  
          if (this.encodingId.equals(newEncodingId)) {
              return this;
          } else {
              // The "fast" path: body is a encoded in Default Binary or Default XML.
              // No need to look up the DataTypeEncoding.
              Object struct = decodeOrNull(context);
  
              if (struct != null) {
                  Object encoded = newEncoding.encode(context, struct, newEncodingId);
  
                  return new ExtensionObject(encoded, newEncodingId);
              } else {
                  // TODO look up current DataTypeEncoding via this.encodingId, try decoding again using that.
                  return this;
              }
          }
      }
  
      public static ExtensionObject encode(
          SerializationContext context,
          UaStructure struct
      ) throws UaSerializationException {
  
          NodeId encodingId = struct.getBinaryEncodingId()
              .toNodeId(context.getNamespaceTable())
              .orElseThrow(
                  () ->
                      new UaSerializationException(
                          StatusCodes.Bad_EncodingError,
                          "namespace not registered: " +
                              struct.getBinaryEncodingId().getNamespaceUri())
              );
  
          return encodeDefaultBinary(context, struct, encodingId);
      }
  
      public static ExtensionObject[] encodeArray(
          SerializationContext context,
          UaStructure[] structArray
      ) throws UaSerializationException {
  
          ExtensionObject[] xos = new ExtensionObject[structArray.length];
  
          for (int i = 0; i < xos.length; i++) {
              xos[i] = encode(context, structArray[i]);
          }
  
          return xos;
      }
  
      public static ExtensionObject encodeDefaultBinary(
          SerializationContext context,
          Object object,
          NodeId encodingId
      ) throws UaSerializationException {
  
          return encode(
              context,
              object,
              encodingId,
              OpcUaDefaultBinaryEncoding.getInstance()
          );
      }
  
      public static ExtensionObject encodeDefaultXml(
          SerializationContext context,
          Object object,
          NodeId encodingId
      ) throws UaSerializationException {
  
          return encode(
              context,
              object,
              encodingId,
              OpcUaDefaultXmlEncoding.getInstance()
          );
      }
  
      public static ExtensionObject encode(
          SerializationContext context,
          Object object,
          ExpandedNodeId xEncodingId,
          DataTypeEncoding encoding
      ) throws UaSerializationException {
  
          NodeId encodingId = xEncodingId.toNodeId(context.getNamespaceTable())
              .orElseThrow(
                  () ->
                      new UaSerializationException(
                          StatusCodes.Bad_EncodingError,
                          "namespace not registered: " +
                              xEncodingId.getNamespaceUri())
              );
  
          Object body = encoding.encode(context, object, encodingId);
  
          return new ExtensionObject(body, encodingId);
      }
  
      public static ExtensionObject encode(
          SerializationContext context,
          Object object,
          NodeId encodingId,
          DataTypeEncoding encoding
      ) throws UaSerializationException {
  
          Object body = encoding.encode(context, object, encodingId);
  
          return new ExtensionObject(body, encodingId);
      }
  
      @Override
      public boolean equals(Object o) {
          if (this == o) return true;
          if (o == null || getClass() != o.getClass()) return false;
  
          ExtensionObject that = (ExtensionObject) o;
  
          return Objects.equal(body, that.body) &&
              Objects.equal(encodingId, that.encodingId);
      }
  
      @Override
      public int hashCode() {
          return Objects.hashCode(body, encodingId);
      }
  
      @Override
      public String toString() {
          return MoreObjects.toStringHelper(this)
              .add("encoded", body)
              .add("encodingId", encodingId)
              .toString();
      }
  
  }
  