--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java Wed Feb 07 21:45:37 2018 +0000
@@ -0,0 +1,525 @@
+/*
+ * Copyright (c) 2014, 2018, 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.internal.net.http.hpack;
+
+import jdk.internal.net.http.hpack.HPACK.Logger;
+
+import java.nio.ByteBuffer;
+import java.nio.ReadOnlyBufferException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA;
+import static jdk.internal.net.http.hpack.HPACK.Logger.Level.NORMAL;
+
+/**
+ * Encodes headers to their binary representation.
+ *
+ * <p> Typical lifecycle looks like this:
+ *
+ * <p> {@link #Encoder(int) new Encoder}
+ * ({@link #setMaxCapacity(int) setMaxCapacity}?
+ * {@link #encode(ByteBuffer) encode})*
+ *
+ * <p> Suppose headers are represented by {@code Map<String, List<String>>}.
+ * A supplier and a consumer of {@link ByteBuffer}s in forms of
+ * {@code Supplier<ByteBuffer>} and {@code Consumer<ByteBuffer>} respectively.
+ * Then to encode headers, the following approach might be used:
+ *
+ * <pre>{@code
+ * for (Map.Entry<String, List<String>> h : headers.entrySet()) {
+ * String name = h.getKey();
+ * for (String value : h.getValue()) {
+ * encoder.header(name, value); // Set up header
+ * boolean encoded;
+ * do {
+ * ByteBuffer b = buffersSupplier.get();
+ * encoded = encoder.encode(b); // Encode the header
+ * buffersConsumer.accept(b);
+ * } while (!encoded);
+ * }
+ * }
+ * }</pre>
+ *
+ * <p> Though the specification <a href="https://tools.ietf.org/html/rfc7541#section-2">does not define</a>
+ * how an encoder is to be implemented, a default implementation is provided by
+ * the method {@link #header(CharSequence, CharSequence, boolean)}.
+ *
+ * <p> To provide a custom encoding implementation, {@code Encoder} has to be
+ * extended. A subclass then can access methods for encoding using specific
+ * representations (e.g. {@link #literal(int, CharSequence, boolean) literal},
+ * {@link #indexed(int) indexed}, etc.)
+ *
+ * @apiNote
+ *
+ * <p> An Encoder provides an incremental way of encoding headers.
+ * {@link #encode(ByteBuffer)} takes a buffer a returns a boolean indicating
+ * whether, or not, the buffer was sufficiently sized to hold the
+ * remaining of the encoded representation.
+ *
+ * <p> This way, there's no need to provide a buffer of a specific size, or to
+ * resize (and copy) the buffer on demand, when the remaining encoded
+ * representation will not fit in the buffer's remaining space. Instead, an
+ * array of existing buffers can be used, prepended with a frame that encloses
+ * the resulting header block afterwards.
+ *
+ * <p> Splitting the encoding operation into header set up and header encoding,
+ * separates long lived arguments ({@code name}, {@code value},
+ * {@code sensitivity}, etc.) from the short lived ones (e.g. {@code buffer}),
+ * simplifying each operation itself.
+ *
+ * @implNote
+ *
+ * <p> The default implementation does not use dynamic table. It reports to a
+ * coupled Decoder a size update with the value of {@code 0}, and never changes
+ * it afterwards.
+ *
+ * @since 9
+ */
+public class Encoder {
+
+ private static final AtomicLong ENCODERS_IDS = new AtomicLong();
+
+ // TODO: enum: no huffman/smart huffman/always huffman
+ private static final boolean DEFAULT_HUFFMAN = true;
+
+ private final Logger logger;
+ private final long id;
+ private final IndexedWriter indexedWriter = new IndexedWriter();
+ private final LiteralWriter literalWriter = new LiteralWriter();
+ private final LiteralNeverIndexedWriter literalNeverIndexedWriter
+ = new LiteralNeverIndexedWriter();
+ private final LiteralWithIndexingWriter literalWithIndexingWriter
+ = new LiteralWithIndexingWriter();
+ private final SizeUpdateWriter sizeUpdateWriter = new SizeUpdateWriter();
+ private final BulkSizeUpdateWriter bulkSizeUpdateWriter
+ = new BulkSizeUpdateWriter();
+
+ private BinaryRepresentationWriter writer;
+ private final HeaderTable headerTable;
+
+ private boolean encoding;
+
+ private int maxCapacity;
+ private int currCapacity;
+ private int lastCapacity;
+ private long minCapacity;
+ private boolean capacityUpdate;
+ private boolean configuredCapacityUpdate;
+
+ /**
+ * Constructs an {@code Encoder} with the specified maximum capacity of the
+ * header table.
+ *
+ * <p> The value has to be agreed between decoder and encoder out-of-band,
+ * e.g. by a protocol that uses HPACK
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
+ *
+ * @param maxCapacity
+ * a non-negative integer
+ *
+ * @throws IllegalArgumentException
+ * if maxCapacity is negative
+ */
+ public Encoder(int maxCapacity) {
+ id = ENCODERS_IDS.incrementAndGet();
+ this.logger = HPACK.getLogger().subLogger("Encoder#" + id);
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("new encoder with maximum table size %s",
+ maxCapacity));
+ }
+ if (logger.isLoggable(EXTRA)) {
+ /* To correlate with logging outside HPACK, knowing
+ hashCode/toString is important */
+ logger.log(EXTRA, () -> {
+ String hashCode = Integer.toHexString(
+ System.identityHashCode(this));
+ /* Since Encoder can be subclassed hashCode AND identity
+ hashCode might be different. So let's print both. */
+ return format("toString='%s', hashCode=%s, identityHashCode=%s",
+ toString(), hashCode(), hashCode);
+ });
+ }
+ if (maxCapacity < 0) {
+ throw new IllegalArgumentException(
+ "maxCapacity >= 0: " + maxCapacity);
+ }
+ // Initial maximum capacity update mechanics
+ minCapacity = Long.MAX_VALUE;
+ currCapacity = -1;
+ setMaxCapacity0(maxCapacity);
+ headerTable = new HeaderTable(lastCapacity, logger.subLogger("HeaderTable"));
+ }
+
+ /**
+ * Sets up the given header {@code (name, value)}.
+ *
+ * <p> Fixates {@code name} and {@code value} for the duration of encoding.
+ *
+ * @param name
+ * the name
+ * @param value
+ * the value
+ *
+ * @throws NullPointerException
+ * if any of the arguments are {@code null}
+ * @throws IllegalStateException
+ * if the encoder hasn't fully encoded the previous header, or
+ * hasn't yet started to encode it
+ * @see #header(CharSequence, CharSequence, boolean)
+ */
+ public void header(CharSequence name, CharSequence value)
+ throws IllegalStateException {
+ header(name, value, false);
+ }
+
+ /**
+ * Sets up the given header {@code (name, value)} with possibly sensitive
+ * value.
+ *
+ * <p> If the {@code value} is sensitive (think security, secrecy, etc.)
+ * this encoder will compress it using a special representation
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">6.2.3. Literal Header Field Never Indexed</a>).
+ *
+ * <p> Fixates {@code name} and {@code value} for the duration of encoding.
+ *
+ * @param name
+ * the name
+ * @param value
+ * the value
+ * @param sensitive
+ * whether or not the value is sensitive
+ *
+ * @throws NullPointerException
+ * if any of the arguments are {@code null}
+ * @throws IllegalStateException
+ * if the encoder hasn't fully encoded the previous header, or
+ * hasn't yet started to encode it
+ * @see #header(CharSequence, CharSequence)
+ * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean)
+ */
+ public void header(CharSequence name,
+ CharSequence value,
+ boolean sensitive) throws IllegalStateException {
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("encoding ('%s', '%s'), sensitive: %s",
+ name, value, sensitive));
+ }
+ // Arguably a good balance between complexity of implementation and
+ // efficiency of encoding
+ requireNonNull(name, "name");
+ requireNonNull(value, "value");
+ HeaderTable t = getHeaderTable();
+ int index = t.indexOf(name, value);
+ if (index > 0) {
+ indexed(index);
+ } else if (index < 0) {
+ if (sensitive) {
+ literalNeverIndexed(-index, value, DEFAULT_HUFFMAN);
+ } else {
+ literal(-index, value, DEFAULT_HUFFMAN);
+ }
+ } else {
+ if (sensitive) {
+ literalNeverIndexed(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN);
+ } else {
+ literal(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN);
+ }
+ }
+ }
+
+ /**
+ * Sets a maximum capacity of the header table.
+ *
+ * <p> The value has to be agreed between decoder and encoder out-of-band,
+ * e.g. by a protocol that uses HPACK
+ * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
+ *
+ * <p> May be called any number of times after or before a complete header
+ * has been encoded.
+ *
+ * <p> If the encoder decides to change the actual capacity, an update will
+ * be encoded before a new encoding operation starts.
+ *
+ * @param capacity
+ * a non-negative integer
+ *
+ * @throws IllegalArgumentException
+ * if capacity is negative
+ * @throws IllegalStateException
+ * if the encoder hasn't fully encoded the previous header, or
+ * hasn't yet started to encode it
+ */
+ public void setMaxCapacity(int capacity) {
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("setting maximum table size to %s",
+ capacity));
+ }
+ setMaxCapacity0(capacity);
+ }
+
+ private void setMaxCapacity0(int capacity) {
+ checkEncoding();
+ if (capacity < 0) {
+ throw new IllegalArgumentException("capacity >= 0: " + capacity);
+ }
+ int calculated = calculateCapacity(capacity);
+ if (logger.isLoggable(NORMAL)) {
+ logger.log(NORMAL, () -> format("actual maximum table size will be %s",
+ calculated));
+ }
+ if (calculated < 0 || calculated > capacity) {
+ throw new IllegalArgumentException(
+ format("0 <= calculated <= capacity: calculated=%s, capacity=%s",
+ calculated, capacity));
+ }
+ capacityUpdate = true;
+ // maxCapacity needs to be updated unconditionally, so the encoder
+ // always has the newest one (in case it decides to update it later
+ // unsolicitedly)
+ // Suppose maxCapacity = 4096, and the encoder has decided to use only
+ // 2048. It later can choose anything else from the region [0, 4096].
+ maxCapacity = capacity;
+ lastCapacity = calculated;
+ minCapacity = Math.min(minCapacity, lastCapacity);
+ }
+
+ /**
+ * Calculates actual capacity to be used by this encoder in response to
+ * a request to update maximum table size.
+ *
+ * <p> Default implementation does not add anything to the headers table,
+ * hence this method returns {@code 0}.
+ *
+ * <p> It is an error to return a value {@code c}, where {@code c < 0} or
+ * {@code c > maxCapacity}.
+ *
+ * @param maxCapacity
+ * upper bound
+ *
+ * @return actual capacity
+ */
+ protected int calculateCapacity(int maxCapacity) {
+ return 0;
+ }
+
+ /**
+ * Encodes the {@linkplain #header(CharSequence, CharSequence) set up}
+ * header into the given buffer.
+ *
+ * <p> The encoder writes as much as possible of the header's binary
+ * representation into the given buffer, starting at the buffer's position,
+ * and increments its position to reflect the bytes written. The buffer's
+ * mark and limit will not be modified.
+ *
+ * <p> Once the method has returned {@code true}, the current header is
+ * deemed encoded. A new header may be set up.
+ *
+ * @param headerBlock
+ * the buffer to encode the header into, may be empty
+ *
+ * @return {@code true} if the current header has been fully encoded,
+ * {@code false} otherwise
+ *
+ * @throws NullPointerException
+ * if the buffer is {@code null}
+ * @throws ReadOnlyBufferException
+ * if this buffer is read-only
+ * @throws IllegalStateException
+ * if there is no set up header
+ */
+ public final boolean encode(ByteBuffer headerBlock) {
+ if (!encoding) {
+ throw new IllegalStateException("A header hasn't been set up");
+ }
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("writing to %s", headerBlock));
+ }
+ if (!prependWithCapacityUpdate(headerBlock)) { // TODO: log
+ return false;
+ }
+ boolean done = writer.write(headerTable, headerBlock);
+ if (done) {
+ writer.reset(); // FIXME: WHY?
+ encoding = false;
+ }
+ return done;
+ }
+
+ private boolean prependWithCapacityUpdate(ByteBuffer headerBlock) {
+ if (capacityUpdate) {
+ if (!configuredCapacityUpdate) {
+ List<Integer> sizes = new LinkedList<>();
+ if (minCapacity < currCapacity) {
+ sizes.add((int) minCapacity);
+ if (minCapacity != lastCapacity) {
+ sizes.add(lastCapacity);
+ }
+ } else if (lastCapacity != currCapacity) {
+ sizes.add(lastCapacity);
+ }
+ bulkSizeUpdateWriter.maxHeaderTableSizes(sizes);
+ configuredCapacityUpdate = true;
+ }
+ boolean done = bulkSizeUpdateWriter.write(headerTable, headerBlock);
+ if (done) {
+ minCapacity = lastCapacity;
+ currCapacity = lastCapacity;
+ bulkSizeUpdateWriter.reset();
+ capacityUpdate = false;
+ configuredCapacityUpdate = false;
+ }
+ return done;
+ }
+ return true;
+ }
+
+ protected final void indexed(int index) throws IndexOutOfBoundsException {
+ checkEncoding();
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("indexed %s", index));
+ }
+ encoding = true;
+ writer = indexedWriter.index(index);
+ }
+
+ protected final void literal(int index,
+ CharSequence value,
+ boolean useHuffman)
+ throws IndexOutOfBoundsException {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')",
+ index, value));
+ }
+ checkEncoding();
+ encoding = true;
+ writer = literalWriter
+ .index(index).value(value, useHuffman);
+ }
+
+ protected final void literal(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')",
+ name, value));
+ }
+ checkEncoding();
+ encoding = true;
+ writer = literalWriter
+ .name(name, nameHuffman).value(value, valueHuffman);
+ }
+
+ protected final void literalNeverIndexed(int index,
+ CharSequence value,
+ boolean valueHuffman)
+ throws IndexOutOfBoundsException {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')",
+ index, value));
+ }
+ checkEncoding();
+ encoding = true;
+ writer = literalNeverIndexedWriter
+ .index(index).value(value, valueHuffman);
+ }
+
+ protected final void literalNeverIndexed(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')",
+ name, value));
+ }
+ checkEncoding();
+ encoding = true;
+ writer = literalNeverIndexedWriter
+ .name(name, nameHuffman).value(value, valueHuffman);
+ }
+
+ protected final void literalWithIndexing(int index,
+ CharSequence value,
+ boolean valueHuffman)
+ throws IndexOutOfBoundsException {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')",
+ index, value));
+ }
+ checkEncoding();
+ encoding = true;
+ writer = literalWithIndexingWriter
+ .index(index).value(value, valueHuffman);
+ }
+
+ protected final void literalWithIndexing(CharSequence name,
+ boolean nameHuffman,
+ CharSequence value,
+ boolean valueHuffman) {
+ if (logger.isLoggable(EXTRA)) { // TODO: include huffman info?
+ logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')",
+ name, value));
+ }
+ checkEncoding();
+ encoding = true;
+ writer = literalWithIndexingWriter
+ .name(name, nameHuffman).value(value, valueHuffman);
+ }
+
+ protected final void sizeUpdate(int capacity)
+ throws IllegalArgumentException {
+ if (logger.isLoggable(EXTRA)) {
+ logger.log(EXTRA, () -> format("dynamic table size update %s",
+ capacity));
+ }
+ checkEncoding();
+ // Ensure subclass follows the contract
+ if (capacity > this.maxCapacity) {
+ throw new IllegalArgumentException(
+ format("capacity <= maxCapacity: capacity=%s, maxCapacity=%s",
+ capacity, maxCapacity));
+ }
+ writer = sizeUpdateWriter.maxHeaderTableSize(capacity);
+ }
+
+ protected final int getMaxCapacity() {
+ return maxCapacity;
+ }
+
+ protected final HeaderTable getHeaderTable() {
+ return headerTable;
+ }
+
+ protected final void checkEncoding() { // TODO: better name e.g. checkIfEncodingInProgress()
+ if (encoding) {
+ throw new IllegalStateException(
+ "Previous encoding operation hasn't finished yet");
+ }
+ }
+}