summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/GzipNonChunkedCompressingEntity.java
blob: 1e238c022a4c544cb2653e8ef92bf962b82dab5d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.sync.net;

import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.client.entity.GzipCompressingEntity;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Wrapping entity that compresses content when {@link #writeTo writing}.
 *
 * This differs from {@link GzipCompressingEntity} in that it does not chunk
 * the sent data, therefore replacing the "Transfer-Encoding" HTTP header with
 * the "Content-Length" header required by some servers.
 *
 * However, to measure the content length, the gzipped content will be temporarily
 * stored in memory so be careful what content you send!
 */
public class GzipNonChunkedCompressingEntity extends GzipCompressingEntity {
    final int MAX_BUFFER_SIZE_BYTES = 10 * 1000 * 1000; // 10 MB.

    private byte[] gzippedContent;

    public GzipNonChunkedCompressingEntity(final HttpEntity entity) {
        super(entity);
    }

    /**
     * @return content length for gzipped content or -1 if there is an error
     */
    @Override
    public long getContentLength() {
        try {
            initBuffer();
        } catch (final IOException e) {
            // GzipCompressingEntity always returns -1 in which case a 'Content-Length' header is omitted.
            // Presumably, without it the request will fail (either client-side or server-side).
            return -1;
        }
        return gzippedContent.length;
    }

    @Override
    public boolean isChunked() {
        // "Content-Length" & chunked encoding are mutually exclusive:
        //   https://en.wikipedia.org/wiki/Chunked_transfer_encoding
        return false;
    }

    @Override
    public InputStream getContent() throws IOException {
        initBuffer();
        return new ByteArrayInputStream(gzippedContent);
    }

    @Override
    public void writeTo(final OutputStream outstream) throws IOException {
        initBuffer();
        outstream.write(gzippedContent);
    }

    private void initBuffer() throws IOException {
        if (gzippedContent != null) {
            return;
        }

        final long unzippedContentLength = wrappedEntity.getContentLength();
        if (unzippedContentLength > MAX_BUFFER_SIZE_BYTES) {
            throw new IOException(
                    "Wrapped entity content length, " + unzippedContentLength + " bytes, exceeds max: " + MAX_BUFFER_SIZE_BYTES);
        }

        // The buffer size needed by the gzipped content should be smaller than this,
        // but it's more efficient just to allocate one larger buffer than allocate
        // twice if the gzipped content is too large for the default buffer.
        final ByteArrayOutputStream s = new ByteArrayOutputStream((int) unzippedContentLength);
        try {
            super.writeTo(s);
        } finally {
            s.close();
        }

        gzippedContent = s.toByteArray();
    }
}