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.server.util;
  
  import java.util.Optional;
  import java.util.Set;
  
  import org.eclipse.milo.opcua.sdk.core.AccessLevel;
  import org.eclipse.milo.opcua.sdk.core.NumericRange;
  import org.eclipse.milo.opcua.sdk.core.Reference;
  import org.eclipse.milo.opcua.sdk.core.ValueRanks;
  import org.eclipse.milo.opcua.sdk.core.WriteMask;
  import org.eclipse.milo.opcua.sdk.core.nodes.DataTypeNode;
  import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
  import org.eclipse.milo.opcua.sdk.server.nodes.AttributeContext;
  import org.eclipse.milo.opcua.sdk.server.nodes.UaNode;
  import org.eclipse.milo.opcua.sdk.server.nodes.UaServerNode;
  import org.eclipse.milo.opcua.stack.core.AttributeId;
  import org.eclipse.milo.opcua.stack.core.Identifiers;
  import org.eclipse.milo.opcua.stack.core.StatusCodes;
  import org.eclipse.milo.opcua.stack.core.UaException;
  import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString;
  import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
  import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime;
  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.Variant;
  import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte;
  import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
  import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass;
  import org.eclipse.milo.opcua.stack.core.util.ArrayUtil;
  import org.eclipse.milo.opcua.stack.core.util.TypeUtil;
  import org.jetbrains.annotations.Nullable;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import static org.eclipse.milo.opcua.sdk.core.util.StreamUtil.opt2stream;
  import static org.eclipse.milo.opcua.sdk.server.util.AttributeUtil.extract;
  import static org.eclipse.milo.opcua.sdk.server.util.AttributeUtil.getAccessLevels;
  import static org.eclipse.milo.opcua.sdk.server.util.AttributeUtil.getUserAccessLevels;
  import static org.eclipse.milo.opcua.sdk.server.util.AttributeUtil.getUserWriteMasks;
  import static org.eclipse.milo.opcua.sdk.server.util.AttributeUtil.getWriteMasks;
  
  public class AttributeWriter {
  
      private static final Logger LOGGER = LoggerFactory.getLogger(AttributeWriter.class);
  
      public static void writeAttribute(AttributeContext context,
                                        UaServerNode node,
                                        AttributeId attributeId,
                                        DataValue value,
                                        @Nullable String indexRange) throws UaException {
  
          AttributeContext internalContext = new AttributeContext(context.getServer());
  
          NodeClass nodeClass = node.getNodeClass();
  
          if (attributeId == AttributeId.Value && nodeClass == NodeClass.Variable) {
              Set<AccessLevel> accessLevels = getAccessLevels(node, internalContext);
              if (!accessLevels.contains(AccessLevel.CurrentWrite)) {
                  throw new UaException(StatusCodes.Bad_NotWritable);
              }
  
              Set<AccessLevel> userAccessLevels = getUserAccessLevels(node, context);
              if (!userAccessLevels.contains(AccessLevel.CurrentWrite)) {
                  throw new UaException(StatusCodes.Bad_UserAccessDenied);
              }
          } else {
              WriteMask writeMask = writeMaskForAttribute(attributeId);
  
              Set<WriteMask> writeMasks = getWriteMasks(node, internalContext);
              if (!writeMasks.contains(writeMask)) {
                  throw new UaException(StatusCodes.Bad_NotWritable);
              }
  
              Set<WriteMask> userWriteMasks = getUserWriteMasks(node, context);
              if (!userWriteMasks.contains(writeMask)) {
                  throw new UaException(StatusCodes.Bad_UserAccessDenied);
              }
          }
  
          Variant updateVariant = value.getValue();
  
          if (indexRange != null) {
              NumericRange range = NumericRange.parse(indexRange);
  
              DataValue current = node.getAttribute(
                  internalContext,
                  attributeId
              );
  
              Variant currentVariant = current.getValue();
  
              Object valueAtRange = NumericRange.writeToValueAtRange(
                  currentVariant,
                  updateVariant,
                  range
              );
  
              updateVariant = new Variant(valueAtRange);
          }
  
          DateTime sourceTime = value.getSourceTime();
          DateTime serverTime = value.getServerTime();
  
          value = new DataValue(
              updateVariant,
              value.getStatusCode(),
              (sourceTime == null || sourceTime.isNull()) ? DateTime.now() : sourceTime,
              (serverTime == null || serverTime.isNull()) ? DateTime.now() : serverTime
          );
  
          if (attributeId == AttributeId.Value) {
              NodeId dataType = extract(
                  node.getAttribute(
                      internalContext,
                      AttributeId.DataType)
              );
  
              if (dataType != null) {
                  value = validateDataType(context.getServer(), dataType, value);
              }
  
              Integer valueRank = extract(
                  node.getAttribute(
                      internalContext,
                      AttributeId.ValueRank)
              );
  
              if (valueRank == null) valueRank = 0;
  
              if (valueRank > 0) {
                  UInteger[] arrayDimensions = extract(
                      node.getAttribute(
                          context,
                          AttributeId.ArrayDimensions)
                  );
  
                  validateArrayType(valueRank, arrayDimensions, value);
              }
          }
  
          node.setAttribute(context, attributeId, value);
      }
  
      private static WriteMask writeMaskForAttribute(AttributeId attributeId) {
          switch (attributeId) {
              case AccessLevel:
                  return WriteMask.AccessLevel;
              case ArrayDimensions:
                  return WriteMask.ArrayDimensions;
              case BrowseName:
                  return WriteMask.BrowseName;
              case ContainsNoLoops:
                  return WriteMask.ContainsNoLoops;
              case DataType:
                  return WriteMask.DataType;
              case Description:
                  return WriteMask.Description;
              case DisplayName:
                  return WriteMask.DisplayName;
              case EventNotifier:
                  return WriteMask.EventNotifier;
              case Executable:
                  return WriteMask.Executable;
              case Historizing:
                  return WriteMask.Historizing;
              case InverseName:
                  return WriteMask.InverseName;
              case IsAbstract:
                  return WriteMask.IsAbstract;
              case MinimumSamplingInterval:
                  return WriteMask.MinimumSamplingInterval;
              case NodeClass:
                  return WriteMask.NodeClass;
              case NodeId:
                  return WriteMask.NodeId;
              case Symmetric:
                  return WriteMask.Symmetric;
              case UserAccessLevel:
                  return WriteMask.UserAccessLevel;
              case UserExecutable:
                  return WriteMask.UserExecutable;
              case UserWriteMask:
                  return WriteMask.UserWriteMask;
              case Value:
                  return WriteMask.ValueForVariableType;
              case ValueRank:
                  return WriteMask.ValueRank;
              case WriteMask:
                  return WriteMask.WriteMask;
  
              default:
                  throw new IllegalArgumentException("unknown AttributeId: " + attributeId);
          }
      }
  
      private static DataValue validateDataType(
          OpcUaServer server,
          NodeId dataType,
          DataValue value) throws UaException {
  
          Variant variant = value.getValue();
          if (variant == null) return value;
  
          Object o = variant.getValue();
          if (o == null) throw new UaException(StatusCodes.Bad_TypeMismatch);
  
          Class<?> valueClass = o.getClass().isArray() ?
              ArrayUtil.getType(o) : o.getClass();
  
          Class<?> expectedClass = getExpectedClass(server, dataType, valueClass);
  
          if (expectedClass != null) {
              LOGGER.debug(
                  "dataTypeId={}, valueClass={}, expectedClass={}",
                  dataType, valueClass.getSimpleName(), expectedClass.getSimpleName());
  
              if (!expectedClass.isAssignableFrom(valueClass)) {
                  // Writing a ByteString to a UByte[] is explicitly allowed by the spec.
                  if (o instanceof ByteString && expectedClass == UByte.class) {
                      ByteString byteString = (ByteString) o;
  
                      return new DataValue(
                          new Variant(byteString.uBytes()),
                          value.getStatusCode(),
                          value.getSourceTime(),
                          value.getServerTime()
                      );
                  } else if (expectedClass == Variant.class) {
                      // Allow writing anything to a Variant
                      return value;
                  } else {
                      throw new UaException(StatusCodes.Bad_TypeMismatch);
                  }
              }
          } else {
              throw new UaException(StatusCodes.Bad_TypeMismatch);
          }
  
          return value;
      }
  
      private static void validateArrayType(
          Integer valueRank,
          UInteger[] arrayDimensions,
          DataValue value) throws UaException {
  
          Variant variant = value.getValue();
          if (variant == null) return;
  
          Object o = variant.getValue();
          if (o == null) return;
  
          boolean valueIsArray = o.getClass().isArray();
  
          switch (valueRank) {
              case ValueRanks.ScalarOrOneDimension:
                  if (valueIsArray) {
                      int[] valueDimensions = ArrayUtil.getDimensions(o);
  
                      if (valueDimensions.length > 1) {
                          throw new UaException(StatusCodes.Bad_TypeMismatch);
                      }
                  }
                  break;
  
              case ValueRanks.Any:
                  break;
  
              case ValueRanks.Scalar:
                  if (valueIsArray) {
                      throw new UaException(StatusCodes.Bad_TypeMismatch);
                  }
                  break;
  
              case ValueRanks.OneOrMoreDimensions:
                  if (!valueIsArray) {
                      throw new UaException(StatusCodes.Bad_TypeMismatch);
                  }
                  break;
  
              case ValueRanks.OneDimension:
              default:
                  if (!valueIsArray) {
                      throw new UaException(StatusCodes.Bad_TypeMismatch);
                  }
  
                  int[] valueDimensions = ArrayUtil.getDimensions(o);
  
                  if (valueDimensions.length != valueRank) {
                      throw new UaException(StatusCodes.Bad_TypeMismatch);
                  }
  
                  int[] nodeDimensions = Optional.ofNullable(arrayDimensions).map(uia -> {
                      int[] dims = new int[uia.length];
                      for (int i = 0; i < uia.length; i++) {
                          dims[i] = uia[i].intValue();
                      }
                      return dims;
                  }).orElse(new int[0]);
  
                  if (nodeDimensions.length > 0) {
                      if (nodeDimensions.length != valueDimensions.length) {
                          throw new UaException(StatusCodes.Bad_TypeMismatch);
                      }
  
                      for (int i = 0; i < nodeDimensions.length; i++) {
                          if (nodeDimensions[i] > 0 && valueDimensions[i] > nodeDimensions[i]) {
                              throw new UaException(StatusCodes.Bad_TypeMismatch);
                          }
                      }
                  }
                  break;
          }
      }
  
      private static Class<?> getExpectedClass(
          OpcUaServer server,
          NodeId dataTypeId,
          Class<?> valueClass) throws UaException {
  
          if (TypeUtil.isBuiltin(dataTypeId)) {
              return TypeUtil.getBackingClass(dataTypeId);
          } else if (subtypeOf(server, dataTypeId, Identifiers.Structure)) {
              return ExtensionObject.class;
          } else if (subtypeOf(server, dataTypeId, Identifiers.Enumeration)) {
              return Integer.class;
          } else {
              NodeId superBuiltInType = findConcreteBuiltInSuperTypeId(server, dataTypeId);
  
              if (superBuiltInType != null) {
                  // One of dataTypeId's supertypes is a concrete built-in
                  // type; expect the same Class<?> as that built-in type.
                  return TypeUtil.getBackingClass(superBuiltInType);
              } else {
                  int valueDataTypeId = TypeUtil.getBuiltinTypeId(valueClass);
  
                  if (valueDataTypeId > -1) {
                      // The value they sent us maps to a built-in type.
                      // If dataTypeId is a subtype of that built-in type,
                      // the expected class is the class of the value they sent.
                      NodeId builtInTypeId = new NodeId(0, valueDataTypeId);
  
                      if (dataTypeId.equals(builtInTypeId) ||
                          subtypeOf(server, builtInTypeId, dataTypeId)) {
  
                          return valueClass;
                      } else {
                          throw new UaException(StatusCodes.Bad_TypeMismatch);
                      }
                  } else {
                      throw new UaException(StatusCodes.Bad_TypeMismatch);
                  }
              }
          }
      }
  
      /**
       * @return {@code true} if {@code dataTypeId} is a subtype of {@code potentialSuperTypeId}.
       */
      private static boolean subtypeOf(OpcUaServer server, NodeId dataTypeId, NodeId potentialSuperTypeId) {
          UaNode dataTypeNode = server.getAddressSpaceManager()
              .getManagedNode(dataTypeId)
              .orElse(null);
  
          if (dataTypeNode != null) {
              NodeId superTypeId = getSuperTypeId(server, dataTypeId);
  
              if (superTypeId != null) {
                  return superTypeId.equals(potentialSuperTypeId) ||
                      subtypeOf(server, superTypeId, potentialSuperTypeId);
              } else {
                  return false;
              }
          } else {
              return false;
          }
      }
  
      /**
       * Find the first concrete built-in supertype for {@code dataTypeId}, if one exists.
       *
       * @return the first concrete built-in supertype for {@code dataTypeId}, if one exists.
       */
      @Nullable
      private static NodeId findConcreteBuiltInSuperTypeId(OpcUaServer server, NodeId dataTypeId) {
          if (TypeUtil.isBuiltin(dataTypeId) && isConcrete(server, dataTypeId)) {
              return dataTypeId;
          } else {
              NodeId superTypeId = getSuperTypeId(server, dataTypeId);
  
              if (superTypeId != null) {
                  return findConcreteBuiltInSuperTypeId(server, superTypeId);
              } else {
                  return null;
              }
          }
      }
  
      @Nullable
      private static NodeId getSuperTypeId(OpcUaServer server, NodeId dataTypeId) {
          UaNode dataTypeNode = server.getAddressSpaceManager()
              .getManagedNode(dataTypeId)
              .orElse(null);
  
          if (dataTypeNode != null) {
              return dataTypeNode.getReferences()
                  .stream()
                  .filter(Reference.SUBTYPE_OF)
                  .flatMap(r -> opt2stream(r.getTargetNodeId().toNodeId(server.getNamespaceTable())))
                  .findFirst()
                  .orElse(null);
          } else {
              return null;
          }
      }
  
      private static boolean isAbstract(OpcUaServer server, NodeId dataTypeId) {
          UaNode node = server.getAddressSpaceManager().getManagedNode(dataTypeId).orElse(null);
  
          if (node instanceof DataTypeNode) {
              return ((DataTypeNode) node).getIsAbstract();
          } else {
              return false;
          }
      }
  
      private static boolean isConcrete(OpcUaServer server, NodeId dataTypeId) {
          return !isAbstract(server, dataTypeId);
      }
  
  }
  