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