src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/internal/hpack/HeaderTable.java
branchhttp-client-branch
changeset 56089 42208b2f224e
parent 56088 38fac6d0521d
child 56090 5c7fb702948a
equal deleted inserted replaced
56088:38fac6d0521d 56089:42208b2f224e
     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.incubator.http.internal.hpack;
       
    26 
       
    27 import jdk.incubator.http.internal.hpack.HPACK.Logger;
       
    28 import jdk.internal.vm.annotation.Stable;
       
    29 
       
    30 import java.util.HashMap;
       
    31 import java.util.LinkedHashMap;
       
    32 import java.util.Map;
       
    33 import java.util.NoSuchElementException;
       
    34 
       
    35 import static java.lang.String.format;
       
    36 import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.EXTRA;
       
    37 import static jdk.incubator.http.internal.hpack.HPACK.Logger.Level.NORMAL;
       
    38 
       
    39 //
       
    40 // Header Table combined from two tables: static and dynamic.
       
    41 //
       
    42 // There is a single address space for index values. Index-aware methods
       
    43 // correspond to the table as a whole. Size-aware methods only to the dynamic
       
    44 // part of it.
       
    45 //
       
    46 final class HeaderTable {
       
    47 
       
    48     @Stable
       
    49     private static final HeaderField[] staticTable = {
       
    50             null, // To make index 1-based, instead of 0-based
       
    51             new HeaderField(":authority"),
       
    52             new HeaderField(":method", "GET"),
       
    53             new HeaderField(":method", "POST"),
       
    54             new HeaderField(":path", "/"),
       
    55             new HeaderField(":path", "/index.html"),
       
    56             new HeaderField(":scheme", "http"),
       
    57             new HeaderField(":scheme", "https"),
       
    58             new HeaderField(":status", "200"),
       
    59             new HeaderField(":status", "204"),
       
    60             new HeaderField(":status", "206"),
       
    61             new HeaderField(":status", "304"),
       
    62             new HeaderField(":status", "400"),
       
    63             new HeaderField(":status", "404"),
       
    64             new HeaderField(":status", "500"),
       
    65             new HeaderField("accept-charset"),
       
    66             new HeaderField("accept-encoding", "gzip, deflate"),
       
    67             new HeaderField("accept-language"),
       
    68             new HeaderField("accept-ranges"),
       
    69             new HeaderField("accept"),
       
    70             new HeaderField("access-control-allow-origin"),
       
    71             new HeaderField("age"),
       
    72             new HeaderField("allow"),
       
    73             new HeaderField("authorization"),
       
    74             new HeaderField("cache-control"),
       
    75             new HeaderField("content-disposition"),
       
    76             new HeaderField("content-encoding"),
       
    77             new HeaderField("content-language"),
       
    78             new HeaderField("content-length"),
       
    79             new HeaderField("content-location"),
       
    80             new HeaderField("content-range"),
       
    81             new HeaderField("content-type"),
       
    82             new HeaderField("cookie"),
       
    83             new HeaderField("date"),
       
    84             new HeaderField("etag"),
       
    85             new HeaderField("expect"),
       
    86             new HeaderField("expires"),
       
    87             new HeaderField("from"),
       
    88             new HeaderField("host"),
       
    89             new HeaderField("if-match"),
       
    90             new HeaderField("if-modified-since"),
       
    91             new HeaderField("if-none-match"),
       
    92             new HeaderField("if-range"),
       
    93             new HeaderField("if-unmodified-since"),
       
    94             new HeaderField("last-modified"),
       
    95             new HeaderField("link"),
       
    96             new HeaderField("location"),
       
    97             new HeaderField("max-forwards"),
       
    98             new HeaderField("proxy-authenticate"),
       
    99             new HeaderField("proxy-authorization"),
       
   100             new HeaderField("range"),
       
   101             new HeaderField("referer"),
       
   102             new HeaderField("refresh"),
       
   103             new HeaderField("retry-after"),
       
   104             new HeaderField("server"),
       
   105             new HeaderField("set-cookie"),
       
   106             new HeaderField("strict-transport-security"),
       
   107             new HeaderField("transfer-encoding"),
       
   108             new HeaderField("user-agent"),
       
   109             new HeaderField("vary"),
       
   110             new HeaderField("via"),
       
   111             new HeaderField("www-authenticate")
       
   112     };
       
   113 
       
   114     private static final int STATIC_TABLE_LENGTH = staticTable.length - 1;
       
   115     private static final int ENTRY_SIZE = 32;
       
   116     private static final Map<String, LinkedHashMap<String, Integer>> staticIndexes;
       
   117 
       
   118     static {
       
   119         staticIndexes = new HashMap<>(STATIC_TABLE_LENGTH); // TODO: Map.of
       
   120         for (int i = 1; i <= STATIC_TABLE_LENGTH; i++) {
       
   121             HeaderField f = staticTable[i];
       
   122             Map<String, Integer> values = staticIndexes
       
   123                     .computeIfAbsent(f.name, k -> new LinkedHashMap<>());
       
   124             values.put(f.value, i);
       
   125         }
       
   126     }
       
   127 
       
   128     private final Logger logger;
       
   129     private final Table dynamicTable = new Table(0);
       
   130     private int maxSize;
       
   131     private int size;
       
   132 
       
   133     public HeaderTable(int maxSize, Logger logger) {
       
   134         this.logger = logger;
       
   135         setMaxSize(maxSize);
       
   136     }
       
   137 
       
   138     //
       
   139     // The method returns:
       
   140     //
       
   141     // * a positive integer i where i (i = [1..Integer.MAX_VALUE]) is an
       
   142     // index of an entry with a header (n, v), where n.equals(name) &&
       
   143     // v.equals(value)
       
   144     //
       
   145     // * a negative integer j where j (j = [-Integer.MAX_VALUE..-1]) is an
       
   146     // index of an entry with a header (n, v), where n.equals(name)
       
   147     //
       
   148     // * 0 if there's no entry e such that e.getName().equals(name)
       
   149     //
       
   150     // The rationale behind this design is to allow to pack more useful data
       
   151     // into a single invocation, facilitating a single pass where possible
       
   152     // (the idea is the same as in java.util.Arrays.binarySearch(int[], int)).
       
   153     //
       
   154     public int indexOf(CharSequence name, CharSequence value) {
       
   155         // Invoking toString() will possibly allocate Strings for the sake of
       
   156         // the search, which doesn't feel right.
       
   157         String n = name.toString();
       
   158         String v = value.toString();
       
   159 
       
   160         // 1. Try exact match in the static region
       
   161         Map<String, Integer> values = staticIndexes.get(n);
       
   162         if (values != null) {
       
   163             Integer idx = values.get(v);
       
   164             if (idx != null) {
       
   165                 return idx;
       
   166             }
       
   167         }
       
   168         // 2. Try exact match in the dynamic region
       
   169         int didx = dynamicTable.indexOf(n, v);
       
   170         if (didx > 0) {
       
   171             return STATIC_TABLE_LENGTH + didx;
       
   172         } else if (didx < 0) {
       
   173             if (values != null) {
       
   174                 // 3. Return name match from the static region
       
   175                 return -values.values().iterator().next(); // Iterator allocation
       
   176             } else {
       
   177                 // 4. Return name match from the dynamic region
       
   178                 return -STATIC_TABLE_LENGTH + didx;
       
   179             }
       
   180         } else {
       
   181             if (values != null) {
       
   182                 // 3. Return name match from the static region
       
   183                 return -values.values().iterator().next(); // Iterator allocation
       
   184             } else {
       
   185                 return 0;
       
   186             }
       
   187         }
       
   188     }
       
   189 
       
   190     public int size() {
       
   191         return size;
       
   192     }
       
   193 
       
   194     public int maxSize() {
       
   195         return maxSize;
       
   196     }
       
   197 
       
   198     public int length() {
       
   199         return STATIC_TABLE_LENGTH + dynamicTable.size();
       
   200     }
       
   201 
       
   202     HeaderField get(int index) {
       
   203         checkIndex(index);
       
   204         if (index <= STATIC_TABLE_LENGTH) {
       
   205             return staticTable[index];
       
   206         } else {
       
   207             return dynamicTable.get(index - STATIC_TABLE_LENGTH);
       
   208         }
       
   209     }
       
   210 
       
   211     void put(CharSequence name, CharSequence value) {
       
   212         // Invoking toString() will possibly allocate Strings. But that's
       
   213         // unavoidable at this stage. If a CharSequence is going to be stored in
       
   214         // the table, it must not be mutable (e.g. for the sake of hashing).
       
   215         put(new HeaderField(name.toString(), value.toString()));
       
   216     }
       
   217 
       
   218     private void put(HeaderField h) {
       
   219         if (logger.isLoggable(NORMAL)) {
       
   220             logger.log(NORMAL, () -> format("adding ('%s', '%s')",
       
   221                                             h.name, h.value));
       
   222         }
       
   223         int entrySize = sizeOf(h);
       
   224         if (logger.isLoggable(EXTRA)) {
       
   225             logger.log(EXTRA, () -> format("size of ('%s', '%s') is %s",
       
   226                                            h.name, h.value, entrySize));
       
   227         }
       
   228         while (entrySize > maxSize - size && size != 0) {
       
   229             if (logger.isLoggable(EXTRA)) {
       
   230                 logger.log(EXTRA, () -> format("insufficient space %s, must evict entry",
       
   231                                                (maxSize - size)));
       
   232             }
       
   233             evictEntry();
       
   234         }
       
   235         if (entrySize > maxSize - size) {
       
   236             if (logger.isLoggable(EXTRA)) {
       
   237                 logger.log(EXTRA, () -> format("not adding ('%s, '%s'), too big",
       
   238                                                h.name, h.value));
       
   239             }
       
   240             return;
       
   241         }
       
   242         size += entrySize;
       
   243         dynamicTable.add(h);
       
   244         if (logger.isLoggable(EXTRA)) {
       
   245             logger.log(EXTRA, () -> format("('%s, '%s') added", h.name, h.value));
       
   246             logger.log(EXTRA, this::toString);
       
   247         }
       
   248     }
       
   249 
       
   250     void setMaxSize(int maxSize) {
       
   251         if (maxSize < 0) {
       
   252             throw new IllegalArgumentException(
       
   253                     "maxSize >= 0: maxSize=" + maxSize);
       
   254         }
       
   255         while (maxSize < size && size != 0) {
       
   256             evictEntry();
       
   257         }
       
   258         this.maxSize = maxSize;
       
   259         int upperBound = (maxSize / ENTRY_SIZE) + 1;
       
   260         this.dynamicTable.setCapacity(upperBound);
       
   261     }
       
   262 
       
   263     HeaderField evictEntry() {
       
   264         HeaderField f = dynamicTable.remove();
       
   265         int s = sizeOf(f);
       
   266         this.size -= s;
       
   267         if (logger.isLoggable(EXTRA)) {
       
   268             logger.log(EXTRA, () -> format("evicted entry ('%s', '%s') of size %s",
       
   269                                            f.name, f.value, s));
       
   270             logger.log(EXTRA, this::toString);
       
   271         }
       
   272         return f;
       
   273     }
       
   274 
       
   275     @Override
       
   276     public String toString() {
       
   277         double used = maxSize == 0 ? 0 : 100 * (((double) size) / maxSize);
       
   278         return format("dynamic length: %d, full length: %s, used space: %s/%s (%.1f%%)",
       
   279                       dynamicTable.size(), length(), size, maxSize, used);
       
   280     }
       
   281 
       
   282     private int checkIndex(int index) {
       
   283         int len = length();
       
   284         if (index < 1 || index > len) {
       
   285             throw new IndexOutOfBoundsException(
       
   286                     format("1 <= index <= length(): index=%s, length()=%s",
       
   287                            index, len));
       
   288         }
       
   289         return index;
       
   290     }
       
   291 
       
   292     int sizeOf(HeaderField f) {
       
   293         return f.name.length() + f.value.length() + ENTRY_SIZE;
       
   294     }
       
   295 
       
   296     //
       
   297     // Diagnostic information in the form used in the RFC 7541
       
   298     //
       
   299     String getStateString() {
       
   300         if (size == 0) {
       
   301             return "empty.";
       
   302         }
       
   303 
       
   304         StringBuilder b = new StringBuilder();
       
   305         for (int i = 1, size = dynamicTable.size(); i <= size; i++) {
       
   306             HeaderField e = dynamicTable.get(i);
       
   307             b.append(format("[%3d] (s = %3d) %s: %s\n", i,
       
   308                     sizeOf(e), e.name, e.value));
       
   309         }
       
   310         b.append(format("      Table size:%4s", this.size));
       
   311         return b.toString();
       
   312     }
       
   313 
       
   314     // Convert to a Value Object (JDK-8046159)?
       
   315     static final class HeaderField {
       
   316 
       
   317         final String name;
       
   318         final String value;
       
   319 
       
   320         public HeaderField(String name) {
       
   321             this(name, "");
       
   322         }
       
   323 
       
   324         public HeaderField(String name, String value) {
       
   325             this.name = name;
       
   326             this.value = value;
       
   327         }
       
   328 
       
   329         @Override
       
   330         public String toString() {
       
   331             return value.isEmpty() ? name : name + ": " + value;
       
   332         }
       
   333 
       
   334         @Override
       
   335         public boolean equals(Object o) {
       
   336             if (this == o) {
       
   337                 return true;
       
   338             }
       
   339             if (o == null || getClass() != o.getClass()) {
       
   340                 return false;
       
   341             }
       
   342             HeaderField that = (HeaderField) o;
       
   343             return name.equals(that.name) && value.equals(that.value);
       
   344         }
       
   345 
       
   346         @Override
       
   347         public int hashCode() {
       
   348             return 31 * name.hashCode() + value.hashCode();
       
   349         }
       
   350     }
       
   351 
       
   352     //
       
   353     // To quickly find an index of an entry in the dynamic table with the given
       
   354     // contents an effective inverse mapping is needed. Here's a simple idea
       
   355     // behind such a mapping.
       
   356     //
       
   357     // # The problem:
       
   358     //
       
   359     // We have a queue with an O(1) lookup by index:
       
   360     //
       
   361     //     get: index -> x
       
   362     //
       
   363     // What we want is an O(1) reverse lookup:
       
   364     //
       
   365     //     indexOf: x -> index
       
   366     //
       
   367     // # Solution:
       
   368     //
       
   369     // Let's store an inverse mapping in a Map<x, Integer>. This have a problem
       
   370     // that when a new element is added to the queue, all indexes in the map
       
   371     // become invalid. Namely, the new element is assigned with an index of 1,
       
   372     // and each index i, i > 1 becomes shifted by 1 to the left:
       
   373     //
       
   374     //     1, 1, 2, 3, ... , n-1, n
       
   375     //
       
   376     // Re-establishing the invariant would seem to require a pass through the
       
   377     // map incrementing all indexes (map values) by 1, which is O(n).
       
   378     //
       
   379     // The good news is we can do much better then this!
       
   380     //
       
   381     // Let's create a single field of type long, called 'counter'. Then each
       
   382     // time a new element 'x' is added to the queue, a value of this field gets
       
   383     // incremented. Then the resulting value of the 'counter_x' is then put as a
       
   384     // value under key 'x' to the map:
       
   385     //
       
   386     //    map.put(x, counter_x)
       
   387     //
       
   388     // It gives us a map that maps an element to a value the counter had at the
       
   389     // time the element had been added.
       
   390     //
       
   391     // In order to retrieve an index of any element 'x' in the queue (at any
       
   392     // given time) we simply need to subtract the value (the snapshot of the
       
   393     // counter at the time when the 'x' was added) from the current value of the
       
   394     // counter. This operation basically answers the question:
       
   395     //
       
   396     //     How many elements ago 'x' was the tail of the queue?
       
   397     //
       
   398     // Which is the same as its index in the queue now. Given, of course, it's
       
   399     // still in the queue.
       
   400     //
       
   401     // I'm pretty sure in a real life long overflow will never happen, so it's
       
   402     // not too practical to add recalibrating code, but a pedantic person might
       
   403     // want to do so:
       
   404     //
       
   405     //     if (counter == Long.MAX_VALUE) {
       
   406     //         recalibrate();
       
   407     //     }
       
   408     //
       
   409     // Where 'recalibrate()' goes through the table doing this:
       
   410     //
       
   411     //     value -= counter
       
   412     //
       
   413     // That's given, of course, the size of the table itself is less than
       
   414     // Long.MAX_VALUE :-)
       
   415     //
       
   416     private static final class Table {
       
   417 
       
   418         private final Map<String, Map<String, Long>> map;
       
   419         private final CircularBuffer<HeaderField> buffer;
       
   420         private long counter = 1;
       
   421 
       
   422         Table(int capacity) {
       
   423             buffer = new CircularBuffer<>(capacity);
       
   424             map = new HashMap<>(capacity);
       
   425         }
       
   426 
       
   427         void add(HeaderField f) {
       
   428             buffer.add(f);
       
   429             Map<String, Long> values = map.computeIfAbsent(f.name, k -> new HashMap<>());
       
   430             values.put(f.value, counter++);
       
   431         }
       
   432 
       
   433         HeaderField get(int index) {
       
   434             return buffer.get(index - 1);
       
   435         }
       
   436 
       
   437         int indexOf(String name, String value) {
       
   438             Map<String, Long> values = map.get(name);
       
   439             if (values == null) {
       
   440                 return 0;
       
   441             }
       
   442             Long index = values.get(value);
       
   443             if (index != null) {
       
   444                 return (int) (counter - index);
       
   445             } else {
       
   446                 assert !values.isEmpty();
       
   447                 Long any = values.values().iterator().next(); // Iterator allocation
       
   448                 return -(int) (counter - any);
       
   449             }
       
   450         }
       
   451 
       
   452         HeaderField remove() {
       
   453             HeaderField f = buffer.remove();
       
   454             Map<String, Long> values = map.get(f.name);
       
   455             Long index = values.remove(f.value);
       
   456             assert index != null;
       
   457             if (values.isEmpty()) {
       
   458                 map.remove(f.name);
       
   459             }
       
   460             return f;
       
   461         }
       
   462 
       
   463         int size() {
       
   464             return buffer.size;
       
   465         }
       
   466 
       
   467         public void setCapacity(int capacity) {
       
   468             buffer.resize(capacity);
       
   469         }
       
   470     }
       
   471 
       
   472     //                    head
       
   473     //                    v
       
   474     // [ ][ ][A][B][C][D][ ][ ][ ]
       
   475     //        ^
       
   476     //        tail
       
   477     //
       
   478     //       |<- size ->| (4)
       
   479     // |<------ capacity ------->| (9)
       
   480     //
       
   481     static final class CircularBuffer<E> {
       
   482 
       
   483         int tail, head, size, capacity;
       
   484         Object[] elements;
       
   485 
       
   486         CircularBuffer(int capacity) {
       
   487             this.capacity = capacity;
       
   488             elements = new Object[capacity];
       
   489         }
       
   490 
       
   491         void add(E elem) {
       
   492             if (size == capacity) {
       
   493                 throw new IllegalStateException(
       
   494                         format("No room for '%s': capacity=%s", elem, capacity));
       
   495             }
       
   496             elements[head] = elem;
       
   497             head = (head + 1) % capacity;
       
   498             size++;
       
   499         }
       
   500 
       
   501         @SuppressWarnings("unchecked")
       
   502         E remove() {
       
   503             if (size == 0) {
       
   504                 throw new NoSuchElementException("Empty");
       
   505             }
       
   506             E elem = (E) elements[tail];
       
   507             elements[tail] = null;
       
   508             tail = (tail + 1) % capacity;
       
   509             size--;
       
   510             return elem;
       
   511         }
       
   512 
       
   513         @SuppressWarnings("unchecked")
       
   514         E get(int index) {
       
   515             if (index < 0 || index >= size) {
       
   516                 throw new IndexOutOfBoundsException(
       
   517                         format("0 <= index <= capacity: index=%s, capacity=%s",
       
   518                                 index, capacity));
       
   519             }
       
   520             int idx = (tail + (size - index - 1)) % capacity;
       
   521             return (E) elements[idx];
       
   522         }
       
   523 
       
   524         public void resize(int newCapacity) {
       
   525             if (newCapacity < size) {
       
   526                 throw new IllegalStateException(
       
   527                         format("newCapacity >= size: newCapacity=%s, size=%s",
       
   528                                 newCapacity, size));
       
   529             }
       
   530 
       
   531             Object[] newElements = new Object[newCapacity];
       
   532 
       
   533             if (tail < head || size == 0) {
       
   534                 System.arraycopy(elements, tail, newElements, 0, size);
       
   535             } else {
       
   536                 System.arraycopy(elements, tail, newElements, 0, elements.length - tail);
       
   537                 System.arraycopy(elements, 0, newElements, elements.length - tail, head);
       
   538             }
       
   539 
       
   540             elements = newElements;
       
   541             tail = 0;
       
   542             head = size;
       
   543             this.capacity = newCapacity;
       
   544         }
       
   545     }
       
   546 }