001/** 002 * 003 * Copyright 2018 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smack.compression.zlib; 018 019import java.io.IOException; 020import java.nio.ByteBuffer; 021import java.util.zip.DataFormatException; 022import java.util.zip.Deflater; 023import java.util.zip.Inflater; 024 025import org.jivesoftware.smack.ConnectionConfiguration; 026import org.jivesoftware.smack.XmppInputOutputFilter; 027import org.jivesoftware.smack.compression.XMPPInputOutputStream; 028import org.jivesoftware.smack.compression.XMPPInputOutputStream.FlushMethod; 029import org.jivesoftware.smack.compression.XmppCompressionFactory; 030 031public final class ZlibXmppCompressionFactory extends XmppCompressionFactory { 032 033 public static final ZlibXmppCompressionFactory INSTANCE = new ZlibXmppCompressionFactory(); 034 035 private ZlibXmppCompressionFactory() { 036 super("zlib", 100); 037 } 038 039 @Override 040 public XmppInputOutputFilter fabricate(ConnectionConfiguration configuration) { 041 return new ZlibXmppInputOutputFilter(); 042 } 043 044 private static final class ZlibXmppInputOutputFilter implements XmppInputOutputFilter { 045 046 private static final int MINIMUM_OUTPUT_BUFFER_INITIAL_SIZE = 4; 047 private static final int MINIMUM_OUTPUT_BUFFER_INCREASE = 480; 048 049 private final Deflater compressor; 050 private final Inflater decompressor = new Inflater(); 051 052 private long compressorInBytes; 053 private long compressorOutBytes; 054 055 private long decompressorInBytes; 056 private long decompressorOutBytes; 057 058 private int maxOutputOutput = -1; 059 private int maxInputOutput = -1; 060 061 private int maxBytesWrittenAfterFullFlush = -1; 062 063 private ZlibXmppInputOutputFilter() { 064 this(Deflater.DEFAULT_COMPRESSION); 065 } 066 067 private ZlibXmppInputOutputFilter(int compressionLevel) { 068 compressor = new Deflater(compressionLevel); 069 } 070 071 private ByteBuffer outputBuffer; 072 073 @Override 074 public OutputResult output(ByteBuffer outputData, boolean isFinalDataOfElement, boolean destinationAddressChanged, 075 boolean moreDataAvailable) throws IOException { 076 if (destinationAddressChanged && XMPPInputOutputStream.getFlushMethod() == FlushMethod.FULL_FLUSH) { 077 outputBuffer = ByteBuffer.allocate(256); 078 079 int bytesWritten = deflate(Deflater.FULL_FLUSH); 080 081 maxBytesWrittenAfterFullFlush = Math.max(bytesWritten, maxBytesWrittenAfterFullFlush); 082 compressorOutBytes += bytesWritten; 083 } 084 085 if (outputData == null && outputBuffer == null) { 086 return OutputResult.NO_OUTPUT; 087 } 088 089 int bytesRemaining = outputData.remaining(); 090 if (outputBuffer == null) { 091 final int outputBufferSize = bytesRemaining < MINIMUM_OUTPUT_BUFFER_INITIAL_SIZE ? MINIMUM_OUTPUT_BUFFER_INITIAL_SIZE : bytesRemaining; 092 // We assume that the compressed data will not take more space as the uncompressed. Even if this is not 093 // always true, the automatic buffer resize mechanism of deflate() will take care. 094 outputBuffer = ByteBuffer.allocate(outputBufferSize); 095 } 096 097 // There is an invariant of Deflater/Inflater that input should only be set if needsInput() return true. 098 assert compressor.needsInput(); 099 100 final byte[] compressorInputBuffer; 101 final int compressorInputBufferOffset, compressorInputBufferLength; 102 if (outputData.hasArray()) { 103 compressorInputBuffer = outputData.array(); 104 compressorInputBufferOffset = outputData.arrayOffset(); 105 compressorInputBufferLength = outputData.remaining(); 106 } else { 107 compressorInputBuffer = new byte[outputData.remaining()]; 108 compressorInputBufferOffset = 0; 109 compressorInputBufferLength = compressorInputBuffer.length; 110 outputData.get(compressorInputBuffer); 111 } 112 113 compressorInBytes += compressorInputBufferLength; 114 115 compressor.setInput(compressorInputBuffer, compressorInputBufferOffset, compressorInputBufferLength); 116 117 int flushMode; 118 if (moreDataAvailable) { 119 flushMode = Deflater.NO_FLUSH; 120 } else { 121 flushMode = Deflater.SYNC_FLUSH; 122 } 123 124 int bytesWritten = deflate(flushMode); 125 126 maxOutputOutput = Math.max(outputBuffer.position(), maxOutputOutput); 127 compressorOutBytes += bytesWritten; 128 129 OutputResult outputResult = new OutputResult(outputBuffer); 130 outputBuffer = null; 131 return outputResult; 132 } 133 134 private int deflate(int flushMode) { 135// compressor.finish(); 136 137 int totalBytesWritten = 0; 138 while (true) { 139 int initialOutputBufferPosition = outputBuffer.position(); 140 byte[] buffer = outputBuffer.array(); 141 int length = outputBuffer.limit() - initialOutputBufferPosition; 142 143 int bytesWritten = compressor.deflate(buffer, initialOutputBufferPosition, length, flushMode); 144 145 int newOutputBufferPosition = initialOutputBufferPosition + bytesWritten; 146 outputBuffer.position(newOutputBufferPosition); 147 148 totalBytesWritten += bytesWritten; 149 150 if (compressor.needsInput() && outputBuffer.hasRemaining()) { 151 break; 152 } 153 154 int increasedBufferSize = outputBuffer.capacity() * 2; 155 if (increasedBufferSize < MINIMUM_OUTPUT_BUFFER_INCREASE) { 156 increasedBufferSize = MINIMUM_OUTPUT_BUFFER_INCREASE; 157 } 158 ByteBuffer newCurrentOutputBuffer = ByteBuffer.allocate(increasedBufferSize); 159 outputBuffer.flip(); 160 newCurrentOutputBuffer.put(outputBuffer); 161 outputBuffer = newCurrentOutputBuffer; 162 } 163 164 return totalBytesWritten; 165 } 166 167 @Override 168 public ByteBuffer input(ByteBuffer inputData) throws IOException { 169 int bytesRemaining = inputData.remaining(); 170 171 final byte[] inputBytes; 172 final int offset, length; 173 if (inputData.hasArray()) { 174 inputBytes = inputData.array(); 175 offset = inputData.arrayOffset(); 176 length = inputData.remaining(); 177 } else { 178 // Copy since we are dealing with a buffer whose array is not accessible (possibly a direct buffer). 179 inputBytes = new byte[bytesRemaining]; 180 inputData.get(inputBytes); 181 offset = 0; 182 length = inputBytes.length; 183 } 184 185 decompressorInBytes += length; 186 187 decompressor.setInput(inputBytes, offset, length); 188 189 int bytesInflated; 190 // Assume that the inflated/decompressed result will be roughly at most twice the size of the compressed 191 // variant. It appears to hold most of the times, if not, then the buffer resize mechanism will take care of 192 // it. 193 ByteBuffer outputBuffer = ByteBuffer.allocate(2 * length); 194 while (true) { 195 byte[] inflateOutputBuffer = outputBuffer.array(); 196 int inflateOutputBufferOffset = outputBuffer.position(); 197 int inflateOutputBufferLength = outputBuffer.limit() - inflateOutputBufferOffset; 198 try { 199 bytesInflated = decompressor.inflate(inflateOutputBuffer, inflateOutputBufferOffset, inflateOutputBufferLength); 200 } 201 catch (DataFormatException e) { 202 throw new IOException(e); 203 } 204 205 outputBuffer.position(inflateOutputBufferOffset + bytesInflated); 206 207 decompressorOutBytes += bytesInflated; 208 209 if (decompressor.needsInput()) { 210 break; 211 } 212 213 int increasedBufferSize = outputBuffer.capacity() * 2; 214 ByteBuffer increasedOutputBuffer = ByteBuffer.allocate(increasedBufferSize); 215 outputBuffer.flip(); 216 increasedOutputBuffer.put(outputBuffer); 217 outputBuffer = increasedOutputBuffer; 218 } 219 220 if (bytesInflated == 0) { 221 return null; 222 } 223 224 maxInputOutput = Math.max(outputBuffer.position(), maxInputOutput); 225 226 return outputBuffer; 227 } 228 229 @Override 230 public Stats getStats() { 231 return new Stats(this); 232 } 233 234 @Override 235 public String getFilterName() { 236 return "Compression (zlib)"; 237 } 238 } 239 240 public static final class Stats { 241 public final long compressorInBytes; 242 public final long compressorOutBytes; 243 public final double compressionRatio; 244 245 public final long decompressorInBytes; 246 public final long decompressorOutBytes; 247 public final double decompressionRatio; 248 249 public final int maxOutputOutput; 250 public final int maxInputOutput; 251 252 public final int maxBytesWrittenAfterFullFlush; 253 254 private Stats(ZlibXmppInputOutputFilter filter) { 255 // Note that we read the out bytes before the in bytes to not over approximate the compression ratio. 256 compressorOutBytes = filter.compressorOutBytes; 257 compressorInBytes = filter.compressorInBytes; 258 compressionRatio = (double) compressorOutBytes / compressorInBytes; 259 260 decompressorOutBytes = filter.decompressorOutBytes; 261 decompressorInBytes = filter.decompressorInBytes; 262 decompressionRatio = (double) decompressorInBytes / decompressorOutBytes; 263 264 maxOutputOutput = filter.maxOutputOutput; 265 maxInputOutput = filter.maxInputOutput; 266 maxBytesWrittenAfterFullFlush = filter.maxBytesWrittenAfterFullFlush; 267 } 268 269 private transient String toStringCache; 270 271 @Override 272 public String toString() { 273 if (toStringCache != null) { 274 return toStringCache; 275 } 276 277 toStringCache = 278 "compressor-in-bytes: " + compressorInBytes + '\n' 279 + "compressor-out-bytes: " + compressorOutBytes + '\n' 280 + "compression-ratio: " + compressionRatio + '\n' 281 + "decompressor-in-bytes: " + decompressorInBytes + '\n' 282 + "decompressor-out-bytes: " + decompressorOutBytes + '\n' 283 + "decompression-ratio: " + decompressionRatio + '\n' 284 + "max-output-output: " + maxOutputOutput + '\n' 285 + "max-input-output: " + maxInputOutput + '\n' 286 + "max-bytes-written-after-full-flush: " + maxBytesWrittenAfterFullFlush + '\n' 287 ; 288 289 return toStringCache; 290 } 291 } 292}