Smack/smack-core/src/main/java/org/jivesoftware/smack/compression/zlib/ZlibXmppCompressionFactory....

288 lines
11 KiB
Java

/**
*
* Copyright 2018 Florian Schmaus
*
* 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 org.jivesoftware.smack.compression.zlib;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.XmppInputOutputFilter;
import org.jivesoftware.smack.compression.XMPPInputOutputStream;
import org.jivesoftware.smack.compression.XMPPInputOutputStream.FlushMethod;
import org.jivesoftware.smack.compression.XmppCompressionFactory;
public final class ZlibXmppCompressionFactory extends XmppCompressionFactory {
public static final ZlibXmppCompressionFactory INSTANCE = new ZlibXmppCompressionFactory();
private ZlibXmppCompressionFactory() {
super("zlib", 100);
}
@Override
public XmppInputOutputFilter fabricate(ConnectionConfiguration configuration) {
return new ZlibXmppInputOutputFilter();
}
private static final class ZlibXmppInputOutputFilter implements XmppInputOutputFilter {
private static final int MINIMUM_OUTPUT_BUFFER_INITIAL_SIZE = 4;
private static final int MINIMUM_OUTPUT_BUFFER_INCREASE = 480;
private final Deflater compressor;
private final Inflater decompressor = new Inflater();
private long compressorInBytes;
private long compressorOutBytes;
private long decompressorInBytes;
private long decompressorOutBytes;
private int maxOutputOutput = -1;
private int maxInputOutput = -1;
private int maxBytesWrittenAfterFullFlush = -1;
private ZlibXmppInputOutputFilter() {
this(Deflater.DEFAULT_COMPRESSION);
}
private ZlibXmppInputOutputFilter(int compressionLevel) {
compressor = new Deflater(compressionLevel);
}
private ByteBuffer outputBuffer;
@Override
public OutputResult output(ByteBuffer outputData, boolean isFinalDataOfElement, boolean destinationAddressChanged,
boolean moreDataAvailable) throws IOException {
if (destinationAddressChanged && XMPPInputOutputStream.getFlushMethod() == FlushMethod.FULL_FLUSH) {
outputBuffer = ByteBuffer.allocate(256);
int bytesWritten = deflate(Deflater.FULL_FLUSH);
maxBytesWrittenAfterFullFlush = Math.max(bytesWritten, maxBytesWrittenAfterFullFlush);
compressorOutBytes += bytesWritten;
}
if (outputData == null && outputBuffer == null) {
return OutputResult.NO_OUTPUT;
}
int bytesRemaining = outputData.remaining();
if (outputBuffer == null) {
final int outputBufferSize = bytesRemaining < MINIMUM_OUTPUT_BUFFER_INITIAL_SIZE ? MINIMUM_OUTPUT_BUFFER_INITIAL_SIZE : bytesRemaining;
// We assume that the compressed data will not take more space as the uncompressed. Even if this is not
// always true, the automatic buffer resize mechanism of deflate() will take care.
outputBuffer = ByteBuffer.allocate(outputBufferSize);
}
// There is an invariant of Deflater/Inflater that input should only be set if needsInput() return true.
assert compressor.needsInput();
final byte[] compressorInputBuffer;
final int compressorInputBufferOffset, compressorInputBufferLength;
if (outputData.hasArray()) {
compressorInputBuffer = outputData.array();
compressorInputBufferOffset = outputData.arrayOffset();
compressorInputBufferLength = outputData.remaining();
} else {
compressorInputBuffer = new byte[outputData.remaining()];
compressorInputBufferOffset = 0;
compressorInputBufferLength = compressorInputBuffer.length;
outputData.get(compressorInputBuffer);
}
compressorInBytes += compressorInputBufferLength;
compressor.setInput(compressorInputBuffer, compressorInputBufferOffset, compressorInputBufferLength);
int flushMode;
if (moreDataAvailable) {
flushMode = Deflater.NO_FLUSH;
} else {
flushMode = Deflater.SYNC_FLUSH;
}
int bytesWritten = deflate(flushMode);
maxOutputOutput = Math.max(outputBuffer.position(), maxOutputOutput);
compressorOutBytes += bytesWritten;
OutputResult outputResult = new OutputResult(outputBuffer);
outputBuffer = null;
return outputResult;
}
private int deflate(int flushMode) {
// compressor.finish();
int totalBytesWritten = 0;
while (true) {
int initialOutputBufferPosition = outputBuffer.position();
byte[] buffer = outputBuffer.array();
int length = outputBuffer.limit() - initialOutputBufferPosition;
int bytesWritten = compressor.deflate(buffer, initialOutputBufferPosition, length, flushMode);
int newOutputBufferPosition = initialOutputBufferPosition + bytesWritten;
outputBuffer.position(newOutputBufferPosition);
totalBytesWritten += bytesWritten;
if (compressor.needsInput() && outputBuffer.hasRemaining()) {
break;
}
int increasedBufferSize = outputBuffer.capacity() * 2;
if (increasedBufferSize < MINIMUM_OUTPUT_BUFFER_INCREASE) {
increasedBufferSize = MINIMUM_OUTPUT_BUFFER_INCREASE;
}
ByteBuffer newCurrentOutputBuffer = ByteBuffer.allocate(increasedBufferSize);
outputBuffer.flip();
newCurrentOutputBuffer.put(outputBuffer);
outputBuffer = newCurrentOutputBuffer;
}
return totalBytesWritten;
}
@Override
public ByteBuffer input(ByteBuffer inputData) throws IOException {
int bytesRemaining = inputData.remaining();
final byte[] inputBytes;
final int offset, length;
if (inputData.hasArray()) {
inputBytes = inputData.array();
offset = inputData.arrayOffset();
length = inputData.remaining();
} else {
// Copy since we are dealing with a buffer whose array is not accessible (possibly a direct buffer).
inputBytes = new byte[bytesRemaining];
inputData.get(inputBytes);
offset = 0;
length = inputBytes.length;
}
decompressorInBytes += length;
decompressor.setInput(inputBytes, offset, length);
int bytesInflated;
// Assume that the inflated/decompressed result will be roughly at most twice the size of the compressed
// variant. It appears to hold most of the times, if not, then the buffer resize mechanism will take care of
// it.
ByteBuffer outputBuffer = ByteBuffer.allocate(2 * length);
while (true) {
byte[] inflateOutputBuffer = outputBuffer.array();
int inflateOutputBufferOffset = outputBuffer.position();
int inflateOutputBufferLength = outputBuffer.limit() - inflateOutputBufferOffset;
try {
bytesInflated = decompressor.inflate(inflateOutputBuffer, inflateOutputBufferOffset, inflateOutputBufferLength);
}
catch (DataFormatException e) {
throw new IOException(e);
}
outputBuffer.position(inflateOutputBufferOffset + bytesInflated);
decompressorOutBytes += bytesInflated;
if (decompressor.needsInput()) {
break;
}
int increasedBufferSize = outputBuffer.capacity() * 2;
ByteBuffer increasedOutputBuffer = ByteBuffer.allocate(increasedBufferSize);
outputBuffer.flip();
increasedOutputBuffer.put(outputBuffer);
outputBuffer = increasedOutputBuffer;
}
if (bytesInflated == 0) {
return null;
}
maxInputOutput = Math.max(outputBuffer.position(), maxInputOutput);
return outputBuffer;
}
@Override
public Stats getStats() {
return new Stats(this);
}
}
public static final class Stats {
public final long compressorInBytes;
public final long compressorOutBytes;
public final double compressionRatio;
public final long decompressorInBytes;
public final long decompressorOutBytes;
public final double decompressionRatio;
public final int maxOutputOutput;
public final int maxInputOutput;
public final int maxBytesWrittenAfterFullFlush;
private Stats(ZlibXmppInputOutputFilter filter) {
// Note that we read the out bytes before the in bytes to not over approximate the compression ratio.
compressorOutBytes = filter.compressorOutBytes;
compressorInBytes = filter.compressorInBytes;
compressionRatio = (double) compressorOutBytes / compressorInBytes;
decompressorOutBytes = filter.decompressorOutBytes;
decompressorInBytes = filter.decompressorInBytes;
decompressionRatio = (double) decompressorInBytes / decompressorOutBytes;
maxOutputOutput = filter.maxOutputOutput;
maxInputOutput = filter.maxInputOutput;
maxBytesWrittenAfterFullFlush = filter.maxBytesWrittenAfterFullFlush;
}
private transient String toStringCache;
@Override
public String toString() {
if (toStringCache != null) {
return toStringCache;
}
toStringCache =
"compressor-in-bytes: " + compressorInBytes + '\n'
+ "compressor-out-bytes: " + compressorOutBytes + '\n'
+ "compression-ratio: " + compressionRatio + '\n'
+ "decompressor-in-bytes: " + decompressorInBytes + '\n'
+ "decompressor-out-bytes: " + decompressorOutBytes + '\n'
+ "decompression-ratio: " + decompressionRatio + '\n'
+ "max-output-output: " + maxOutputOutput + '\n'
+ "max-input-output: " + maxInputOutput + '\n'
+ "max-bytes-written-after-full-flush: " + maxBytesWrittenAfterFullFlush + '\n'
;
return toStringCache;
}
}
}