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;
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.FlushMethod;
028
029public final class ZlibXmppCompressionFactory extends XmppCompressionFactory {
030
031    public static final ZlibXmppCompressionFactory INSTANCE = new ZlibXmppCompressionFactory();
032
033    private ZlibXmppCompressionFactory() {
034        super("zlib", 100);
035    }
036
037    @Override
038    public XmppInputOutputFilter fabricate(ConnectionConfiguration configuration) {
039        return new ZlibXmppInputOutputFilter();
040    }
041
042    private static final class ZlibXmppInputOutputFilter implements XmppInputOutputFilter {
043
044        private final Deflater compressor;
045        private final Inflater decompressor = new Inflater();
046
047        private int maxOutputOutput = -1;
048        private int maxInputOutput = -1;
049
050        private int maxBytesWrittenAfterFullFlush = -1;
051
052        private ZlibXmppInputOutputFilter() {
053            this(Deflater.DEFAULT_COMPRESSION);
054        }
055
056        private ZlibXmppInputOutputFilter(int compressionLevel) {
057            compressor = new Deflater(compressionLevel);
058        }
059
060        private ByteBuffer outputBuffer;
061
062        @Override
063        public OutputResult output(ByteBuffer outputData, boolean isFinalDataOfElement, boolean destinationAddressChanged,
064                        boolean moreDataAvailable) throws IOException {
065            if (destinationAddressChanged && XMPPInputOutputStream.getFlushMethod() == FlushMethod.FULL_FLUSH) {
066                outputBuffer = ByteBuffer.allocate(1024);
067                int bytesWritten = deflate(Deflater.FULL_FLUSH);
068                maxBytesWrittenAfterFullFlush = Math.max(bytesWritten, maxBytesWrittenAfterFullFlush);
069            }
070
071            if (outputData == null && outputBuffer == null) {
072                return OutputResult.NO_OUTPUT;
073            }
074
075            int bytesRemaining = outputData.remaining();
076            if (outputBuffer == null) {
077                // We assume that the compressed data will not take more space as the uncompressed. Even if this is not
078                // always true, the automatic buffer resize mechanism of deflate() will take care.
079                outputBuffer = ByteBuffer.allocate(bytesRemaining);
080            }
081
082            // There is an invariant of Deflater/Inflater that input should only be set if needsInput() return true.
083            assert (compressor.needsInput());
084            compressor.setInput(outputData.array(), outputData.position(), outputData.limit());
085
086            int flushMode;
087            if (moreDataAvailable) {
088                flushMode = Deflater.NO_FLUSH;
089            } else {
090                flushMode = Deflater.SYNC_FLUSH;
091            }
092
093            /*
094            long bytesReadBefore = compressor.getBytesRead();
095            */
096            /* int bytesWritten = */ deflate(flushMode);
097            /*
098            long bytesReadAfter = compressor.getBytesRead();
099            long bytesConsumed = bytesReadAfter - bytesReadBefore;
100            assert(bytesConsumed <= Integer.MAX_VALUE);
101
102            int newOutputDataPosition = outputData.position() + (int) bytesConsumed;
103            outputData.position(newOutputDataPosition);
104            */
105
106            maxOutputOutput = Math.max(outputBuffer.position(), maxOutputOutput);
107
108            OutputResult outputResult = new OutputResult(outputBuffer);
109            outputBuffer = null;
110            return outputResult;
111        }
112
113        private int deflate(int flushMode) {
114            int totalBytesWritten = 0;
115            while (true) {
116                int initialOutputBufferPosition = outputBuffer.position();
117
118                int bytesWritten = compressor.deflate(outputBuffer.array(), initialOutputBufferPosition,
119                                outputBuffer.limit(), flushMode);
120
121                int newOutputBufferPosition = initialOutputBufferPosition + bytesWritten;
122                outputBuffer.position(newOutputBufferPosition);
123
124                totalBytesWritten += bytesWritten;
125
126                if (compressor.needsInput()) {
127                    break;
128                }
129
130                int increasedBufferSize = outputBuffer.capacity() * 2;
131                ByteBuffer newCurrentOutputBuffer = ByteBuffer.allocate(increasedBufferSize);
132                outputBuffer.put(newCurrentOutputBuffer);
133                outputBuffer = newCurrentOutputBuffer;
134            }
135
136            return totalBytesWritten;
137        }
138
139        @Override
140        public ByteBuffer input(ByteBuffer inputData) throws IOException {
141            int bytesRemaining = inputData.remaining();
142
143            int offset, length;
144            byte[] inputBytes;
145            if (inputData.isDirect()) {
146                // Copy since we are dealing with a direct buffer.
147                inputBytes = new byte[bytesRemaining];
148                inputData.get(inputBytes);
149                offset = 0;
150                length = inputBytes.length;
151            } else {
152                inputBytes = inputData.array();
153                offset = inputData.position();
154                length = inputData.limit() - inputData.position();
155            }
156
157            decompressor.setInput(inputBytes, offset, length);
158
159            int bytesInflated;
160            ByteBuffer outputBuffer = ByteBuffer.allocate(bytesRemaining);
161            while (true) {
162                try {
163                    bytesInflated = decompressor.inflate(outputBuffer.array());
164                }
165                catch (DataFormatException e) {
166                    throw new IOException(e);
167                }
168                if (decompressor.needsInput()) {
169                    break;
170                }
171
172                int increasedBufferSize = outputBuffer.capacity() * 2;
173                ByteBuffer increasedOutputBuffer = ByteBuffer.allocate(increasedBufferSize);
174                increasedOutputBuffer.put(outputBuffer);
175                outputBuffer = increasedOutputBuffer;
176            }
177
178            if (bytesInflated == 0) {
179                return null;
180            }
181
182            maxInputOutput = Math.max(outputBuffer.position(), maxInputOutput);
183
184            return outputBuffer;
185        }
186
187    }
188}