< prev index next >
src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/Encoder.java
Print this page
@@ -1,7 +1,7 @@
/*
- * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
+ * 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,31 +22,36 @@
* 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> 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:
+ * <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,14 +64,13 @@
* } 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> 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,12 +87,12 @@
* 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}),
+ * 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,13 +101,17 @@
*
* @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,29 +135,47 @@
/**
* 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>).
+ * 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);
+ throw new IllegalArgumentException(
+ "maxCapacity >= 0: " + maxCapacity);
}
// Initial maximum capacity update mechanics
minCapacity = Long.MAX_VALUE;
currCapacity = -1;
- setMaxCapacity(maxCapacity);
- headerTable = new HeaderTable(lastCapacity);
+ setMaxCapacity0(maxCapacity);
+ headerTable = new HeaderTable(lastCapacity, logger.subLogger("HeaderTable"));
}
/**
* Sets up the given header {@code (name, value)}.
*
@@ -174,10 +200,14 @@
/**
* 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,12 +221,17 @@
* 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,
+ 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,13 +255,12 @@
/**
* 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>).
+ * 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,15 +274,27 @@
* @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,13 +307,26 @@
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) {
- // 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}
@@ -296,11 +355,14 @@
*/
public final boolean encode(ByteBuffer headerBlock) {
if (!encoding) {
throw new IllegalStateException("A header hasn't been set up");
}
- if (!prependWithCapacityUpdate(headerBlock)) {
+ 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,73 +399,107 @@
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,
+ 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) {
+ 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,11 +514,11 @@
protected final HeaderTable getHeaderTable() {
return headerTable;
}
- protected final void checkEncoding() {
+ 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 >