src/java.net.http/share/classes/jdk/internal/net/http/hpack/Encoder.java
branchhttp-client-branch
changeset 56092 fd85b2bf2b0d
parent 56089 42208b2f224e
child 56387 c08eb1e2dc38
equal deleted inserted replaced
56091:aedd6133e7a0 56092:fd85b2bf2b0d
       
     1 /*
       
     2  * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.  Oracle designates this
       
     8  * particular file as subject to the "Classpath" exception as provided
       
     9  * by Oracle in the LICENSE file that accompanied this code.
       
    10  *
       
    11  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    14  * version 2 for more details (a copy is included in the LICENSE file that
       
    15  * accompanied this code).
       
    16  *
       
    17  * You should have received a copy of the GNU General Public License version
       
    18  * 2 along with this work; if not, write to the Free Software Foundation,
       
    19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    20  *
       
    21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    22  * or visit www.oracle.com if you need additional information or have any
       
    23  * questions.
       
    24  */
       
    25 package jdk.internal.net.http.hpack;
       
    26 
       
    27 import jdk.internal.net.http.hpack.HPACK.Logger;
       
    28 
       
    29 import java.nio.ByteBuffer;
       
    30 import java.nio.ReadOnlyBufferException;
       
    31 import java.util.LinkedList;
       
    32 import java.util.List;
       
    33 import java.util.concurrent.atomic.AtomicLong;
       
    34 
       
    35 import static java.lang.String.format;
       
    36 import static java.util.Objects.requireNonNull;
       
    37 import static jdk.internal.net.http.hpack.HPACK.Logger.Level.EXTRA;
       
    38 import static jdk.internal.net.http.hpack.HPACK.Logger.Level.NORMAL;
       
    39 
       
    40 /**
       
    41  * Encodes headers to their binary representation.
       
    42  *
       
    43  * <p> Typical lifecycle looks like this:
       
    44  *
       
    45  * <p> {@link #Encoder(int) new Encoder}
       
    46  * ({@link #setMaxCapacity(int) setMaxCapacity}?
       
    47  * {@link #encode(ByteBuffer) encode})*
       
    48  *
       
    49  * <p> Suppose headers are represented by {@code Map<String, List<String>>}.
       
    50  * A supplier and a consumer of {@link ByteBuffer}s in forms of
       
    51  * {@code Supplier<ByteBuffer>} and {@code Consumer<ByteBuffer>} respectively.
       
    52  * Then to encode headers, the following approach might be used:
       
    53  *
       
    54  * <pre>{@code
       
    55  *     for (Map.Entry<String, List<String>> h : headers.entrySet()) {
       
    56  *         String name = h.getKey();
       
    57  *         for (String value : h.getValue()) {
       
    58  *             encoder.header(name, value);        // Set up header
       
    59  *             boolean encoded;
       
    60  *             do {
       
    61  *                 ByteBuffer b = buffersSupplier.get();
       
    62  *                 encoded = encoder.encode(b);    // Encode the header
       
    63  *                 buffersConsumer.accept(b);
       
    64  *             } while (!encoded);
       
    65  *         }
       
    66  *     }
       
    67  * }</pre>
       
    68  *
       
    69  * <p> Though the specification <a href="https://tools.ietf.org/html/rfc7541#section-2">does not define</a>
       
    70  * how an encoder is to be implemented, a default implementation is provided by
       
    71  * the method {@link #header(CharSequence, CharSequence, boolean)}.
       
    72  *
       
    73  * <p> To provide a custom encoding implementation, {@code Encoder} has to be
       
    74  * extended. A subclass then can access methods for encoding using specific
       
    75  * representations (e.g. {@link #literal(int, CharSequence, boolean) literal},
       
    76  * {@link #indexed(int) indexed}, etc.)
       
    77  *
       
    78  * @apiNote
       
    79  *
       
    80  * <p> An Encoder provides an incremental way of encoding headers.
       
    81  * {@link #encode(ByteBuffer)} takes a buffer a returns a boolean indicating
       
    82  * whether, or not, the buffer was sufficiently sized to hold the
       
    83  * remaining of the encoded representation.
       
    84  *
       
    85  * <p> This way, there's no need to provide a buffer of a specific size, or to
       
    86  * resize (and copy) the buffer on demand, when the remaining encoded
       
    87  * representation will not fit in the buffer's remaining space. Instead, an
       
    88  * array of existing buffers can be used, prepended with a frame that encloses
       
    89  * the resulting header block afterwards.
       
    90  *
       
    91  * <p> Splitting the encoding operation into header set up and header encoding,
       
    92  * separates long lived arguments ({@code name}, {@code value},
       
    93  * {@code sensitivity}, etc.) from the short lived ones (e.g. {@code buffer}),
       
    94  * simplifying each operation itself.
       
    95  *
       
    96  * @implNote
       
    97  *
       
    98  * <p> The default implementation does not use dynamic table. It reports to a
       
    99  * coupled Decoder a size update with the value of {@code 0}, and never changes
       
   100  * it afterwards.
       
   101  *
       
   102  * @since 9
       
   103  */
       
   104 public class Encoder {
       
   105 
       
   106     private static final AtomicLong ENCODERS_IDS = new AtomicLong();
       
   107 
       
   108     // TODO: enum: no huffman/smart huffman/always huffman
       
   109     private static final boolean DEFAULT_HUFFMAN = true;
       
   110 
       
   111     private final Logger logger;
       
   112     private final long id;
       
   113     private final IndexedWriter indexedWriter = new IndexedWriter();
       
   114     private final LiteralWriter literalWriter = new LiteralWriter();
       
   115     private final LiteralNeverIndexedWriter literalNeverIndexedWriter
       
   116             = new LiteralNeverIndexedWriter();
       
   117     private final LiteralWithIndexingWriter literalWithIndexingWriter
       
   118             = new LiteralWithIndexingWriter();
       
   119     private final SizeUpdateWriter sizeUpdateWriter = new SizeUpdateWriter();
       
   120     private final BulkSizeUpdateWriter bulkSizeUpdateWriter
       
   121             = new BulkSizeUpdateWriter();
       
   122 
       
   123     private BinaryRepresentationWriter writer;
       
   124     private final HeaderTable headerTable;
       
   125 
       
   126     private boolean encoding;
       
   127 
       
   128     private int maxCapacity;
       
   129     private int currCapacity;
       
   130     private int lastCapacity;
       
   131     private long minCapacity;
       
   132     private boolean capacityUpdate;
       
   133     private boolean configuredCapacityUpdate;
       
   134 
       
   135     /**
       
   136      * Constructs an {@code Encoder} with the specified maximum capacity of the
       
   137      * header table.
       
   138      *
       
   139      * <p> The value has to be agreed between decoder and encoder out-of-band,
       
   140      * e.g. by a protocol that uses HPACK
       
   141      * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
       
   142      *
       
   143      * @param maxCapacity
       
   144      *         a non-negative integer
       
   145      *
       
   146      * @throws IllegalArgumentException
       
   147      *         if maxCapacity is negative
       
   148      */
       
   149     public Encoder(int maxCapacity) {
       
   150         id = ENCODERS_IDS.incrementAndGet();
       
   151         this.logger = HPACK.getLogger().subLogger("Encoder#" + id);
       
   152         if (logger.isLoggable(NORMAL)) {
       
   153             logger.log(NORMAL, () -> format("new encoder with maximum table size %s",
       
   154                                             maxCapacity));
       
   155         }
       
   156         if (logger.isLoggable(EXTRA)) {
       
   157             /* To correlate with logging outside HPACK, knowing
       
   158                hashCode/toString is important */
       
   159             logger.log(EXTRA, () -> {
       
   160                 String hashCode = Integer.toHexString(
       
   161                         System.identityHashCode(this));
       
   162                 /* Since Encoder can be subclassed hashCode AND identity
       
   163                    hashCode might be different. So let's print both. */
       
   164                 return format("toString='%s', hashCode=%s, identityHashCode=%s",
       
   165                               toString(), hashCode(), hashCode);
       
   166             });
       
   167         }
       
   168         if (maxCapacity < 0) {
       
   169             throw new IllegalArgumentException(
       
   170                     "maxCapacity >= 0: " + maxCapacity);
       
   171         }
       
   172         // Initial maximum capacity update mechanics
       
   173         minCapacity = Long.MAX_VALUE;
       
   174         currCapacity = -1;
       
   175         setMaxCapacity0(maxCapacity);
       
   176         headerTable = new HeaderTable(lastCapacity, logger.subLogger("HeaderTable"));
       
   177     }
       
   178 
       
   179     /**
       
   180      * Sets up the given header {@code (name, value)}.
       
   181      *
       
   182      * <p> Fixates {@code name} and {@code value} for the duration of encoding.
       
   183      *
       
   184      * @param name
       
   185      *         the name
       
   186      * @param value
       
   187      *         the value
       
   188      *
       
   189      * @throws NullPointerException
       
   190      *         if any of the arguments are {@code null}
       
   191      * @throws IllegalStateException
       
   192      *         if the encoder hasn't fully encoded the previous header, or
       
   193      *         hasn't yet started to encode it
       
   194      * @see #header(CharSequence, CharSequence, boolean)
       
   195      */
       
   196     public void header(CharSequence name, CharSequence value)
       
   197             throws IllegalStateException {
       
   198         header(name, value, false);
       
   199     }
       
   200 
       
   201     /**
       
   202      * Sets up the given header {@code (name, value)} with possibly sensitive
       
   203      * value.
       
   204      *
       
   205      * <p> If the {@code value} is sensitive (think security, secrecy, etc.)
       
   206      * this encoder will compress it using a special representation
       
   207      * (see <a href="https://tools.ietf.org/html/rfc7541#section-6.2.3">6.2.3.  Literal Header Field Never Indexed</a>).
       
   208      *
       
   209      * <p> Fixates {@code name} and {@code value} for the duration of encoding.
       
   210      *
       
   211      * @param name
       
   212      *         the name
       
   213      * @param value
       
   214      *         the value
       
   215      * @param sensitive
       
   216      *         whether or not the value is sensitive
       
   217      *
       
   218      * @throws NullPointerException
       
   219      *         if any of the arguments are {@code null}
       
   220      * @throws IllegalStateException
       
   221      *         if the encoder hasn't fully encoded the previous header, or
       
   222      *         hasn't yet started to encode it
       
   223      * @see #header(CharSequence, CharSequence)
       
   224      * @see DecodingCallback#onDecoded(CharSequence, CharSequence, boolean)
       
   225      */
       
   226     public void header(CharSequence name,
       
   227                        CharSequence value,
       
   228                        boolean sensitive) throws IllegalStateException {
       
   229         if (logger.isLoggable(NORMAL)) {
       
   230             logger.log(NORMAL, () -> format("encoding ('%s', '%s'), sensitive: %s",
       
   231                                             name, value, sensitive));
       
   232         }
       
   233         // Arguably a good balance between complexity of implementation and
       
   234         // efficiency of encoding
       
   235         requireNonNull(name, "name");
       
   236         requireNonNull(value, "value");
       
   237         HeaderTable t = getHeaderTable();
       
   238         int index = t.indexOf(name, value);
       
   239         if (index > 0) {
       
   240             indexed(index);
       
   241         } else if (index < 0) {
       
   242             if (sensitive) {
       
   243                 literalNeverIndexed(-index, value, DEFAULT_HUFFMAN);
       
   244             } else {
       
   245                 literal(-index, value, DEFAULT_HUFFMAN);
       
   246             }
       
   247         } else {
       
   248             if (sensitive) {
       
   249                 literalNeverIndexed(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN);
       
   250             } else {
       
   251                 literal(name, DEFAULT_HUFFMAN, value, DEFAULT_HUFFMAN);
       
   252             }
       
   253         }
       
   254     }
       
   255 
       
   256     /**
       
   257      * Sets a maximum capacity of the header table.
       
   258      *
       
   259      * <p> The value has to be agreed between decoder and encoder out-of-band,
       
   260      * e.g. by a protocol that uses HPACK
       
   261      * (see <a href="https://tools.ietf.org/html/rfc7541#section-4.2">4.2. Maximum Table Size</a>).
       
   262      *
       
   263      * <p> May be called any number of times after or before a complete header
       
   264      * has been encoded.
       
   265      *
       
   266      * <p> If the encoder decides to change the actual capacity, an update will
       
   267      * be encoded before a new encoding operation starts.
       
   268      *
       
   269      * @param capacity
       
   270      *         a non-negative integer
       
   271      *
       
   272      * @throws IllegalArgumentException
       
   273      *         if capacity is negative
       
   274      * @throws IllegalStateException
       
   275      *         if the encoder hasn't fully encoded the previous header, or
       
   276      *         hasn't yet started to encode it
       
   277      */
       
   278     public void setMaxCapacity(int capacity) {
       
   279         if (logger.isLoggable(NORMAL)) {
       
   280             logger.log(NORMAL, () -> format("setting maximum table size to %s",
       
   281                                             capacity));
       
   282         }
       
   283         setMaxCapacity0(capacity);
       
   284     }
       
   285 
       
   286     private void setMaxCapacity0(int capacity) {
       
   287         checkEncoding();
       
   288         if (capacity < 0) {
       
   289             throw new IllegalArgumentException("capacity >= 0: " + capacity);
       
   290         }
       
   291         int calculated = calculateCapacity(capacity);
       
   292         if (logger.isLoggable(NORMAL)) {
       
   293             logger.log(NORMAL, () -> format("actual maximum table size will be %s",
       
   294                                             calculated));
       
   295         }
       
   296         if (calculated < 0 || calculated > capacity) {
       
   297             throw new IllegalArgumentException(
       
   298                     format("0 <= calculated <= capacity: calculated=%s, capacity=%s",
       
   299                             calculated, capacity));
       
   300         }
       
   301         capacityUpdate = true;
       
   302         // maxCapacity needs to be updated unconditionally, so the encoder
       
   303         // always has the newest one (in case it decides to update it later
       
   304         // unsolicitedly)
       
   305         // Suppose maxCapacity = 4096, and the encoder has decided to use only
       
   306         // 2048. It later can choose anything else from the region [0, 4096].
       
   307         maxCapacity = capacity;
       
   308         lastCapacity = calculated;
       
   309         minCapacity = Math.min(minCapacity, lastCapacity);
       
   310     }
       
   311 
       
   312     /**
       
   313      * Calculates actual capacity to be used by this encoder in response to
       
   314      * a request to update maximum table size.
       
   315      *
       
   316      * <p> Default implementation does not add anything to the headers table,
       
   317      * hence this method returns {@code 0}.
       
   318      *
       
   319      * <p> It is an error to return a value {@code c}, where {@code c < 0} or
       
   320      * {@code c > maxCapacity}.
       
   321      *
       
   322      * @param maxCapacity
       
   323      *         upper bound
       
   324      *
       
   325      * @return actual capacity
       
   326      */
       
   327     protected int calculateCapacity(int maxCapacity) {
       
   328         return 0;
       
   329     }
       
   330 
       
   331     /**
       
   332      * Encodes the {@linkplain #header(CharSequence, CharSequence) set up}
       
   333      * header into the given buffer.
       
   334      *
       
   335      * <p> The encoder writes as much as possible of the header's binary
       
   336      * representation into the given buffer, starting at the buffer's position,
       
   337      * and increments its position to reflect the bytes written. The buffer's
       
   338      * mark and limit will not be modified.
       
   339      *
       
   340      * <p> Once the method has returned {@code true}, the current header is
       
   341      * deemed encoded. A new header may be set up.
       
   342      *
       
   343      * @param headerBlock
       
   344      *         the buffer to encode the header into, may be empty
       
   345      *
       
   346      * @return {@code true} if the current header has been fully encoded,
       
   347      *         {@code false} otherwise
       
   348      *
       
   349      * @throws NullPointerException
       
   350      *         if the buffer is {@code null}
       
   351      * @throws ReadOnlyBufferException
       
   352      *         if this buffer is read-only
       
   353      * @throws IllegalStateException
       
   354      *         if there is no set up header
       
   355      */
       
   356     public final boolean encode(ByteBuffer headerBlock) {
       
   357         if (!encoding) {
       
   358             throw new IllegalStateException("A header hasn't been set up");
       
   359         }
       
   360         if (logger.isLoggable(EXTRA)) {
       
   361             logger.log(EXTRA, () -> format("writing to %s", headerBlock));
       
   362         }
       
   363         if (!prependWithCapacityUpdate(headerBlock)) { // TODO: log
       
   364             return false;
       
   365         }
       
   366         boolean done = writer.write(headerTable, headerBlock);
       
   367         if (done) {
       
   368             writer.reset(); // FIXME: WHY?
       
   369             encoding = false;
       
   370         }
       
   371         return done;
       
   372     }
       
   373 
       
   374     private boolean prependWithCapacityUpdate(ByteBuffer headerBlock) {
       
   375         if (capacityUpdate) {
       
   376             if (!configuredCapacityUpdate) {
       
   377                 List<Integer> sizes = new LinkedList<>();
       
   378                 if (minCapacity < currCapacity) {
       
   379                     sizes.add((int) minCapacity);
       
   380                     if (minCapacity != lastCapacity) {
       
   381                         sizes.add(lastCapacity);
       
   382                     }
       
   383                 } else if (lastCapacity != currCapacity) {
       
   384                     sizes.add(lastCapacity);
       
   385                 }
       
   386                 bulkSizeUpdateWriter.maxHeaderTableSizes(sizes);
       
   387                 configuredCapacityUpdate = true;
       
   388             }
       
   389             boolean done = bulkSizeUpdateWriter.write(headerTable, headerBlock);
       
   390             if (done) {
       
   391                 minCapacity = lastCapacity;
       
   392                 currCapacity = lastCapacity;
       
   393                 bulkSizeUpdateWriter.reset();
       
   394                 capacityUpdate = false;
       
   395                 configuredCapacityUpdate = false;
       
   396             }
       
   397             return done;
       
   398         }
       
   399         return true;
       
   400     }
       
   401 
       
   402     protected final void indexed(int index) throws IndexOutOfBoundsException {
       
   403         checkEncoding();
       
   404         if (logger.isLoggable(EXTRA)) {
       
   405             logger.log(EXTRA, () -> format("indexed %s", index));
       
   406         }
       
   407         encoding = true;
       
   408         writer = indexedWriter.index(index);
       
   409     }
       
   410 
       
   411     protected final void literal(int index,
       
   412                                  CharSequence value,
       
   413                                  boolean useHuffman)
       
   414             throws IndexOutOfBoundsException {
       
   415         if (logger.isLoggable(EXTRA)) {
       
   416             logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')",
       
   417                                            index, value));
       
   418         }
       
   419         checkEncoding();
       
   420         encoding = true;
       
   421         writer = literalWriter
       
   422                 .index(index).value(value, useHuffman);
       
   423     }
       
   424 
       
   425     protected final void literal(CharSequence name,
       
   426                                  boolean nameHuffman,
       
   427                                  CharSequence value,
       
   428                                  boolean valueHuffman) {
       
   429         if (logger.isLoggable(EXTRA)) {
       
   430             logger.log(EXTRA, () -> format("literal without indexing ('%s', '%s')",
       
   431                                            name, value));
       
   432         }
       
   433         checkEncoding();
       
   434         encoding = true;
       
   435         writer = literalWriter
       
   436                 .name(name, nameHuffman).value(value, valueHuffman);
       
   437     }
       
   438 
       
   439     protected final void literalNeverIndexed(int index,
       
   440                                              CharSequence value,
       
   441                                              boolean valueHuffman)
       
   442             throws IndexOutOfBoundsException {
       
   443         if (logger.isLoggable(EXTRA)) {
       
   444             logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')",
       
   445                                            index, value));
       
   446         }
       
   447         checkEncoding();
       
   448         encoding = true;
       
   449         writer = literalNeverIndexedWriter
       
   450                 .index(index).value(value, valueHuffman);
       
   451     }
       
   452 
       
   453     protected final void literalNeverIndexed(CharSequence name,
       
   454                                              boolean nameHuffman,
       
   455                                              CharSequence value,
       
   456                                              boolean valueHuffman) {
       
   457         if (logger.isLoggable(EXTRA)) {
       
   458             logger.log(EXTRA, () -> format("literal never indexed ('%s', '%s')",
       
   459                                            name, value));
       
   460         }
       
   461         checkEncoding();
       
   462         encoding = true;
       
   463         writer = literalNeverIndexedWriter
       
   464                 .name(name, nameHuffman).value(value, valueHuffman);
       
   465     }
       
   466 
       
   467     protected final void literalWithIndexing(int index,
       
   468                                              CharSequence value,
       
   469                                              boolean valueHuffman)
       
   470             throws IndexOutOfBoundsException {
       
   471         if (logger.isLoggable(EXTRA)) {
       
   472             logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')",
       
   473                                            index, value));
       
   474         }
       
   475         checkEncoding();
       
   476         encoding = true;
       
   477         writer = literalWithIndexingWriter
       
   478                 .index(index).value(value, valueHuffman);
       
   479     }
       
   480 
       
   481     protected final void literalWithIndexing(CharSequence name,
       
   482                                              boolean nameHuffman,
       
   483                                              CharSequence value,
       
   484                                              boolean valueHuffman) {
       
   485         if (logger.isLoggable(EXTRA)) { // TODO: include huffman info?
       
   486             logger.log(EXTRA, () -> format("literal with incremental indexing ('%s', '%s')",
       
   487                                            name, value));
       
   488         }
       
   489         checkEncoding();
       
   490         encoding = true;
       
   491         writer = literalWithIndexingWriter
       
   492                 .name(name, nameHuffman).value(value, valueHuffman);
       
   493     }
       
   494 
       
   495     protected final void sizeUpdate(int capacity)
       
   496             throws IllegalArgumentException {
       
   497         if (logger.isLoggable(EXTRA)) {
       
   498             logger.log(EXTRA, () -> format("dynamic table size update %s",
       
   499                                            capacity));
       
   500         }
       
   501         checkEncoding();
       
   502         // Ensure subclass follows the contract
       
   503         if (capacity > this.maxCapacity) {
       
   504             throw new IllegalArgumentException(
       
   505                     format("capacity <= maxCapacity: capacity=%s, maxCapacity=%s",
       
   506                             capacity, maxCapacity));
       
   507         }
       
   508         writer = sizeUpdateWriter.maxHeaderTableSize(capacity);
       
   509     }
       
   510 
       
   511     protected final int getMaxCapacity() {
       
   512         return maxCapacity;
       
   513     }
       
   514 
       
   515     protected final HeaderTable getHeaderTable() {
       
   516         return headerTable;
       
   517     }
       
   518 
       
   519     protected final void checkEncoding() { // TODO: better name e.g. checkIfEncodingInProgress()
       
   520         if (encoding) {
       
   521             throw new IllegalStateException(
       
   522                     "Previous encoding operation hasn't finished yet");
       
   523         }
       
   524     }
       
   525 }