package com.linkedin.databus.core;
/*
* Copyright 2013 LinkedIn Corp. All rights reserved
*
* 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.
*/
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.apache.commons.codec.binary.Hex;
import org.codehaus.jackson.JsonGenerator;
import com.linkedin.databus.core.DbusEvent.SchemaDigestType;
import com.linkedin.databus.core.util.Base64;
import com.linkedin.databus.core.util.Utils;
/**
* This class holds fields from a portion of the DbusEvent.
* DbusEventV2 carries key, metadata, and payload components, each having
* the following attributes:
* <pre>
* - Schema digest type (MD5 or CRC-32)
* - Schema digest
* - Schema version
* - Data blob (meaning the actual key, metadata, or payload)
* </pre>
* See wiki: Databus+Event+enhancement+%28Multi-Colo%29 for event layout.
*/
public class DbusEventPart
{
private static final short SCHEMA_DIGEST_TYPE_MD5 = 0;
private static final short SCHEMA_DIGEST_TYPE_CRC32 = 1;
private static final short VERSION_SHIFT = 2;
private static final short DIGEST_MASK = 0x3;
private static final int AttributesOffset = 4;
private static final int AttributesLen = 2;
private static final int MAX_DATA_BYTES_PRINTED = 64;
private final SchemaDigestType _schemaDigestType;
private final byte[] _schemaDigest;
private final short _schemaVersion;
private ByteBuffer _data = null;
public DbusEventPart(SchemaDigestType schemaDigestType, byte[] schemaDigest, short schemaVersion, ByteBuffer data)
{
_schemaDigestType = schemaDigestType;
_schemaDigest = schemaDigest.clone();
_schemaVersion = schemaVersion;
_data = data;
switch(_schemaDigestType)
{
case MD5:
if (_schemaDigest.length != DbusEvent.MD5_DIGEST_LEN)
{
throw new DatabusRuntimeException("Invalid MD5 schema digest length:" + _schemaDigest.length);
}
break;
case CRC32:
if (_schemaDigest.length != DbusEvent.CRC32_DIGEST_LEN)
{
throw new DatabusRuntimeException("Invalid CRC-32 schema digest length:" + _schemaDigest.length);
}
break;
default:
throw new UnsupportedOperationException("Unsupported schema digest type:" + _schemaDigestType);
}
}
public SchemaDigestType getSchemaDigestType()
{
return _schemaDigestType;
}
public byte[] getSchemaDigest()
{
return _schemaDigest.clone();
}
public short getSchemaVersion()
{
return _schemaVersion;
}
/**
* Returns the actual data blob of the event-part. The returned ByteBuffer is
* read-only, and its position and limit may be freely modified.
* <b>NOTE: The data may be subsequently overwritten; if you need it beyond the
* onDataEvent() call, save your own copy before returning.</b>
*/
public ByteBuffer getData()
{
return _data.asReadOnlyBuffer().slice().order(_data.order());
}
/** Returns number of bytes remaining. */ // TODO: make this public?
private int getDataLength()
{
return _data.limit() - _data.position(); // why not just _data.remaining() ? ("elements" here are always bytes)
}
public void encode(ByteBuffer buf)
{
int curPos = _data.position();
buf.putInt(getDataLength());
short attributes = 0;
switch (_schemaDigestType)
{
case MD5:
attributes = SCHEMA_DIGEST_TYPE_MD5;
break;
case CRC32:
attributes = SCHEMA_DIGEST_TYPE_CRC32;
break;
default:
throw new UnsupportedOperationException("Unsupported schema digest type:" + _schemaDigestType);
}
attributes |= (_schemaVersion << VERSION_SHIFT);
buf.putShort(attributes);
buf.put(_schemaDigest);
buf.put(_data);
_data.position(curPos);
}
private static SchemaDigestType digestType(short attributes)
{
short digestType = (short)(attributes & DIGEST_MASK);
switch (digestType)
{
case SCHEMA_DIGEST_TYPE_CRC32:
return SchemaDigestType.CRC32;
case SCHEMA_DIGEST_TYPE_MD5:
return SchemaDigestType.MD5;
default:
throw new UnsupportedOperationException("Digest type " + digestType + " not supported");
}
}
private static int digestLen(SchemaDigestType digestType)
{
switch (digestType)
{
case MD5:
return DbusEvent.MD5_DIGEST_LEN;
case CRC32:
return DbusEvent.CRC32_DIGEST_LEN;
default:
throw new UnsupportedOperationException("Digest type " + digestType + " not supported");
}
}
/**
* Decodes a bytebuffer and returns DbusEventPart. Preserves the ByteBuffer position.
* @param buf
* @return
*/
public static DbusEventPart decode(ByteBuffer buf)
{
int pos = buf.position();
int dataLen = buf.getInt(pos);
if (dataLen < 0)
{
throw new UnsupportedOperationException("Data length " + dataLen + " not supported");
}
short attributes = buf.getShort(pos+AttributesOffset);
short schemaVersion = (short)(attributes >> VERSION_SHIFT);
SchemaDigestType schemaDigestType = digestType(attributes);
int digestLen = digestLen(schemaDigestType);
byte[] digest = new byte[digestLen];
for (int i = 0; i < digestLen; i++)
{
digest[i] = buf.get(pos+AttributesOffset+AttributesLen+i);
}
// NOTE - this will create a new ByteBuffer object pointing to the
// same memory. So the position of this new BB is beginning of the data
// and limit is set to the end of the data.
ByteBuffer dataBuf = buf.asReadOnlyBuffer();
dataBuf.position(pos + AttributesOffset + AttributesLen + digestLen);
dataBuf.limit(dataBuf.position() + dataLen);
return new DbusEventPart(schemaDigestType, digest, schemaVersion, dataBuf);
}
public static int computePartLength(SchemaDigestType digestType, int dataLen)
{
return AttributesOffset+AttributesLen + dataLen + digestLen(digestType);
}
/**
* Replace the schema-digest in a serialized DbusEventPart.
*
* @param buf The buffer that contains the serialized DbusEventPart.
* @param position the position in the buffer where the DbusEventPart starts.
* @param schemaDigest The digest value to substitute. The value must match in length to the existing value.
*/
public static void replaceSchemaDigest(ByteBuffer buf, int position, byte[] schemaDigest)
{
DbusEvent.SchemaDigestType digestType = digestType(buf.getShort(position+AttributesOffset));
int digestLen = digestLen(digestType);
if (schemaDigest.length != digestLen)
{
throw new RuntimeException("Expecting length " + digestLen + " for type " + digestType + ", found " + schemaDigest.length);
}
for (int i = 0; i < digestLen; i++)
{
buf.put(position+AttributesOffset+AttributesLen+i, schemaDigest[i]);
}
}
public void printString (String prefix, JsonGenerator g, Encoding encoding ) throws IOException
{
g.writeNumberField(prefix + "Length", getDataLength());
g.writeNumberField(prefix + "SchemaVersion", getSchemaVersion());
//This is really the payload schema digest, but for legacy reasons we have to call it schemaid
g.writeStringField(prefix + "SchemaId", Base64.encodeBytes(getSchemaDigest()));
//This is really the payload but for historical reasons we have to call it value
if (encoding.equals(Encoding.JSON))
{
g.writeStringField(prefix + "Value", Base64.encodeBytes(Utils.byteBufferToBytes(getData())));
}
else
{
g.writeStringField(prefix + "Value", Utils.byteBufferToString(getData()));
}
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder(64);
sb.append("Length=")
.append(getDataLength())
.append(";SchemaVersion=")
.append(getSchemaVersion())
.append(";SchemaId=")
.append("0x")
.append(Hex.encodeHexString(getSchemaDigest()));
ByteBuffer dataBB = getData();
if (dataBB.remaining() > MAX_DATA_BYTES_PRINTED)
{
dataBB.limit(MAX_DATA_BYTES_PRINTED);
}
byte[] data = new byte[dataBB.remaining()];
dataBB.get(data);
sb.append(";Value=")
.append("0x")
.append(Hex.encodeHexString(data));
return sb.toString();
}
/**
* @return The number of bytes that this DbusEventPart would take up, if it were to be serialized.
*/
public int computePartLength()
{
return AttributesOffset+AttributesLen + _data.remaining() + _schemaDigest.length;
}
/**
* @return the length of the DbusEventPart that is encoded in 'buf' at position 'position'.
* Callers can use this method to advance across the DbusEventPart in a serialized V2 event.
*/
public static int partLength(ByteBuffer buf, int position)
{
DbusEvent.SchemaDigestType digestType = digestType(buf.getShort(position+AttributesOffset));
int digestLen = digestLen(digestType);
return AttributesOffset + AttributesLen + digestLen + buf.getInt(position);
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
DbusEventPart other = (DbusEventPart)obj;
if (_schemaVersion != other._schemaVersion)
{
return false;
}
if (_schemaDigestType != other._schemaDigestType)
{
return false;
}
if (_schemaDigest == null && other._schemaDigest != null ||
_schemaDigest != null && other._schemaDigest == null)
{
return false;
}
if (!Arrays.equals(_schemaDigest, other._schemaDigest))
{
return false;
}
if (_data == null && other._data != null ||
_data != null && other._data == null)
{
return false;
}
if (!_data.equals(other._data))
{
return false;
}
return true;
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + (_schemaDigest == null ? 0 : Arrays.hashCode(_schemaDigest));
result = prime * result + (_data == null ? 0 : _data.hashCode());
return result;
}
}