/**
* Copyright 2014 Ricardo Padilha
*
* 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 net.dsys.snio.impl.codec;
import java.nio.ByteBuffer;
import javax.annotation.Nonnegative;
import net.dsys.commons.api.exception.Bug;
import net.dsys.snio.api.codec.InvalidEncodingException;
import net.dsys.snio.api.codec.InvalidLengthException;
import net.dsys.snio.api.codec.InvalidMessageException;
import net.dsys.snio.api.codec.MessageCodec;
import net.jpountz.lz4.LZ4Compressor;
import net.jpountz.lz4.LZ4Exception;
import net.jpountz.lz4.LZ4Factory;
import net.jpountz.lz4.LZ4FastDecompressor;
/**
* Frame encoding that compresses messages using LZ4. Messages cannot be longer
* than 65252 bytes to make sure that they will fit in an UDP datagram.
* Thread-safety is guaranteed only between encoding and decoding, i.e., two
* different threads can encode and decode at the same time, but two threads
* cannot encode at the same time.
*
* @author Ricardo Padilha
*/
final class LZ4CompressionCodec implements MessageCodec {
private static final int UNSIGNED_INT_MASK = Integer.MAX_VALUE;
private static final int INT_LENGTH = Integer.SIZE / Byte.SIZE;
private static final int HEADER_LENGTH = 2 * INT_LENGTH;
private static final int FOOTER_LENGTH = 0;
private static final int MAX_BODY_LENGTH = maxUncompressedLength(UNSIGNED_INT_MASK) - HEADER_LENGTH;
private final int headerLength;
private final int bodyLength;
private final int compressedLength;
private final int footerLength;
private final int frameLength;
private final LZ4Compressor compressor;
private final LZ4FastDecompressor decompressor;
private final byte[] compressInput;
private final byte[] compressOutput;
private final byte[] decompressInput;
private final byte[] decompressOutput;
LZ4CompressionCodec(@Nonnegative final int bodyLength) {
if (bodyLength < 1 || bodyLength > MAX_BODY_LENGTH) {
throw new IllegalArgumentException("bodyLength < 1 || bodyLength > 0xFEEC: " + bodyLength);
}
final LZ4Factory factory = LZ4Factory.fastestInstance();
this.compressor = factory.fastCompressor();
this.decompressor = factory.fastDecompressor();
this.bodyLength = bodyLength;
this.headerLength = HEADER_LENGTH;
this.compressedLength = compressor.maxCompressedLength(bodyLength);
this.footerLength = FOOTER_LENGTH;
this.frameLength = headerLength + compressedLength + footerLength;
this.compressInput = new byte[this.bodyLength];
this.compressOutput = new byte[compressedLength];
this.decompressInput = new byte[compressedLength];
this.decompressOutput = new byte[this.bodyLength];
}
private static int maxUncompressedLength(final int length) {
final LZ4Compressor c = LZ4Factory.fastestInstance().fastCompressor();
final int compressed = c.maxCompressedLength(length);
return Math.abs(compressed - length);
}
/**
* {@inheritDoc}
*/
@Override
public int getHeaderLength() {
return headerLength;
}
/**
* {@inheritDoc}
*/
@Override
public int getBodyLength() {
return bodyLength;
}
/**
* {@inheritDoc}
*/
@Override
public int getFooterLength() {
return footerLength;
}
/**
* {@inheritDoc}
*/
@Override
public int getFrameLength() {
return frameLength;
}
/**
* {@inheritDoc}
*/
@Override
public int getEncodedLength(final ByteBuffer in) {
return headerLength + compressor.maxCompressedLength(in.remaining());
}
/**
* {@inheritDoc}
*
* @throws InvalidLengthException
*/
@Override
public boolean isValid(final ByteBuffer out) {
final int length = out.remaining();
return length > 0 && length <= bodyLength;
}
/**
* {@inheritDoc}
*/
@Override
public void put(final ByteBuffer in, final ByteBuffer out) throws InvalidMessageException {
final int decompressed = in.remaining();
final int offsetIn;
final byte[] arrayIn;
if (in.hasArray()) {
offsetIn = in.arrayOffset() + in.position();
arrayIn = in.array();
in.position(in.position() + decompressed);
} else {
offsetIn = 0;
arrayIn = compressInput;
in.get(compressInput, 0, decompressed);
}
final int offsetOut;
final byte[] arrayOut;
if (out.hasArray()) {
offsetOut = out.arrayOffset() + out.position() + HEADER_LENGTH;
arrayOut = out.array();
} else {
offsetOut = 0;
arrayOut = compressOutput;
}
final int compressed;
try {
compressed = compressor.compress(arrayIn, offsetIn, decompressed, arrayOut, offsetOut);
if (compressed < 1 || compressed > compressedLength) {
throw new Bug("Unexpected compressed size: " + compressed);
}
} catch (final LZ4Exception e) {
throw new InvalidMessageException(e);
}
out.putInt(compressed + INT_LENGTH);
out.putInt(decompressed);
if (out.hasArray()) {
out.position(out.position() + compressed);
} else {
out.put(compressOutput, 0, compressed);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean hasNext(final ByteBuffer in) throws InvalidEncodingException {
final int rem = in.remaining();
if (rem < headerLength) {
return false;
}
final int compressed = (in.getInt(in.position()) & UNSIGNED_INT_MASK) - INT_LENGTH;
if (compressed < 1 || compressed > compressedLength) {
throw new InvalidLengthException(compressed);
}
final int decompressed = in.getInt(in.position() + INT_LENGTH) & UNSIGNED_INT_MASK;
if (decompressed < 1 || decompressed > bodyLength) {
throw new InvalidLengthException(decompressed);
}
return (rem >= headerLength + compressed);
}
/**
* {@inheritDoc}
*/
@Override
public int getDecodedLength(final ByteBuffer in) {
return in.getInt(in.position()) & UNSIGNED_INT_MASK;
}
/**
* {@inheritDoc}
*/
@Override
public void get(final ByteBuffer in, final ByteBuffer out) throws InvalidEncodingException {
final int compressed = (in.getInt() & UNSIGNED_INT_MASK) - INT_LENGTH;
final int decompressed = in.getInt() & UNSIGNED_INT_MASK;
final byte[] arrayIn;
final int offsetIn;
if (in.hasArray()) {
offsetIn = in.arrayOffset() + in.position();
arrayIn = in.array();
in.position(in.position() + compressed);
} else {
offsetIn = 0;
arrayIn = decompressInput;
in.get(decompressInput, 0, compressed);
}
final int offsetOut;
final byte[] arrayOut;
if (out.hasArray()) {
offsetOut = out.arrayOffset() + out.position();
arrayOut = out.array();
} else {
offsetOut = 0;
arrayOut = decompressOutput;
}
final int read;
try {
read = decompressor.decompress(arrayIn, offsetIn, arrayOut, offsetOut, decompressed);
} catch (final LZ4Exception e) {
throw new InvalidEncodingException(e);
}
if (read != compressed) {
throw new InvalidEncodingException("read != compressed");
}
if (out.hasArray()) {
out.position(out.position() + decompressed);
} else {
out.put(decompressOutput, 0, decompressed);
}
}
/**
* {@inheritDoc}
*/
@Override
public void close() {
return;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return "LZ4CompressionCodec(" + headerLength + ":" + bodyLength + ")";
}
}