< prev index next >
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Encoder.java
Print this page
*** 1,7 ****
/*
! * Copyright (c) 2014, 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
--- 1,7 ----
/*
! * Copyright (c) 2014, 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
*** 22,52 ****
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.incubator.http.internal.hpack;
import java.nio.ByteBuffer;
import java.nio.ReadOnlyBufferException;
import java.util.LinkedList;
import java.util.List;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
/**
* 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()) {
--- 22,57 ----
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.incubator.http.internal.hpack;
+ import jdk.incubator.http.internal.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.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA;
+ import static jdk.incubator.http.internal.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()) {
*** 59,72 ****
* } 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.)
--- 64,76 ----
* } 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.)
*** 83,94 ****
* 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
--- 87,98 ----
* 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
*** 97,109 ****
--- 101,117 ----
*
* @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
*** 127,155 ****
/**
* 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) {
if (maxCapacity < 0) {
! throw new IllegalArgumentException("maxCapacity >= 0: " + maxCapacity);
}
// Initial maximum capacity update mechanics
minCapacity = Long.MAX_VALUE;
currCapacity = -1;
! setMaxCapacity(maxCapacity);
! headerTable = new HeaderTable(lastCapacity);
}
/**
* Sets up the given header {@code (name, value)}.
*
--- 135,181 ----
/**
* 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)}.
*
*** 174,183 ****
--- 200,213 ----
/**
* 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
*** 191,202 ****
* 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 {
// Arguably a good balance between complexity of implementation and
// efficiency of encoding
requireNonNull(name, "name");
requireNonNull(value, "value");
HeaderTable t = getHeaderTable();
--- 221,237 ----
* 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();
*** 220,232 ****
/**
* 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
--- 255,266 ----
/**
* 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
*** 240,254 ****
--- 274,300 ----
* @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));
}
*** 261,273 ****
maxCapacity = capacity;
lastCapacity = calculated;
minCapacity = Math.min(minCapacity, lastCapacity);
}
protected int calculateCapacity(int maxCapacity) {
- // Default implementation of the Encoder won't add anything to the
- // table, therefore no need for a table space
return 0;
}
/**
* Encodes the {@linkplain #header(CharSequence, CharSequence) set up}
--- 307,332 ----
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}
*** 296,306 ****
*/
public final boolean encode(ByteBuffer headerBlock) {
if (!encoding) {
throw new IllegalStateException("A header hasn't been set up");
}
! if (!prependWithCapacityUpdate(headerBlock)) {
return false;
}
boolean done = writer.write(headerTable, headerBlock);
if (done) {
writer.reset(); // FIXME: WHY?
--- 355,368 ----
*/
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?
*** 337,409 ****
return true;
}
protected final void indexed(int index) throws IndexOutOfBoundsException {
checkEncoding();
encoding = true;
writer = indexedWriter.index(index);
}
! protected final void literal(int index, CharSequence value,
boolean useHuffman)
throws IndexOutOfBoundsException {
checkEncoding();
encoding = true;
writer = literalWriter
.index(index).value(value, useHuffman);
}
! protected final void literal(CharSequence name, boolean nameHuffman,
! CharSequence value, boolean valueHuffman) {
checkEncoding();
encoding = true;
writer = literalWriter
.name(name, nameHuffman).value(value, valueHuffman);
}
protected final void literalNeverIndexed(int index,
CharSequence value,
boolean valueHuffman)
throws IndexOutOfBoundsException {
checkEncoding();
encoding = true;
writer = literalNeverIndexedWriter
.index(index).value(value, valueHuffman);
}
protected final void literalNeverIndexed(CharSequence name,
boolean nameHuffman,
CharSequence value,
boolean valueHuffman) {
checkEncoding();
encoding = true;
writer = literalNeverIndexedWriter
.name(name, nameHuffman).value(value, valueHuffman);
}
protected final void literalWithIndexing(int index,
CharSequence value,
boolean valueHuffman)
throws IndexOutOfBoundsException {
checkEncoding();
encoding = true;
writer = literalWithIndexingWriter
.index(index).value(value, valueHuffman);
}
protected final void literalWithIndexing(CharSequence name,
boolean nameHuffman,
CharSequence value,
boolean valueHuffman) {
checkEncoding();
encoding = true;
writer = literalWithIndexingWriter
.name(name, nameHuffman).value(value, valueHuffman);
}
protected final void sizeUpdate(int capacity)
throws IllegalArgumentException {
checkEncoding();
// Ensure subclass follows the contract
if (capacity > this.maxCapacity) {
throw new IllegalArgumentException(
format("capacity <= maxCapacity: capacity=%s, maxCapacity=%s",
--- 399,505 ----
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",
*** 418,428 ****
protected final HeaderTable getHeaderTable() {
return headerTable;
}
! protected final void checkEncoding() {
if (encoding) {
throw new IllegalStateException(
"Previous encoding operation hasn't finished yet");
}
}
--- 514,524 ----
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");
}
}
< prev index next >