/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 io.jafka.message;
import static java.lang.String.format;
import java.nio.ByteBuffer;
import io.jafka.api.ICalculable;
import io.jafka.common.UnknownMagicByteException;
import io.jafka.utils.Utils;
/**
* * A message. The format of an N byte message is the following:
*
* <p>
* magic byte is 1
*
* <pre>
* 1. 1 byte "magic" identifier to allow format changes
* 2. 1 byte "attributes" identifier to allow annotations on the message
* independent of the version (e.g. compression enabled, type of codec used)
* 3. 4 byte CRC32 of the payload
* 4. N - 6 byte payload
* </pre>
*
*
* @author adyliu (imxylz@gmail.com)
* @since 1.0
*/
public class Message implements ICalculable {
private static final byte MAGIC_VERSION2 = 1;
public static final byte CurrentMagicValue = 1;
public static final byte MAGIC_OFFSET = 0;
public static final byte MAGIC_LENGTH = 1;
public static final byte ATTRIBUTE_OFFSET = MAGIC_OFFSET + MAGIC_LENGTH;
public static final byte ATTRIBUT_ELENGTH = 1;
/**
* Specifies the mask for the compression code. 2 bits to hold the
* compression codec. 0 is reserved to indicate no compression
*/
public static final int CompressionCodeMask = 0x03; //
public static final int NoCompression = 0;
/**
* Computes the CRC value based on the magic byte
*
* @param magic Specifies the magic byte value. Possible values are 1
* (compression)
* @return crc value
*/
public static int crcOffset(byte magic) {
switch (magic) {
case MAGIC_VERSION2:
return ATTRIBUTE_OFFSET + ATTRIBUT_ELENGTH;
}
throw new UnknownMagicByteException(format("Magic byte value of %d is unknown", magic));
}
public static final byte CrcLength = 4;
/**
* Computes the offset to the message payload based on the magic byte
*
* @param magic Specifies the magic byte value. Possible values are 0
* and 1 0 for no compression 1 for compression
* @return offset data
*/
public static int payloadOffset(byte magic) {
return crcOffset(magic) + CrcLength;
}
/**
* Computes the size of the message header based on the magic byte
*
* @param magic Specifies the magic byte value. Possible values are 0
* and 1 0 for no compression 1 for compression
* @return size of header
*/
public static int headerSize(byte magic) {
return payloadOffset(magic);
}
/**
* Size of the header for magic byte 0. This is the minimum size of any
* message header
*/
public static final int MinHeaderSize = headerSize((byte) 1);
final ByteBuffer buffer;
private final int messageSize;
public Message(ByteBuffer buffer) {
this.buffer = buffer;
this.messageSize = buffer.limit();
}
public Message(long checksum, byte[] bytes, CompressionCodec compressionCodec) {
this(ByteBuffer.allocate(Message.headerSize(Message.CurrentMagicValue) + bytes.length));
buffer.put(CurrentMagicValue);
byte attributes = 0;
if (compressionCodec.codec > 0) {
attributes = (byte) (attributes | (CompressionCodeMask & compressionCodec.codec));
}
buffer.put(attributes);
Utils.putUnsignedInt(buffer, checksum);
buffer.put(bytes);
buffer.rewind();
}
public Message(long checksum, byte[] bytes) {
this(checksum, bytes, CompressionCodec.NoCompressionCodec);
}
public Message(byte[] bytes, CompressionCodec compressionCodec) {
this(Utils.crc32(bytes), bytes, compressionCodec);
}
/**
* create no compression message
*
* @param bytes message data
* @see CompressionCodec#NoCompressionCodec
*/
public Message(byte[] bytes) {
this(bytes, CompressionCodec.NoCompressionCodec);
}
//
public int getSizeInBytes() {
return messageSize;
}
/**
* magic code ( constant 1)
*
* @return 1
*/
public byte magic() {
return buffer.get(MAGIC_OFFSET);
}
public int payloadSize() {
return getSizeInBytes() - headerSize(magic());
}
public byte attributes() {
return buffer.get(ATTRIBUTE_OFFSET);
}
public CompressionCodec compressionCodec() {
byte magicByte = magic();
switch (magicByte) {
case 0:
return CompressionCodec.NoCompressionCodec;
case 1:
return CompressionCodec.valueOf(buffer.get(ATTRIBUTE_OFFSET) & CompressionCodeMask);
}
throw new RuntimeException("Invalid magic byte " + magicByte);
}
public long checksum() {
return Utils.getUnsignedInt(buffer, crcOffset(magic()));
}
/**
* get the real data without message header
* @return message data(without header)
*/
public ByteBuffer payload() {
ByteBuffer payload = buffer.duplicate();
payload.position(headerSize(magic()));
payload = payload.slice();
payload.limit(payloadSize());
payload.rewind();
return payload;
}
public boolean isValid() {
return checksum() == Utils.crc32(buffer.array(), buffer.position() + buffer.arrayOffset() + payloadOffset(magic()), payloadSize());
}
public int serializedSize() {
return 4 /* int size */+ buffer.limit();
}
public void serializeTo(ByteBuffer serBuffer) {
serBuffer.putInt(buffer.limit());
serBuffer.put(buffer.duplicate());
}
//
@Override
public String toString() {
return format("message(magic = %d, attributes = %d, crc = %d, payload = %s)",//
magic(), attributes(), checksum(), payload());
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Message) {
Message m = (Message) obj;
return getSizeInBytes() == m.getSizeInBytes()//
&& attributes() == m.attributes()//
&& checksum() == m.checksum()//
&& payload() == m.payload()//
&& magic() == m.magic();
}
return false;
}
@Override
public int hashCode() {
return buffer.hashCode();
}
}