/*
* Copyright 2015 Kevin Herron
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.digitalpetri.opcua.stack.core.serialization.binary;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.nio.ByteOrder;
import java.util.UUID;
import java.util.function.BiConsumer;
import javax.annotation.Nonnull;
import com.digitalpetri.opcua.stack.core.StatusCodes;
import com.digitalpetri.opcua.stack.core.UaSerializationException;
import com.digitalpetri.opcua.stack.core.channel.ChannelConfig;
import com.digitalpetri.opcua.stack.core.serialization.DelegateRegistry;
import com.digitalpetri.opcua.stack.core.serialization.EncoderDelegate;
import com.digitalpetri.opcua.stack.core.serialization.UaEncoder;
import com.digitalpetri.opcua.stack.core.serialization.UaEnumeration;
import com.digitalpetri.opcua.stack.core.serialization.UaSerializable;
import com.digitalpetri.opcua.stack.core.serialization.UaStructure;
import com.digitalpetri.opcua.stack.core.types.builtin.ByteString;
import com.digitalpetri.opcua.stack.core.types.builtin.DataValue;
import com.digitalpetri.opcua.stack.core.types.builtin.DateTime;
import com.digitalpetri.opcua.stack.core.types.builtin.DiagnosticInfo;
import com.digitalpetri.opcua.stack.core.types.builtin.ExpandedNodeId;
import com.digitalpetri.opcua.stack.core.types.builtin.ExtensionObject;
import com.digitalpetri.opcua.stack.core.types.builtin.LocalizedText;
import com.digitalpetri.opcua.stack.core.types.builtin.NodeId;
import com.digitalpetri.opcua.stack.core.types.builtin.QualifiedName;
import com.digitalpetri.opcua.stack.core.types.builtin.StatusCode;
import com.digitalpetri.opcua.stack.core.types.builtin.Variant;
import com.digitalpetri.opcua.stack.core.types.builtin.XmlElement;
import com.digitalpetri.opcua.stack.core.types.builtin.unsigned.UByte;
import com.digitalpetri.opcua.stack.core.types.builtin.unsigned.UInteger;
import com.digitalpetri.opcua.stack.core.types.builtin.unsigned.ULong;
import com.digitalpetri.opcua.stack.core.types.builtin.unsigned.UShort;
import com.digitalpetri.opcua.stack.core.types.builtin.unsigned.Unsigned;
import com.digitalpetri.opcua.stack.core.types.enumerated.IdType;
import com.digitalpetri.opcua.stack.core.util.ArrayUtil;
import com.digitalpetri.opcua.stack.core.util.TypeUtil;
import io.netty.buffer.ByteBuf;
import org.slf4j.LoggerFactory;
public class BinaryEncoder implements UaEncoder {
private volatile ByteBuf buffer;
private final int maxArrayLength;
private final int maxStringLength;
public BinaryEncoder() {
this(ChannelConfig.DEFAULT_MAX_ARRAY_LENGTH, ChannelConfig.DEFAULT_MAX_STRING_LENGTH);
}
public BinaryEncoder(int maxArrayLength, int maxStringLength) {
this.maxArrayLength = maxArrayLength;
this.maxStringLength = maxStringLength;
}
public BinaryEncoder setBuffer(ByteBuf buffer) {
this.buffer = buffer;
return this;
}
public ByteBuf getBuffer() {
return buffer;
}
@Override
public void encodeBoolean(String field, Boolean value) {
if (value == null) {
buffer.writeBoolean(false);
} else {
buffer.writeBoolean(value);
}
}
@Override
public void encodeSByte(String field, Byte value) {
if (value == null) {
buffer.writeByte(0);
} else {
buffer.writeByte(value);
}
}
@Override
public void encodeInt16(String field, Short value) {
if (value == null) {
buffer.writeShort(0);
} else {
buffer.writeShort(value);
}
}
@Override
public void encodeInt32(String field, Integer value) {
if (value == null) {
buffer.writeInt(0);
} else {
buffer.writeInt(value);
}
}
@Override
public void encodeInt64(String field, Long value) {
if (value == null) {
buffer.writeLong(0);
} else {
buffer.writeLong(value);
}
}
@Override
public void encodeByte(String field, UByte value) throws UaSerializationException {
if (value == null) {
buffer.writeByte(0);
} else {
buffer.writeByte(value.intValue());
}
}
@Override
public void encodeUInt16(String field, UShort value) throws UaSerializationException {
if (value == null) {
buffer.writeShort(0);
} else {
buffer.writeShort(value.intValue());
}
}
@Override
public void encodeUInt32(String field, UInteger value) throws UaSerializationException {
if (value == null) {
buffer.writeInt(0);
} else {
buffer.writeInt(value.intValue());
}
}
@Override
public void encodeUInt64(String field, ULong value) throws UaSerializationException {
if (value == null) {
buffer.writeLong(0);
} else {
buffer.writeLong(value.longValue());
}
}
@Override
public void encodeFloat(String field, Float value) {
if (value == null) {
buffer.writeFloat(0);
} else {
buffer.writeFloat(value);
}
}
@Override
public void encodeDouble(String field, Double value) {
if (value == null) {
buffer.writeDouble(0);
} else {
buffer.writeDouble(value);
}
}
@Override
public void encodeString(String field, String value) throws UaSerializationException {
if (value == null) {
buffer.writeInt(-1);
} else {
if (value.length() > maxStringLength) {
throw new UaSerializationException(StatusCodes.Bad_EncodingLimitsExceeded,
"max string length exceeded");
}
try {
// Record the current index and write a placeholder for the length.
int lengthIndex = buffer.writerIndex();
buffer.writeInt(0x42424242);
// Write the string bytes.
int indexBefore = buffer.writerIndex();
buffer.writeBytes(value.getBytes("UTF-8"));
int indexAfter = buffer.writerIndex();
int bytesWritten = indexAfter - indexBefore;
// Go back and update the length.
buffer.writerIndex(lengthIndex);
buffer.writeInt(bytesWritten);
// Return to where we were after writing the string.
buffer.writerIndex(indexAfter);
} catch (UnsupportedEncodingException e) {
throw new UaSerializationException(StatusCodes.Bad_EncodingError, e);
}
}
}
@Override
public void encodeDateTime(String field, DateTime value) {
if (value == null) {
buffer.writeLong(0L);
} else {
buffer.writeLong(value.getUtcTime());
}
}
@Override
public void encodeGuid(String field, UUID value) {
if (value == null) {
buffer.writeZero(16);
} else {
long msb = value.getMostSignificantBits();
long lsb = value.getLeastSignificantBits();
buffer.writeInt((int) (msb >>> 32));
buffer.writeShort((int) (msb >>> 16) & 0xFFFF);
buffer.writeShort((int) (msb) & 0xFFFF);
buffer.order(ByteOrder.BIG_ENDIAN).writeLong(lsb);
}
}
@Override
public void encodeByteString(String field, ByteString value) {
if (value == null || value.isNull()) {
buffer.writeInt(-1);
} else {
byte[] bytes = value.bytes();
assert (bytes != null);
buffer.writeInt(bytes.length);
buffer.writeBytes(bytes);
}
}
@Override
public void encodeXmlElement(String field, XmlElement value) throws UaSerializationException {
if (value == null || value.isNull()) {
buffer.writeInt(-1);
} else {
try {
encodeByteString(null, new ByteString(value.getFragment().getBytes("UTF-8")));
} catch (UnsupportedEncodingException e) {
throw new UaSerializationException(StatusCodes.Bad_DecodingError, e);
}
}
}
@Override
public void encodeNodeId(String field, NodeId value) throws UaSerializationException {
if (value == null) value = NodeId.NULL_VALUE;
int namespaceIndex = value.getNamespaceIndex().intValue();
if (value.getType() == IdType.Numeric) {
UInteger identifier = (UInteger) value.getIdentifier();
long idv = identifier.longValue();
if (namespaceIndex == 0 && idv >= 0 && idv <= 255) {
/* Two-byte format */
buffer.writeByte(0x00);
buffer.writeByte((int) idv);
} else if (namespaceIndex >= 0 && namespaceIndex <= 255 && idv <= 65535) {
/* Four-byte format */
buffer.writeByte(0x01);
buffer.writeByte(namespaceIndex);
buffer.writeShort((int) idv);
} else {
/* Numeric format */
buffer.writeByte(0x02);
buffer.writeShort(namespaceIndex);
buffer.writeInt((int) idv);
}
} else if (value.getType() == IdType.String) {
String identifier = (String) value.getIdentifier();
buffer.writeByte(0x03);
buffer.writeShort(namespaceIndex);
encodeString(null, identifier);
} else if (value.getType() == IdType.Guid) {
UUID identifier = (UUID) value.getIdentifier();
buffer.writeByte(0x04);
buffer.writeShort(namespaceIndex);
encodeGuid(null, identifier);
} else if (value.getType() == IdType.Opaque) {
ByteString identifier = (ByteString) value.getIdentifier();
buffer.writeByte(0x05);
buffer.writeShort(namespaceIndex);
encodeByteString(null, identifier);
} else {
throw new UaSerializationException(StatusCodes.Bad_EncodingError, "invalid identifier: " + value.getIdentifier());
}
}
@Override
public void encodeExpandedNodeId(String field, ExpandedNodeId value) throws UaSerializationException {
if (value == null) value = ExpandedNodeId.NULL_VALUE;
int flags = 0;
String namespaceUri = value.getNamespaceUri();
long serverIndex = value.getServerIndex();
if (namespaceUri != null && namespaceUri.length() > 0) {
flags |= 0x80;
}
if (serverIndex > 0) {
flags |= 0x40;
}
int namespaceIndex = value.getNamespaceIndex().intValue();
if (value.getType() == IdType.Numeric) {
UInteger identifier = (UInteger) value.getIdentifier();
long idv = identifier.longValue();
if (namespaceIndex == 0 && idv >= 0 && idv <= 255) {
/* Two-byte format */
buffer.writeByte(flags);
buffer.writeByte((int) idv);
} else if (namespaceIndex >= 0 && namespaceIndex <= 255 && idv <= 65535) {
/* Four-byte format */
buffer.writeByte(0x01 | flags);
buffer.writeByte(namespaceIndex);
buffer.writeShort((int) idv);
} else {
/* Numeric format */
buffer.writeByte(0x02 | flags);
buffer.writeShort(namespaceIndex);
buffer.writeInt((int) idv);
}
} else if (value.getType() == IdType.String) {
String identifier = (String) value.getIdentifier();
buffer.writeByte(0x03 | flags);
buffer.writeShort(namespaceIndex);
encodeString(null, identifier);
} else if (value.getType() == IdType.Guid) {
UUID identifier = (UUID) value.getIdentifier();
buffer.writeByte(0x04 | flags);
buffer.writeShort(namespaceIndex);
encodeGuid(null, identifier);
} else if (value.getType() == IdType.Opaque) {
ByteString identifier = (ByteString) value.getIdentifier();
buffer.writeByte(0x05 | flags);
buffer.writeShort(namespaceIndex);
encodeByteString(null, identifier);
} else {
throw new UaSerializationException(StatusCodes.Bad_EncodingError, "invalid identifier: " + value.getIdentifier());
}
if (namespaceUri != null && namespaceUri.length() > 0) {
encodeString(null, namespaceUri);
}
if (serverIndex > 0) {
encodeUInt32(null, Unsigned.uint(serverIndex));
}
}
@Override
public void encodeStatusCode(String field, StatusCode value) {
if (value == null) {
buffer.writeInt(0);
} else {
encodeUInt32(null, Unsigned.uint(value.getValue()));
}
}
@Override
public void encodeQualifiedName(String field, QualifiedName value) throws UaSerializationException {
if (value == null) value = QualifiedName.NULL_VALUE;
encodeUInt16(null, value.getNamespaceIndex());
encodeString(null, value.getName());
}
@Override
public void encodeLocalizedText(String field, LocalizedText value) throws UaSerializationException {
if (value == null) value = LocalizedText.NULL_VALUE;
String locale = value.getLocale();
String text = value.getText();
int mask = 1 | 2;
if (locale == null || locale.isEmpty()) {
mask ^= 1;
}
if (text == null || text.isEmpty()) {
mask ^= 2;
}
buffer.writeByte(mask);
if (locale != null && !locale.isEmpty()) {
encodeString(null, locale);
}
if (text != null && !text.isEmpty()) {
encodeString(null, text);
}
}
@Override
public void encodeExtensionObject(String field, ExtensionObject value) throws UaSerializationException {
if (value == null || value.getEncoded() == null) {
encodeNodeId(null, NodeId.NULL_VALUE);
buffer.writeByte(0); // No body is encoded
} else {
Object object = value.getEncoded();
switch (value.getBodyType()) {
case ByteString: {
ByteString byteString = (ByteString) object;
encodeNodeId(null, value.getEncodingTypeId());
buffer.writeByte(1); // Body is binary encoded
encodeByteString(null, byteString);
break;
}
case XmlElement: {
XmlElement xmlElement = (XmlElement) object;
encodeNodeId(null, value.getEncodingTypeId());
buffer.writeByte(2);
encodeXmlElement(null, xmlElement);
break;
}
}
}
}
@Override
public void encodeDataValue(String field, DataValue value) throws UaSerializationException {
if (value == null) {
buffer.writeByte(0);
} else {
int mask = 0x00;
if (value.getValue() != null && value.getValue().isNotNull()) mask |= 0x01;
if (!StatusCode.GOOD.equals(value.getStatusCode())) mask |= 0x02;
if (!DateTime.MIN_VALUE.equals(value.getSourceTime())) mask |= 0x04;
if (!DateTime.MIN_VALUE.equals(value.getServerTime())) mask |= 0x08;
buffer.writeByte(mask);
if ((mask & 0x01) == 0x01) encodeVariant(null, value.getValue());
if ((mask & 0x02) == 0x02) encodeStatusCode(null, value.getStatusCode());
if ((mask & 0x04) == 0x04) encodeDateTime(null, value.getSourceTime());
if ((mask & 0x08) == 0x08) encodeDateTime(null, value.getServerTime());
}
}
@Override
public void encodeVariant(String field, Variant variant) throws UaSerializationException {
Object value = variant.getValue();
if (value == null) {
buffer.writeByte(0);
} else {
boolean structure = false;
boolean enumeration = false;
Class<?> valueClass = getClass(value);
if (UaStructure.class.isAssignableFrom(valueClass)) {
valueClass = ExtensionObject.class;
structure = true;
} else if (UaEnumeration.class.isAssignableFrom(valueClass)) {
valueClass = Integer.class;
enumeration = true;
}
int typeId = TypeUtil.getBuiltinTypeId(valueClass);
if (typeId == -1) {
LoggerFactory.getLogger(getClass())
.warn("Not a built-in type: {}", valueClass);
}
if (value.getClass().isArray()) {
int[] dimensions = ArrayUtil.getDimensions(value);
if (dimensions.length == 1) {
buffer.writeByte(typeId | 0x80);
int length = Array.getLength(value);
buffer.writeInt(length);
for (int i = 0; i < length; i++) {
Object o = Array.get(value, i);
encodeValue(o, typeId, structure, enumeration);
}
} else {
buffer.writeByte(typeId | 0xC0);
Object flattened = ArrayUtil.flatten(value);
int length = Array.getLength(flattened);
buffer.writeInt(length);
for (int i = 0; i < length; i++) {
Object o = Array.get(flattened, i);
encodeValue(o, typeId, structure, enumeration);
}
encodeInt32(null, dimensions.length);
for (int dimension : dimensions) {
encodeInt32(null, dimension);
}
}
} else {
buffer.writeByte(typeId);
encodeValue(value, typeId, structure, enumeration);
}
}
}
private void encodeValue(Object value, int typeId, boolean structure, boolean enumeration) {
if (structure) {
ExtensionObject extensionObject = ExtensionObject.encode((UaStructure) value);
encodeBuiltinType(typeId, extensionObject);
} else if (enumeration) {
encodeBuiltinType(typeId, ((UaEnumeration) value).getValue());
} else {
encodeBuiltinType(typeId, value);
}
}
private Class<?> getClass(@Nonnull Object o) {
if (o.getClass().isArray()) {
return ArrayUtil.getType(o);
} else {
return o.getClass();
}
}
@Override
public void encodeDiagnosticInfo(String field, DiagnosticInfo value) throws UaSerializationException {
if (value == null) {
buffer.writeByte(0);
} else {
int mask = 0x7F;
if (value.getSymbolicId() == -1) mask ^= 0x01;
if (value.getNamespaceUri() == -1) mask ^= 0x02;
if (value.getLocalizedText() == -1) mask ^= 0x04;
if (value.getLocale() == -1) mask ^= 0x08;
if (value.getAdditionalInfo() == null || value.getAdditionalInfo().isEmpty()) mask ^= 0x10;
if (value.getInnerStatusCode() == null) mask ^= 0x20;
if (value.getInnerDiagnosticInfo() == null) mask ^= 0x40;
buffer.writeByte(mask);
if ((mask & 0x01) == 0x01) encodeInt32(null, value.getSymbolicId());
if ((mask & 0x02) == 0x02) encodeInt32(null, value.getNamespaceUri());
if ((mask & 0x04) == 0x04) encodeInt32(null, value.getLocalizedText());
if ((mask & 0x08) == 0x08) encodeInt32(null, value.getLocale());
if ((mask & 0x10) == 0x10) encodeString(null, value.getAdditionalInfo());
if ((mask & 0x20) == 0x20) encodeStatusCode(null, value.getInnerStatusCode());
if ((mask & 0x40) == 0x40) encodeDiagnosticInfo(null, value.getInnerDiagnosticInfo());
}
}
@Override
public <T extends UaStructure> void encodeMessage(String field, T message) throws UaSerializationException {
EncoderDelegate<T> delegate = DelegateRegistry.getEncoder(message.getBinaryEncodingId());
encodeNodeId(null, message.getBinaryEncodingId());
delegate.encode(message, this);
}
@Override
public <T extends UaEnumeration> void encodeEnumeration(String field, T value) throws UaSerializationException {
if (value == null) {
encodeInt32(null, -1);
} else {
EncoderDelegate<T> delegate = DelegateRegistry.getEncoder(value);
delegate.encode(value, this);
}
}
@Override
public <T extends UaSerializable> void encodeSerializable(String field, T value) throws UaSerializationException {
EncoderDelegate<T> delegate = DelegateRegistry.getEncoder(value);
delegate.encode(value, this);
}
@Override
public <T> void encodeArray(String field, T[] values, BiConsumer<String, T> consumer) throws UaSerializationException {
if (values == null) {
buffer.writeInt(-1);
} else {
if (values.length > maxArrayLength) {
throw new UaSerializationException(StatusCodes.Bad_EncodingLimitsExceeded,
"max array length exceeded");
}
encodeInt32(null, values.length);
for (T t : values) {
consumer.accept(null, t);
}
}
}
private void encodeBuiltinType(int typeId, Object value) throws UaSerializationException {
switch (typeId) {
case 1:
encodeBoolean(null, (Boolean) value);
break;
case 2:
encodeSByte(null, (Byte) value);
break;
case 3:
encodeByte(null, (UByte) value);
break;
case 4:
encodeInt16(null, (Short) value);
break;
case 5:
encodeUInt16(null, (UShort) value);
break;
case 6:
encodeInt32(null, (Integer) value);
break;
case 7:
encodeUInt32(null, (UInteger) value);
break;
case 8:
encodeInt64(null, (Long) value);
break;
case 9:
encodeUInt64(null, (ULong) value);
break;
case 10:
encodeFloat(null, (Float) value);
break;
case 11:
encodeDouble(null, (Double) value);
break;
case 12:
encodeString(null, (String) value);
break;
case 13:
encodeDateTime(null, (DateTime) value);
break;
case 14:
encodeGuid(null, (UUID) value);
break;
case 15:
encodeByteString(null, (ByteString) value);
break;
case 16:
encodeXmlElement(null, (XmlElement) value);
break;
case 17:
encodeNodeId(null, (NodeId) value);
break;
case 18:
encodeExpandedNodeId(null, (ExpandedNodeId) value);
break;
case 19:
encodeStatusCode(null, (StatusCode) value);
break;
case 20:
encodeQualifiedName(null, (QualifiedName) value);
break;
case 21:
encodeLocalizedText(null, (LocalizedText) value);
break;
case 22:
encodeExtensionObject(null, (ExtensionObject) value);
break;
case 23:
encodeDataValue(null, (DataValue) value);
break;
case 24:
encodeVariant(null, (Variant) value);
break;
case 25:
encodeDiagnosticInfo(null, (DiagnosticInfo) value);
break;
default:
throw new UaSerializationException(StatusCodes.Bad_DecodingError, "unknown builtin type: " + typeId);
}
}
}