diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/InfoRefsServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/InfoRefsServlet.java index b766196fd..f667ce95a 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/InfoRefsServlet.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/InfoRefsServlet.java @@ -44,9 +44,9 @@ package org.eclipse.jgit.http.server; import static org.eclipse.jgit.http.server.ServletUtils.getRepository; -import static org.eclipse.jgit.http.server.ServletUtils.send; import java.io.IOException; +import java.io.OutputStreamWriter; import java.util.Map; import javax.servlet.http.HttpServlet; @@ -69,20 +69,18 @@ class InfoRefsServlet extends HttpServlet { final HttpServletResponse rsp) throws IOException { // Assume a dumb client and send back the dumb client // version of the info/refs file. - final byte[] raw = dumbHttp(req); rsp.setContentType(HttpSupport.TEXT_PLAIN); rsp.setCharacterEncoding(Constants.CHARACTER_ENCODING); - send(raw, req, rsp); - } - private byte[] dumbHttp(final HttpServletRequest req) throws IOException { final Repository db = getRepository(req); final RevWalk walk = new RevWalk(db); final RevFlag ADVERTISED = walk.newFlag("ADVERTISED"); - final StringBuilder out = new StringBuilder(); + + final OutputStreamWriter out = new OutputStreamWriter( + new SmartOutputStream(req, rsp), Constants.CHARSET); final RefAdvertiser adv = new RefAdvertiser() { @Override - protected void writeOne(final CharSequence line) { + protected void writeOne(final CharSequence line) throws IOException { // Whoever decided that info/refs should use a different // delimiter than the native git:// protocol shouldn't // be allowed to design this sort of stuff. :-( @@ -100,6 +98,6 @@ class InfoRefsServlet extends HttpServlet { Map refs = db.getAllRefs(); refs.remove(Constants.HEAD); adv.send(refs); - return out.toString().getBytes(Constants.CHARACTER_ENCODING); + out.close(); } } diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java index 1a426af96..ba8b8ab66 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java @@ -50,9 +50,7 @@ import static javax.servlet.http.HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE; import static org.eclipse.jgit.http.server.ServletUtils.getInputStream; import static org.eclipse.jgit.http.server.ServletUtils.getRepository; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.OutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -104,11 +102,14 @@ class ReceivePackServlet extends HttpServlet { } final Repository db = getRepository(req); - final ByteArrayOutputStream out = new ByteArrayOutputStream(); try { final ReceivePack rp = receivePackFactory.create(req, db); rp.setBiDirectionalPipe(false); + rsp.setContentType(RSP_TYPE); + + final SmartOutputStream out = new SmartOutputStream(req, rsp); rp.receive(getInputStream(req), out, null); + out.close(); } catch (ServiceNotAuthorizedException e) { rsp.sendError(SC_UNAUTHORIZED); @@ -123,20 +124,5 @@ class ReceivePackServlet extends HttpServlet { rsp.sendError(SC_INTERNAL_SERVER_ERROR); return; } - - reply(rsp, out.toByteArray()); - } - - private void reply(final HttpServletResponse rsp, final byte[] result) - throws IOException { - rsp.setContentType(RSP_TYPE); - rsp.setContentLength(result.length); - final OutputStream os = rsp.getOutputStream(); - try { - os.write(result); - os.flush(); - } finally { - os.close(); - } } } diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ServletUtils.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ServletUtils.java index d6b039246..7514d7c7e 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ServletUtils.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ServletUtils.java @@ -189,7 +189,7 @@ public final class ServletUtils { return content; } - private static boolean acceptsGzipEncoding(final HttpServletRequest req) { + static boolean acceptsGzipEncoding(final HttpServletRequest req) { final String accepts = req.getHeader(HDR_ACCEPT_ENCODING); return accepts != null && 0 <= accepts.indexOf(ENCODING_GZIP); } diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/SmartOutputStream.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/SmartOutputStream.java new file mode 100644 index 000000000..00cb67ca9 --- /dev/null +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/SmartOutputStream.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2010, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.http.server; + +import static org.eclipse.jgit.http.server.ServletUtils.acceptsGzipEncoding; +import static org.eclipse.jgit.util.HttpSupport.ENCODING_GZIP; +import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.GZIPOutputStream; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jgit.util.TemporaryBuffer; + +/** + * Buffers a response, trying to gzip it if the user agent supports that. + *

+ * If the response overflows the buffer, gzip is skipped and the response is + * streamed to the client as its produced, most likely using HTTP/1.1 chunked + * encoding. This is useful for servlets that produce mixed-mode content, where + * smaller payloads are primarily pure text that compresses well, while much + * larger payloads are heavily compressed binary data. {@link UploadPackServlet} + * is one such servlet. + */ +class SmartOutputStream extends TemporaryBuffer { + private static final int LIMIT = 32 * 1024; + + private final HttpServletRequest req; + + private final HttpServletResponse rsp; + + private boolean startedOutput; + + SmartOutputStream(final HttpServletRequest req, + final HttpServletResponse rsp) { + super(LIMIT); + this.req = req; + this.rsp = rsp; + } + + @Override + protected OutputStream overflow() throws IOException { + startedOutput = true; + return rsp.getOutputStream(); + } + + public void close() throws IOException { + super.close(); + + if (!startedOutput) { + // If output hasn't started yet, the entire thing fit into our + // buffer. Try to use a proper Content-Length header, and also + // deflate the response with gzip if it will be smaller. + TemporaryBuffer out = this; + + if (256 < out.length() && acceptsGzipEncoding(req)) { + TemporaryBuffer gzbuf = new TemporaryBuffer.Heap(LIMIT); + try { + GZIPOutputStream gzip = new GZIPOutputStream(gzbuf); + out.writeTo(gzip, null); + gzip.close(); + if (gzbuf.length() < out.length()) { + out = gzbuf; + rsp.setHeader(HDR_CONTENT_ENCODING, ENCODING_GZIP); + } + } catch (IOException err) { + // Most likely caused by overflowing the buffer, meaning + // its larger if it were compressed. Discard compressed + // copy and use the original. + } + } + + // The Content-Length cannot overflow when cast to an int, our + // hardcoded LIMIT constant above assures us we wouldn't store + // more than 2 GiB of content in memory. + rsp.setContentLength((int) out.length()); + final OutputStream os = rsp.getOutputStream(); + try { + out.writeTo(os, null); + os.flush(); + } finally { + os.close(); + } + } + } +} diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/SmartServiceInfoRefs.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/SmartServiceInfoRefs.java index 96716d070..94e2e7f6f 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/SmartServiceInfoRefs.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/SmartServiceInfoRefs.java @@ -46,9 +46,7 @@ package org.eclipse.jgit.http.server; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; import static org.eclipse.jgit.http.server.ServletUtils.getRepository; -import static org.eclipse.jgit.http.server.ServletUtils.send; -import java.io.ByteArrayOutputStream; import java.io.IOException; import javax.servlet.Filter; @@ -90,13 +88,14 @@ abstract class SmartServiceInfoRefs implements Filter { final HttpServletResponse rsp = (HttpServletResponse) response; try { final Repository db = getRepository(req); - final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + rsp.setContentType("application/x-" + svc + "-advertisement"); + + final SmartOutputStream buf = new SmartOutputStream(req, rsp); final PacketLineOut out = new PacketLineOut(buf); out.writeString("# service=" + svc + "\n"); out.end(); advertise(req, db, new PacketLineOutRefAdvertiser(out)); - rsp.setContentType("application/x-" + svc + "-advertisement"); - send(buf.toByteArray(), req, rsp); + buf.close(); } catch (ServiceNotAuthorizedException e) { rsp.sendError(SC_UNAUTHORIZED); diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java index dc874fa5f..8de5f0678 100644 --- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java +++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java @@ -106,7 +106,10 @@ class UploadPackServlet extends HttpServlet { final UploadPack up = uploadPackFactory.create(req, db); up.setBiDirectionalPipe(false); rsp.setContentType(RSP_TYPE); - up.upload(getInputStream(req), rsp.getOutputStream(), null); + + final SmartOutputStream out = new SmartOutputStream(req, rsp); + up.upload(getInputStream(req), out, null); + out.close(); } catch (ServiceNotAuthorizedException e) { rsp.reset(); diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java index fd78bb451..f7b3bdb20 100644 --- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java +++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2010, Google Inc. + * Copyright (C) 2010, Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -253,8 +253,6 @@ public class SmartClientSmartServerTest extends HttpTestCase { assertEquals(200, service.getStatus()); assertEquals("application/x-git-upload-pack-result", service .getResponseHeader(HDR_CONTENT_TYPE)); - assertNull("no compression (never compressed)", service - .getResponseHeader(HDR_CONTENT_ENCODING)); } public void testFetchUpdateExisting() throws Exception {