jdk/src/java.httpclient/share/classes/java/net/http/Http1Response.java
changeset 36131 379db4b2f95d
child 37720 45cd7cc65382
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/java.httpclient/share/classes/java/net/http/Http1Response.java	Thu Feb 25 23:14:22 2016 +0000
@@ -0,0 +1,290 @@
+/*
+ * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ */
+package java.net.http;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.LongConsumer;
+import static java.net.http.HttpClient.Version.HTTP_1_1;
+
+/**
+ * Handles a HTTP/1.1 response in two blocking calls. readHeaders() and
+ * readBody(). There can be more than one of these per Http exchange.
+ */
+class Http1Response {
+
+    private ResponseContent content;
+    private final HttpRequestImpl request;
+    HttpResponseImpl response;
+    private final HttpConnection connection;
+    private ResponseHeaders headers;
+    private int responseCode;
+    private ByteBuffer buffer; // same buffer used for reading status line and headers
+    private final Http1Exchange exchange;
+    private final boolean redirecting; // redirecting
+    private boolean return2Cache; // return connection to cache when finished
+
+    Http1Response(HttpConnection conn, Http1Exchange exchange) {
+        this.request = exchange.request();
+        this.exchange = exchange;
+        this.connection = conn;
+        this.redirecting = false;
+        buffer = connection.getRemaining();
+    }
+
+    // called when the initial read should come from a buffer left
+    // over from a previous response.
+    void setBuffer(ByteBuffer buffer) {
+        this.buffer = buffer;
+    }
+
+    @SuppressWarnings("unchecked")
+    public void readHeaders() throws IOException {
+        String statusline = readStatusLine();
+        if (statusline == null) {
+            if (Log.errors()) {
+                Log.logError("Connection closed. Retry");
+            }
+            connection.close();
+            // connection was closed
+            throw new IOException("Connection closed");
+        }
+        if (!statusline.startsWith("HTTP/1.")) {
+            throw new IOException("Invalid status line: " + statusline);
+        }
+        char c = statusline.charAt(7);
+        responseCode = Integer.parseInt(statusline.substring(9, 12));
+
+        headers = new ResponseHeaders(connection, buffer);
+        headers.initHeaders();
+        if (Log.headers()) {
+            logHeaders(headers);
+        }
+        response = new HttpResponseImpl(responseCode,
+                                        exchange.exchange,
+                                        headers,
+                                        null,
+                                        connection.sslParameters(),
+                                        HTTP_1_1,
+                                        connection);
+    }
+
+    private boolean finished;
+
+    synchronized void completed() {
+        finished = true;
+    }
+
+    synchronized boolean finished() {
+        return finished;
+    }
+
+    // Blocking flow controller implementation. Only works when a
+    // thread is dedicated to reading response body
+
+    static class FlowController implements LongConsumer {
+        long window ;
+
+        @Override
+        public synchronized void accept(long value) {
+            window += value;
+            notifyAll();
+        }
+
+        public synchronized void request(long value) throws InterruptedException {
+            while (window < value) {
+                wait();
+            }
+            window -= value;
+        }
+    }
+
+    FlowController flowController;
+
+    int fixupContentLen(int clen) {
+        if (request.method().equalsIgnoreCase("HEAD")) {
+            return 0;
+        }
+        if (clen == -1) {
+            if (headers.firstValue("Transfer-encoding").orElse("")
+                       .equalsIgnoreCase("chunked")) {
+                return -1;
+            }
+            return 0;
+        }
+        return clen;
+    }
+
+    private void returnBuffer(ByteBuffer buf) {
+        // not currently used, but will be when we change SSL to use fixed
+        // sized buffers and a single buffer pool for HttpClientImpl
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T> T readBody(java.net.http.HttpResponse.BodyProcessor<T> p,
+                          boolean return2Cache)
+        throws IOException
+    {
+        T body = null; // TODO: check null case below
+        this.return2Cache = return2Cache;
+        final java.net.http.HttpResponse.BodyProcessor<T> pusher = p;
+
+        int clen0 = headers.getContentLength();
+        final int clen = fixupContentLen(clen0);
+
+        flowController = new FlowController();
+
+        body = pusher.onResponseBodyStart(clen, headers, flowController);
+
+        ExecutorWrapper executor;
+        if (body == null) {
+            executor = ExecutorWrapper.callingThread();
+        } else {
+            executor = request.client().executorWrapper();
+        }
+
+        final ResponseHeaders h = headers;
+        if (body == null) {
+            content = new ResponseContent(connection,
+                                          clen,
+                                          h,
+                                          pusher,
+                                          flowController);
+            content.pushBody(headers.getResidue());
+            body = pusher.onResponseComplete();
+            completed();
+            onFinished();
+            return body;
+        } else {
+            executor.execute(() -> {
+                    try {
+                        content = new ResponseContent(connection,
+                                                      clen,
+                                                      h,
+                                                      pusher,
+                                                      flowController);
+                        content.pushBody(headers.getResidue());
+                        pusher.onResponseComplete();
+                        completed();
+                        onFinished();
+                    } catch (Throwable e) {
+                        pusher.onResponseError(e);
+                    }
+                },
+                () -> response.getAccessControlContext());
+        }
+        return body;
+    }
+
+    private void onFinished() {
+        connection.buffer = content.getResidue();
+        if (return2Cache) {
+            connection.returnToCache(headers);
+        }
+    }
+
+    private void logHeaders(ResponseHeaders headers) {
+        Map<String, List<String>> h = headers.mapInternal();
+        Set<String> keys = h.keySet();
+        Set<Map.Entry<String, List<String>>> entries = h.entrySet();
+        for (Map.Entry<String, List<String>> entry : entries) {
+            String key = entry.getKey();
+            StringBuilder sb = new StringBuilder();
+            sb.append(key).append(": ");
+            List<String> values = entry.getValue();
+            if (values != null) {
+                for (String value : values) {
+                    sb.append(value).append(' ');
+                }
+            }
+            Log.logHeaders(sb.toString());
+        }
+    }
+
+    HttpResponseImpl response() {
+        return response;
+    }
+
+    boolean redirecting() {
+        return redirecting;
+    }
+
+    HttpHeaders responseHeaders() {
+        return headers;
+    }
+
+    int responseCode() {
+        return responseCode;
+    }
+
+    static final char CR = '\r';
+    static final char LF = '\n';
+
+    private ByteBuffer getBuffer() throws IOException {
+        if (buffer == null || !buffer.hasRemaining()) {
+            buffer = connection.read();
+        }
+        return buffer;
+    }
+
+    ByteBuffer buffer() {
+        return buffer;
+    }
+
+    String readStatusLine() throws IOException {
+        boolean cr = false;
+        StringBuilder statusLine = new StringBuilder(128);
+        ByteBuffer b;
+        while ((b = getBuffer()) != null) {
+            byte[] buf = b.array();
+            int offset = b.position();
+            int len = b.limit() - offset;
+
+            for (int i = 0; i < len; i++) {
+                char c = (char) buf[i+offset];
+
+                if (cr) {
+                    if (c == LF) {
+                        b.position(i + 1 + offset);
+                        return statusLine.toString();
+                    } else {
+                        throw new IOException("invalid status line");
+                    }
+                }
+                if (c == CR) {
+                    cr = true;
+                } else {
+                    statusLine.append(c);
+                }
+            }
+            // unlikely, but possible, that multiple reads required
+            b.position(b.limit());
+        }
+        return null;
+    }
+}