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}