1 /* 2 * Copyright (c) 2011, 2016, 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 26 package com.sun.webkit.network; 27 28 import com.sun.webkit.Invoker; 29 import com.sun.webkit.LoadListenerClient; 30 import com.sun.webkit.WebPage; 31 import static com.sun.webkit.network.URLs.newURL; 32 import java.io.EOFException; 33 import java.io.File; 34 import java.io.FileNotFoundException; 35 import java.io.IOException; 36 import java.io.InputStream; 37 import java.io.OutputStream; 38 import java.io.UnsupportedEncodingException; 39 import java.net.ConnectException; 40 import java.net.HttpRetryException; 41 import java.net.HttpURLConnection; 42 import java.net.MalformedURLException; 43 import java.net.NoRouteToHostException; 44 import java.net.SocketException; 45 import java.net.SocketTimeoutException; 46 import java.net.URL; 47 import java.net.URLConnection; 48 import java.net.URLDecoder; 49 import java.net.UnknownHostException; 50 import java.nio.ByteBuffer; 51 import java.security.AccessControlException; 52 import java.security.AccessController; 53 import java.security.PrivilegedAction; 54 import java.util.List; 55 import java.util.Locale; 56 import java.util.Map; 57 import java.util.concurrent.CountDownLatch; 58 import java.util.logging.Level; 59 import java.util.logging.Logger; 60 import java.util.zip.GZIPInputStream; 61 import java.util.zip.InflaterInputStream; 62 import javax.net.ssl.SSLHandshakeException; 63 64 /** 65 * A runnable that loads a resource specified by a URL. 66 */ 67 final class URLLoader implements Runnable { 68 69 private static final Logger logger = 70 Logger.getLogger(URLLoader.class.getName()); 71 private static final int MAX_REDIRECTS = 10; 72 private static final int MAX_BUF_COUNT = 3; 73 private static final String GET = "GET"; 74 private static final String HEAD = "HEAD"; 75 private static final String DELETE = "DELETE"; 76 77 78 private final WebPage webPage; 79 private final ByteBufferPool byteBufferPool; 80 private final boolean asynchronous; 81 private String url; 82 private String method; 83 private final String headers; 84 private FormDataElement[] formDataElements; 85 private final long data; 86 private volatile boolean canceled = false; 87 88 89 /** 90 * Creates a new {@code URLLoader}. 91 */ 92 URLLoader(WebPage webPage, 93 ByteBufferPool byteBufferPool, 94 boolean asynchronous, 95 String url, 96 String method, 97 String headers, 98 FormDataElement[] formDataElements, 99 long data) 100 { 101 this.webPage = webPage; 102 this.byteBufferPool = byteBufferPool; 103 this.asynchronous = asynchronous; 104 this.url = url; 105 this.method = method; 106 this.headers = headers; 107 this.formDataElements = formDataElements; 108 this.data = data; 109 } 110 111 112 /** 113 * Cancels this loader. 114 */ 115 private void fwkCancel() { 116 if (logger.isLoggable(Level.FINEST)) { 117 logger.log(Level.FINEST, String.format("data: [0x%016X]", data)); 118 } 119 canceled = true; 120 } 121 122 /** 123 * {@inheritDoc} 124 */ 125 @Override 126 public void run() { 127 // Run the loader in the page's access control context 128 AccessController.doPrivileged((PrivilegedAction<Void>) () -> { 129 doRun(); 130 return null; 131 }, webPage.getAccessControlContext()); 132 } 133 134 /** 135 * Executes this loader. 136 */ 137 private void doRun() { 138 Throwable error = null; 139 int errorCode = 0; 140 try { 141 int redirectCount = 0; 142 boolean streaming = true; 143 while (true) { 144 // RT-14438 145 String actualUrl = url; 146 if (url.startsWith("file:")) { 147 int questionMarkPosition = url.indexOf('?'); 148 if (questionMarkPosition != -1) { 149 actualUrl = url.substring(0, questionMarkPosition); 150 } 151 } 152 153 URL urlObject = newURL(actualUrl); 154 155 // RT-22458 156 workaround7177996(urlObject); 157 158 URLConnection c = urlObject.openConnection(); 159 prepareConnection(c); 160 161 Redirect redirect = null; 162 try { 163 sendRequest(c, streaming); 164 redirect = receiveResponse(c); 165 } catch (HttpRetryException ex) { 166 // RT-19914 167 if (streaming) { 168 streaming = false; 169 continue; // retry without streaming 170 } else { 171 throw ex; 172 } 173 } finally { 174 close(c); 175 } 176 177 if (redirect != null) { 178 if (redirectCount++ >= MAX_REDIRECTS) { 179 throw new TooManyRedirectsException(); 180 } 181 boolean resetRequest = !redirect.preserveRequest 182 && !method.equals(GET) && !method.equals(HEAD); 183 String newMethod = resetRequest ? GET : method; 184 willSendRequest(redirect.url, newMethod, c); 185 // willSendRequest() may cancel this loader 186 if (canceled) { 187 break; 188 } 189 url = redirect.url; 190 method = newMethod; 191 formDataElements = resetRequest ? null : formDataElements; 192 } else { 193 break; 194 } 195 } 196 } catch (MalformedURLException ex) { 197 error = ex; 198 errorCode = LoadListenerClient.MALFORMED_URL; 199 } catch (AccessControlException ex) { 200 error = ex; 201 errorCode = LoadListenerClient.PERMISSION_DENIED; 202 } catch (UnknownHostException ex) { 203 error = ex; 204 errorCode = LoadListenerClient.UNKNOWN_HOST; 205 } catch (NoRouteToHostException ex) { 206 error = ex; 207 errorCode = LoadListenerClient.NO_ROUTE_TO_HOST; 208 } catch (ConnectException ex) { 209 error = ex; 210 errorCode = LoadListenerClient.CONNECTION_REFUSED; 211 } catch (SocketException ex) { 212 error = ex; 213 errorCode = LoadListenerClient.CONNECTION_RESET; 214 } catch (SSLHandshakeException ex) { 215 error = ex; 216 errorCode = LoadListenerClient.SSL_HANDSHAKE; 217 } catch (SocketTimeoutException ex) { 218 error = ex; 219 errorCode = LoadListenerClient.CONNECTION_TIMED_OUT; 220 } catch (InvalidResponseException ex) { 221 error = ex; 222 errorCode = LoadListenerClient.INVALID_RESPONSE; 223 } catch (TooManyRedirectsException ex) { 224 error = ex; 225 errorCode = LoadListenerClient.TOO_MANY_REDIRECTS; 226 } catch (FileNotFoundException ex) { 227 error = ex; 228 errorCode = LoadListenerClient.FILE_NOT_FOUND; 229 } catch (Throwable th) { 230 error = th; 231 errorCode = LoadListenerClient.UNKNOWN_ERROR; 232 } 233 234 if (error != null) { 235 if (errorCode == LoadListenerClient.UNKNOWN_ERROR) { 236 logger.log(Level.WARNING, "Unexpected error", error); 237 } else { 238 logger.log(Level.FINEST, "Load error", error); 239 } 240 didFail(errorCode, error.getMessage()); 241 } 242 } 243 244 private static void workaround7177996(URL url) 245 throws FileNotFoundException 246 { 247 if (!url.getProtocol().equals("file")) { 248 return; 249 } 250 251 String host = url.getHost(); 252 if (host == null || host.equals("") || host.equals("~") 253 || host.equalsIgnoreCase("localhost") ) 254 { 255 return; 256 } 257 258 if (System.getProperty("os.name").startsWith("Windows")) { 259 String path = null; 260 try { 261 path = URLDecoder.decode(url.getPath(), "UTF-8"); 262 } catch (UnsupportedEncodingException e) { 263 // The system should always have the platform default 264 } 265 path = path.replace('/', '\\'); 266 path = path.replace('|', ':'); 267 File file = new File("\\\\" + host + path); 268 if (!file.exists()) { 269 throw new FileNotFoundException("File not found: " + url); 270 } 271 } else { 272 throw new FileNotFoundException("File not found: " + url); 273 } 274 } 275 276 /** 277 * Prepares a connection. 278 */ 279 private void prepareConnection(URLConnection c) throws IOException { 280 // The following two timeouts are quite arbitrary and should 281 // probably be configurable via an API 282 c.setConnectTimeout(30000); // 30 seconds 283 c.setReadTimeout(60000 * 60); // 60 minutes 284 285 // Given that WebKit has its own cache, do not use 286 // any URLConnection caches, even if someone installs them. 287 // As a side effect, this fixes the problem of WebPane not 288 // working well with the plug-in cache, which was one of 289 // the causes for RT-11880. 290 c.setUseCaches(false); 291 292 Locale loc = Locale.getDefault(); 293 String lang = ""; 294 if (!loc.equals(Locale.US) && !loc.equals(Locale.ENGLISH)) { 295 lang = loc.getCountry().isEmpty() ? 296 loc.getLanguage() + ",": 297 loc.getLanguage() + "-" + loc.getCountry() + ","; 298 } 299 c.setRequestProperty("Accept-Language", lang.toLowerCase() + "en-us;q=0.8,en;q=0.7"); 300 c.setRequestProperty("Accept-Encoding", "gzip"); 301 c.setRequestProperty("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7"); 302 303 if (headers != null && headers.length() > 0) { 304 for (String h : headers.split("\n")) { 305 int i = h.indexOf(':'); 306 if (i > 0) { 307 c.addRequestProperty(h.substring(0, i), h.substring(i + 2)); 308 } 309 } 310 } 311 312 if (c instanceof HttpURLConnection) { 313 HttpURLConnection httpConnection = (HttpURLConnection) c; 314 httpConnection.setRequestMethod(method); 315 // There are too many bugs in the way HttpURLConnection handles 316 // redirects, so we will deal with them ourselves 317 httpConnection.setInstanceFollowRedirects(false); 318 } 319 } 320 321 /** 322 * Sends request to the server. 323 */ 324 private void sendRequest(URLConnection c, boolean streaming) 325 throws IOException 326 { 327 OutputStream out = null; 328 try { 329 long bytesToBeSent = 0; 330 boolean sendFormData = formDataElements != null 331 && c instanceof HttpURLConnection 332 && !method.equals(DELETE); 333 boolean isGetOrHead = method.equals(GET) || method.equals(HEAD); 334 if (sendFormData) { 335 c.setDoOutput(true); 336 337 for (FormDataElement formDataElement : formDataElements) { 338 formDataElement.open(); 339 bytesToBeSent += formDataElement.getSize(); 340 } 341 342 if (streaming) { 343 HttpURLConnection http = (HttpURLConnection) c; 344 if (bytesToBeSent <= Integer.MAX_VALUE) { 345 http.setFixedLengthStreamingMode((int) bytesToBeSent); 346 } else { 347 http.setChunkedStreamingMode(0); 348 } 349 } 350 } else if (!isGetOrHead && (c instanceof HttpURLConnection)) { 351 c.setRequestProperty("Content-Length", "0"); 352 } 353 354 int maxTryCount = isGetOrHead ? 3 : 1; 355 c.setConnectTimeout(c.getConnectTimeout() / maxTryCount); 356 int tryCount = 0; 357 while (!canceled) { 358 try { 359 c.connect(); 360 break; 361 } catch (SocketTimeoutException ex) { 362 if (++tryCount >= maxTryCount) { 363 throw ex; 364 } 365 } catch (IllegalArgumentException ex) { 366 // Happens with some malformed URLs 367 throw new MalformedURLException(url); 368 } 369 } 370 371 if (sendFormData) { 372 out = c.getOutputStream(); 373 byte[] buffer = new byte[4096]; 374 long bytesSent = 0; 375 for (FormDataElement formDataElement : formDataElements) { 376 InputStream in = formDataElement.getInputStream(); 377 int count; 378 while ((count = in.read(buffer)) > 0) { 379 out.write(buffer, 0, count); 380 bytesSent += count; 381 didSendData(bytesSent, bytesToBeSent); 382 } 383 formDataElement.close(); 384 } 385 out.flush(); 386 out.close(); 387 out = null; 388 } 389 } finally { 390 if (out != null) { 391 try { 392 out.close(); 393 } catch (IOException ignore) {} 394 } 395 if (formDataElements != null && c instanceof HttpURLConnection) { 396 for (FormDataElement formDataElement : formDataElements) { 397 try { 398 formDataElement.close(); 399 } catch (IOException ignore) {} 400 } 401 } 402 } 403 } 404 405 /** 406 * Receives response from the server. 407 */ 408 private Redirect receiveResponse(URLConnection c) 409 throws IOException, InterruptedException 410 { 411 if (canceled) { 412 return null; 413 } 414 415 InputStream errorStream = null; 416 417 if (c instanceof HttpURLConnection) { 418 HttpURLConnection http = (HttpURLConnection) c; 419 420 int code = http.getResponseCode(); 421 if (code == -1) { 422 throw new InvalidResponseException(); 423 } 424 425 if (canceled) { 426 return null; 427 } 428 429 // See RT-17435 430 switch (code) { 431 case 301: // Moved Permanently 432 case 302: // Found 433 case 303: // See Other 434 case 307: // Temporary Redirect 435 String newLoc = http.getHeaderField("Location"); 436 if (newLoc != null) { 437 URL newUrl; 438 try { 439 newUrl = newURL(newLoc); 440 } catch (MalformedURLException mue) { 441 // Try to treat newLoc as a relative URI to conform 442 // to popular browsers 443 newUrl = newURL(c.getURL(), newLoc); 444 } 445 return new Redirect(newUrl.toExternalForm(), 446 code == 307); 447 } 448 break; 449 450 case 304: // Not Modified 451 didReceiveResponse(c); 452 didFinishLoading(); 453 return null; 454 } 455 456 if (code >= 400 && !method.equals(HEAD)) { 457 errorStream = http.getErrorStream(); 458 } 459 } 460 461 // Let's see if it's an ftp (or ftps) URL and we need to transform 462 // a directory listing into HTML 463 if (url.startsWith("ftp:") || url.startsWith("ftps:")) { 464 boolean dir = false; 465 boolean notsure = false; 466 // Unfortunately, there is no clear way to determine if we are 467 // accessing a directory, so a bit of guessing is in order 468 String path = c.getURL().getPath(); 469 if (path == null || path.isEmpty() || path.endsWith("/") 470 || path.contains(";type=d")) 471 { 472 dir = true; 473 } else { 474 String type = c.getContentType(); 475 if ("text/plain".equalsIgnoreCase(type) 476 || "text/html".equalsIgnoreCase(type)) 477 { 478 dir = true; 479 notsure = true; 480 } 481 } 482 if (dir) { 483 c = new DirectoryURLConnection(c, notsure); 484 } 485 } 486 487 // Same is true for FileURLConnection 488 if (url.startsWith("file:")) { 489 if("text/plain".equals(c.getContentType()) 490 && c.getHeaderField("content-length") == null) 491 { 492 // It is a directory 493 c = new DirectoryURLConnection(c); 494 } 495 } 496 497 didReceiveResponse(c); 498 499 if (method.equals(HEAD)) { 500 didFinishLoading(); 501 return null; 502 } 503 504 InputStream inputStream = null; 505 try { 506 inputStream = errorStream == null 507 ? c.getInputStream() : errorStream; 508 } catch (HttpRetryException ex) { 509 // HttpRetryException is handled from doRun() method. 510 // Hence rethrowing the exception to caller(doRun() method) 511 throw ex; 512 } catch (IOException e) { 513 if (logger.isLoggable(Level.FINE)) { 514 logger.log(Level.FINE, String.format("Exception caught: [%s], %s", 515 e.getClass().getSimpleName(), 516 e.getMessage())); 517 } 518 } 519 520 String encoding = c.getContentEncoding(); 521 if (inputStream != null) { 522 try { 523 if ("gzip".equalsIgnoreCase(encoding)) { 524 inputStream = new GZIPInputStream(inputStream); 525 } else if ("deflate".equalsIgnoreCase(encoding)) { 526 inputStream = new InflaterInputStream(inputStream); 527 } 528 } catch (IOException e) { 529 if (logger.isLoggable(Level.FINE)) { 530 logger.log(Level.FINE, String.format("Exception caught: [%s], %s", 531 e.getClass().getSimpleName(), 532 e.getMessage())); 533 } 534 } 535 } 536 537 ByteBufferAllocator allocator = 538 byteBufferPool.newAllocator(MAX_BUF_COUNT); 539 ByteBuffer byteBuffer = null; 540 try { 541 if (inputStream != null) { 542 // 8192 is the default size of a BufferedInputStream used in 543 // most URLConnections, by using the same size, we avoid quite 544 // a few System.arrayCopy() calls 545 byte[] buffer = new byte[8192]; 546 while (!canceled) { 547 int count; 548 try { 549 count = inputStream.read(buffer); 550 } catch (EOFException ex) { 551 // can be thrown by GZIPInputStream signaling 552 // the end of the stream 553 count = -1; 554 } 555 556 if (count == -1) { 557 break; 558 } 559 560 if (byteBuffer == null) { 561 byteBuffer = allocator.allocate(); 562 } 563 564 int remaining = byteBuffer.remaining(); 565 if (count < remaining) { 566 byteBuffer.put(buffer, 0, count); 567 } else { 568 byteBuffer.put(buffer, 0, remaining); 569 570 byteBuffer.flip(); 571 didReceiveData(byteBuffer, allocator); 572 byteBuffer = null; 573 574 int outstanding = count - remaining; 575 if (outstanding > 0) { 576 byteBuffer = allocator.allocate(); 577 byteBuffer.put(buffer, remaining, outstanding); 578 } 579 } 580 } 581 } 582 if (!canceled) { 583 if (byteBuffer != null && byteBuffer.position() > 0) { 584 byteBuffer.flip(); 585 didReceiveData(byteBuffer, allocator); 586 byteBuffer = null; 587 } 588 didFinishLoading(); 589 } 590 } finally { 591 if (byteBuffer != null) { 592 byteBuffer.clear(); 593 allocator.release(byteBuffer); 594 } 595 } 596 return null; 597 } 598 599 /** 600 * Releases the resources that may be associated with a connection. 601 */ 602 private static void close(URLConnection c) { 603 if (c instanceof HttpURLConnection) { 604 InputStream errorStream = ((HttpURLConnection) c).getErrorStream(); 605 if (errorStream != null) { 606 try { 607 errorStream.close(); 608 } catch (IOException ignore) {} 609 } 610 } 611 try { 612 c.getInputStream().close(); 613 } catch (IOException ignore) {} 614 } 615 616 617 /** 618 * A holder for redirect information. 619 */ 620 private static final class Redirect { 621 private final String url; 622 private final boolean preserveRequest; 623 624 private Redirect(String url, boolean preserveRequest) { 625 this.url = url; 626 this.preserveRequest = preserveRequest; 627 } 628 } 629 630 /** 631 * Signals an invalid response from the server. 632 */ 633 private static final class InvalidResponseException extends IOException { 634 private InvalidResponseException() { 635 super("Invalid server response"); 636 } 637 } 638 639 /** 640 * Signals that too many redirects have been encountered 641 * while processing the request. 642 */ 643 private static final class TooManyRedirectsException extends IOException { 644 private TooManyRedirectsException() { 645 super("Too many redirects"); 646 } 647 } 648 649 private void didSendData(final long totalBytesSent, 650 final long totalBytesToBeSent) 651 { 652 callBack(() -> { 653 if (!canceled) { 654 notifyDidSendData(totalBytesSent, totalBytesToBeSent); 655 } 656 }); 657 } 658 659 private void notifyDidSendData(long totalBytesSent, 660 long totalBytesToBeSent) 661 { 662 if (logger.isLoggable(Level.FINEST)) { 663 logger.log(Level.FINEST, String.format( 664 "totalBytesSent: [%d], " 665 + "totalBytesToBeSent: [%d], " 666 + "data: [0x%016X]", 667 totalBytesSent, 668 totalBytesToBeSent, 669 data)); 670 } 671 twkDidSendData(totalBytesSent, totalBytesToBeSent, data); 672 } 673 674 private void willSendRequest(String newUrl, 675 final String newMethod, 676 URLConnection c) throws InterruptedException 677 { 678 final String adjustedNewUrl = adjustUrlForWebKit(newUrl); 679 final int status = extractStatus(c); 680 final String contentType = c.getContentType(); 681 final String contentEncoding = extractContentEncoding(c); 682 final long contentLength = extractContentLength(c); 683 final String responseHeaders = extractHeaders(c); 684 final String adjustedUrl = adjustUrlForWebKit(url); 685 final CountDownLatch latch = 686 asynchronous ? new CountDownLatch(1) : null; 687 callBack(() -> { 688 try { 689 if (!canceled) { 690 boolean keepGoing = notifyWillSendRequest( 691 adjustedNewUrl, 692 newMethod, 693 status, 694 contentType, 695 contentEncoding, 696 contentLength, 697 responseHeaders, 698 adjustedUrl); 699 if (!keepGoing) { 700 fwkCancel(); 701 } 702 } 703 } finally { 704 if (latch != null) { 705 latch.countDown(); 706 } 707 } 708 }); 709 if (latch != null) { 710 latch.await(); 711 } 712 } 713 714 private boolean notifyWillSendRequest(String newUrl, 715 String newMethod, 716 int status, 717 String contentType, 718 String contentEncoding, 719 long contentLength, 720 String headers, 721 String url) 722 { 723 if (logger.isLoggable(Level.FINEST)) { 724 logger.log(Level.FINEST, String.format( 725 "newUrl: [%s], " 726 + "newMethod: [%s], " 727 + "status: [%d], " 728 + "contentType: [%s], " 729 + "contentEncoding: [%s], " 730 + "contentLength: [%d], " 731 + "url: [%s], " 732 + "data: [0x%016X], " 733 + "headers:%n%s", 734 newUrl, 735 newMethod, 736 status, 737 contentType, 738 contentEncoding, 739 contentLength, 740 url, 741 data, 742 Util.formatHeaders(headers))); 743 } 744 boolean result = twkWillSendRequest( 745 newUrl, 746 newMethod, 747 status, 748 contentType, 749 contentEncoding, 750 contentLength, 751 headers, 752 url, 753 data); 754 if (logger.isLoggable(Level.FINEST)) { 755 logger.log(Level.FINEST, String.format("result: [%s]", result)); 756 } 757 return result; 758 } 759 760 private void didReceiveResponse(URLConnection c) { 761 final int status = extractStatus(c); 762 final String contentType = c.getContentType(); 763 final String contentEncoding = extractContentEncoding(c); 764 final long contentLength = extractContentLength(c); 765 final String responseHeaders = extractHeaders(c); 766 final String adjustedUrl = adjustUrlForWebKit(url); 767 callBack(() -> { 768 if (!canceled) { 769 notifyDidReceiveResponse( 770 status, 771 contentType, 772 contentEncoding, 773 contentLength, 774 responseHeaders, 775 adjustedUrl); 776 } 777 }); 778 } 779 780 private void notifyDidReceiveResponse(int status, 781 String contentType, 782 String contentEncoding, 783 long contentLength, 784 String headers, 785 String url) 786 { 787 if (logger.isLoggable(Level.FINEST)) { 788 logger.log(Level.FINEST, String.format( 789 "status: [%d], " 790 + "contentType: [%s], " 791 + "contentEncoding: [%s], " 792 + "contentLength: [%d], " 793 + "url: [%s], " 794 + "data: [0x%016X], " 795 + "headers:%n%s", 796 status, 797 contentType, 798 contentEncoding, 799 contentLength, 800 url, 801 data, 802 Util.formatHeaders(headers))); 803 } 804 twkDidReceiveResponse( 805 status, 806 contentType, 807 contentEncoding, 808 contentLength, 809 headers, 810 url, 811 data); 812 } 813 814 private void didReceiveData(final ByteBuffer byteBuffer, 815 final ByteBufferAllocator allocator) 816 { 817 callBack(() -> { 818 if (!canceled) { 819 notifyDidReceiveData( 820 byteBuffer, 821 byteBuffer.position(), 822 byteBuffer.remaining()); 823 } 824 byteBuffer.clear(); 825 allocator.release(byteBuffer); 826 }); 827 } 828 829 private void notifyDidReceiveData(ByteBuffer byteBuffer, 830 int position, 831 int remaining) 832 { 833 if (logger.isLoggable(Level.FINEST)) { 834 logger.log(Level.FINEST, String.format( 835 "byteBuffer: [%s], " 836 + "position: [%s], " 837 + "remaining: [%s], " 838 + "data: [0x%016X]", 839 byteBuffer, 840 position, 841 remaining, 842 data)); 843 } 844 twkDidReceiveData(byteBuffer, position, remaining, data); 845 } 846 847 private void didFinishLoading() { 848 callBack(() -> { 849 if (!canceled) { 850 notifyDidFinishLoading(); 851 } 852 }); 853 } 854 855 private void notifyDidFinishLoading() { 856 if (logger.isLoggable(Level.FINEST)) { 857 logger.log(Level.FINEST, String.format("data: [0x%016X]", data)); 858 } 859 twkDidFinishLoading(data); 860 } 861 862 private void didFail(final int errorCode, final String message) { 863 final String adjustedUrl = adjustUrlForWebKit(url); 864 callBack(() -> { 865 if (!canceled) { 866 notifyDidFail(errorCode, adjustedUrl, message); 867 } 868 }); 869 } 870 871 private void notifyDidFail(int errorCode, String url, String message) { 872 if (logger.isLoggable(Level.FINEST)) { 873 logger.log(Level.FINEST, String.format( 874 "errorCode: [%d], " 875 + "url: [%s], " 876 + "message: [%s], " 877 + "data: [0x%016X]", 878 errorCode, 879 url, 880 message, 881 data)); 882 } 883 twkDidFail(errorCode, url, message, data); 884 } 885 886 private void callBack(Runnable runnable) { 887 if (asynchronous) { 888 Invoker.getInvoker().invokeOnEventThread(runnable); 889 } else { 890 runnable.run(); 891 } 892 } 893 894 private static native void twkDidSendData(long totalBytesSent, 895 long totalBytesToBeSent, 896 long data); 897 898 private static native boolean twkWillSendRequest(String newUrl, 899 String newMethod, 900 int status, 901 String contentType, 902 String contentEncoding, 903 long contentLength, 904 String headers, 905 String url, 906 long data); 907 908 private static native void twkDidReceiveResponse(int status, 909 String contentType, 910 String contentEncoding, 911 long contentLength, 912 String headers, 913 String url, 914 long data); 915 916 private static native void twkDidReceiveData(ByteBuffer byteBuffer, 917 int position, 918 int remaining, 919 long data); 920 921 private static native void twkDidFinishLoading(long data); 922 923 private static native void twkDidFail(int errorCode, 924 String url, 925 String message, 926 long data); 927 928 /** 929 * Given a {@link URLConnection}, returns the connection status 930 * for passing into native callbacks. 931 */ 932 private static int extractStatus(URLConnection c) { 933 int status = 0; 934 if (c instanceof HttpURLConnection) { 935 try { 936 status = ((HttpURLConnection) c).getResponseCode(); 937 } catch (java.io.IOException ignore) {} 938 } 939 return status; 940 } 941 942 /** 943 * Given a {@link URLConnection}, returns the content encoding 944 * for passing into native callbacks. 945 */ 946 private static String extractContentEncoding(URLConnection c) { 947 String contentEncoding = c.getContentEncoding(); 948 // For compressed streams, the encoding is in Content-Type 949 if ("gzip".equalsIgnoreCase(contentEncoding) || 950 "deflate".equalsIgnoreCase(contentEncoding)) 951 { 952 contentEncoding = null; 953 String contentType = c.getContentType(); 954 if (contentType != null) { 955 int i = contentType.indexOf("charset="); 956 if (i >= 0) { 957 contentEncoding = contentType.substring(i + 8); 958 i = contentEncoding.indexOf(";"); 959 if (i > 0) { 960 contentEncoding = contentEncoding.substring(0, i); 961 } 962 } 963 } 964 } 965 return contentEncoding; 966 } 967 968 /** 969 * Given a {@link URLConnection}, returns the content length 970 * for passing into native callbacks. 971 */ 972 private static long extractContentLength(URLConnection c) { 973 // Cannot use URLConnection.getContentLength() 974 // as it only returns an int 975 try { 976 return Long.parseLong(c.getHeaderField("content-length")); 977 } catch (Exception ex) { 978 return -1; 979 } 980 } 981 982 /** 983 * Given a {@link URLConnection}, returns the headers string 984 * for passing into native callbacks. 985 */ 986 private static String extractHeaders(URLConnection c) { 987 StringBuilder sb = new StringBuilder(); 988 Map<String, List<String>> headers = c.getHeaderFields(); 989 for (Map.Entry<String, List<String>> entry: headers.entrySet()) { 990 String key = entry.getKey(); 991 List<String> values = entry.getValue(); 992 for (String value : values) { 993 sb.append(key != null ? key : ""); 994 sb.append(':').append(value).append('\n'); 995 } 996 } 997 return sb.toString(); 998 } 999 1000 /** 1001 * Adjust a URL string for passing into WebKit. 1002 */ 1003 private static String adjustUrlForWebKit(String url) { 1004 try { 1005 url = Util.adjustUrlForWebKit(url); 1006 } catch (Exception ignore) { 1007 } 1008 return url; 1009 } 1010 }