src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/ResponseHeaders.java
author lucy
Mon, 09 Oct 2017 11:43:42 +0200
changeset 47606 660175b829e8
parent 47216 71c04702a3d5
child 55763 634d8e14c172
permissions -rw-r--r--
8187964: [s390][ppc]: Intrinsify Math.multiplyHigh(long, long) Reviewed-by: mdoerr

/*
 * Copyright (c) 2015, 2017, 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
 * questions.
 */

package jdk.incubator.http;

import sun.net.www.MessageHeader;

import java.io.IOException;
import java.io.InputStream;
import java.net.ProtocolException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;

import static java.lang.String.format;
import static jdk.incubator.http.internal.common.Utils.isValidName;
import static jdk.incubator.http.internal.common.Utils.isValidValue;
import static java.util.Objects.requireNonNull;

/*
 * Reads entire header block off channel, in blocking mode.
 * This class is not thread-safe.
 */
final class ResponseHeaders implements HttpHeaders {

    private static final char CR = '\r';
    private static final char LF = '\n';

    private final ImmutableHeaders delegate;

    /*
     * This constructor takes a connection from which the header block is read
     * and a buffer which may contain an initial portion of this header block.
     *
     * After the headers have been parsed (this constructor has returned) the
     * leftovers (i.e. data, if any, beyond the header block) are accessible
     * from this same buffer from its position to its limit.
     */
    ResponseHeaders(HttpConnection connection, ByteBuffer buffer) throws IOException {
        requireNonNull(connection);
        requireNonNull(buffer);
        InputStreamWrapper input = new InputStreamWrapper(connection, buffer);
        delegate = ImmutableHeaders.of(parse(input));
    }

    static final class InputStreamWrapper extends InputStream {
        final HttpConnection connection;
        ByteBuffer buffer;
        int lastRead = -1; // last byte read from the buffer
        int consumed = 0; // number of bytes consumed.
        InputStreamWrapper(HttpConnection connection, ByteBuffer buffer) {
            super();
            this.connection = connection;
            this.buffer = buffer;
        }
        @Override
        public int read() throws IOException {
            if (!buffer.hasRemaining()) {
                buffer = connection.read();
                if (buffer == null) {
                    return lastRead = -1;
                }
            }
            // don't let consumed become positive again if it overflowed
            // we just want to make sure that consumed == 1 really means
            // that only one byte was consumed.
            if (consumed >= 0) consumed++;
            return lastRead = buffer.get();
        }
    }

    private static void display(Map<String, List<String>> map) {
        map.forEach((k,v) -> {
            System.out.print (k + ": ");
            for (String val : v) {
                System.out.print(val + ", ");
            }
            System.out.println("");
        });
    }

    private Map<String, List<String>> parse(InputStreamWrapper input)
         throws IOException
    {
        // The bulk of work is done by this time-proven class
        MessageHeader h = new MessageHeader();
        h.parseHeader(input);

        // When there are no headers (and therefore no body), the status line
        // will be followed by an empty CRLF line.
        // In that case MessageHeader.parseHeader() will consume the first
        // CR character and stop there. In this case we must consume the
        // remaining LF.
        if (input.consumed == 1 && CR == (char) input.lastRead) {
            // MessageHeader will not consume LF if the first character it
            // finds is CR. This only happens if there are no headers, and
            // only one byte will be consumed from the buffer. In this case
            // the next byte MUST be LF
            if (input.read() != LF) {
                throw new IOException("Unexpected byte sequence when no headers: "
                     + ((int)CR) + " " + input.lastRead
                     + "(" + ((int)CR) + " " + ((int)LF) + " expected)");
            }
        }

        Map<String, List<String>> rawHeaders = h.getHeaders();

        // Now some additional post-processing to adapt the results received
        // from MessageHeader to what is needed here
        Map<String, List<String>> cookedHeaders = new HashMap<>();
        for (Map.Entry<String, List<String>> e : rawHeaders.entrySet()) {
            String key = e.getKey();
            if (key == null) {
                throw new ProtocolException("Bad header-field");
            }
            if (!isValidName(key)) {
                throw new ProtocolException(format(
                        "Bad header-name: '%s'", key));
            }
            List<String> newValues = e.getValue();
            for (String v : newValues) {
                if (!isValidValue(v)) {
                    throw new ProtocolException(format(
                            "Bad header-value for header-name: '%s'", key));
                }
            }
            String k = key.toLowerCase(Locale.US);
            cookedHeaders.merge(k, newValues,
                    (v1, v2) -> {
                        ArrayList<String> newV = new ArrayList<>();
                        if (v1 != null) {
                            newV.addAll(v1);
                        }
                        newV.addAll(v2);
                        return newV;
                    });
        }
        return cookedHeaders;
    }

    int getContentLength() throws IOException {
        return (int) firstValueAsLong("Content-Length").orElse(-1);
    }

    @Override
    public Optional<String> firstValue(String name) {
        return delegate.firstValue(name);
    }

    @Override
    public OptionalLong firstValueAsLong(String name) {
        return delegate.firstValueAsLong(name);
    }

    @Override
    public List<String> allValues(String name) {
        return delegate.allValues(name);
    }

    @Override
    public Map<String, List<String>> map() {
        return delegate.map();
    }
}