xref: /JGit/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/SmartOutputStream.java (revision db019c473edcf8cc96456f8c4ac07e42a7d2eefa)
1 /*
2  * Copyright (C) 2010, Google Inc. and others
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Distribution License v. 1.0 which is available at
6  * https://www.eclipse.org/org/documents/edl-v10.php.
7  *
8  * SPDX-License-Identifier: BSD-3-Clause
9  */
10 
11 package org.eclipse.jgit.http.server;
12 
13 import static org.eclipse.jgit.http.server.ServletUtils.acceptsGzipEncoding;
14 import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP;
15 import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING;
16 
17 import java.io.IOException;
18 import java.io.OutputStream;
19 import java.util.zip.GZIPOutputStream;
20 
21 import javax.servlet.http.HttpServletRequest;
22 import javax.servlet.http.HttpServletResponse;
23 
24 import org.eclipse.jgit.util.TemporaryBuffer;
25 
26 /**
27  * Buffers a response, trying to gzip it if the user agent supports that.
28  * <p>
29  * If the response overflows the buffer, gzip is skipped and the response is
30  * streamed to the client as its produced, most likely using HTTP/1.1 chunked
31  * encoding. This is useful for servlets that produce mixed-mode content, where
32  * smaller payloads are primarily pure text that compresses well, while much
33  * larger payloads are heavily compressed binary data. {@link UploadPackServlet}
34  * is one such servlet.
35  */
36 class SmartOutputStream extends TemporaryBuffer {
37 	private static final int LIMIT = 32 * 1024;
38 
39 	private final HttpServletRequest req;
40 	private final HttpServletResponse rsp;
41 	private boolean compressStream;
42 	private boolean startedOutput;
43 
SmartOutputStream(final HttpServletRequest req, final HttpServletResponse rsp, boolean compressStream)44 	SmartOutputStream(final HttpServletRequest req,
45 			final HttpServletResponse rsp,
46 			boolean compressStream) {
47 		super(LIMIT);
48 		this.req = req;
49 		this.rsp = rsp;
50 		this.compressStream = compressStream;
51 	}
52 
53 	/** {@inheritDoc} */
54 	@Override
overflow()55 	protected OutputStream overflow() throws IOException {
56 		startedOutput = true;
57 
58 		OutputStream out = rsp.getOutputStream();
59 		if (compressStream && acceptsGzipEncoding(req)) {
60 			rsp.setHeader(HDR_CONTENT_ENCODING, ENCODING_GZIP);
61 			out = new GZIPOutputStream(out);
62 		}
63 		return out;
64 	}
65 
66 	/** {@inheritDoc} */
67 	@Override
close()68 	public void close() throws IOException {
69 		super.close();
70 
71 		if (!startedOutput) {
72 			// If output hasn't started yet, the entire thing fit into our
73 			// buffer. Try to use a proper Content-Length header, and also
74 			// deflate the response with gzip if it will be smaller.
75 			if (256 < this.length() && acceptsGzipEncoding(req)) {
76 				TemporaryBuffer gzbuf = new TemporaryBuffer.Heap(LIMIT);
77 				try {
78 					try (GZIPOutputStream gzip = new GZIPOutputStream(gzbuf)) {
79 						this.writeTo(gzip, null);
80 					}
81 					if (gzbuf.length() < this.length()) {
82 						rsp.setHeader(HDR_CONTENT_ENCODING, ENCODING_GZIP);
83 						writeResponse(gzbuf);
84 						return;
85 					}
86 				} catch (IOException err) {
87 					// Most likely caused by overflowing the buffer, meaning
88 					// its larger if it were compressed. Discard compressed
89 					// copy and use the original.
90 				}
91 			}
92 			writeResponse(this);
93 		}
94 	}
95 
writeResponse(TemporaryBuffer out)96 	private void writeResponse(TemporaryBuffer out) throws IOException {
97 		// The Content-Length cannot overflow when cast to an int, our
98 		// hardcoded LIMIT constant above assures us we wouldn't store
99 		// more than 2 GiB of content in memory.
100 		rsp.setContentLength((int) out.length());
101 		try (OutputStream os = rsp.getOutputStream()) {
102 			out.writeTo(os, null);
103 			os.flush();
104 		}
105 	}
106 }
107