1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package tests.support; 18 19import java.io.*; 20import java.lang.Thread; 21import java.net.*; 22import java.text.SimpleDateFormat; 23import java.util.*; 24import java.util.concurrent.ConcurrentHashMap; 25import java.util.logging.Logger; 26 27/** 28 * TestWebServer is a simulated controllable test server that 29 * can respond to requests from HTTP clients. 30 * 31 * The server can be controlled to change how it reacts to any 32 * requests, and can be told to simulate various events (such as 33 * network failure) that would happen in a real environment. 34 */ 35public class Support_TestWebServer implements Support_HttpConstants { 36 37 /* static class data/methods */ 38 39 /* The ANDROID_LOG_TAG */ 40 private final static String LOGTAG = "httpsv"; 41 42 /** maps the recently requested URLs to the full request snapshot */ 43 private final Map<String, Request> pathToRequest 44 = new ConcurrentHashMap<String, Request>(); 45 46 /* timeout on client connections */ 47 int timeout = 0; 48 49 /* Default socket timeout value */ 50 final static int DEFAULT_TIMEOUT = 5000; 51 52 /* Version string (configurable) */ 53 protected String HTTP_VERSION_STRING = "HTTP/1.1"; 54 55 /* Indicator for whether this server is configured as a HTTP/1.1 56 * or HTTP/1.0 server 57 */ 58 private boolean http11 = true; 59 60 /* The thread handling new requests from clients */ 61 private AcceptThread acceptT; 62 63 /* timeout on client connections */ 64 int mTimeout; 65 66 /* Server port */ 67 int mPort; 68 69 /* Switch on/off logging */ 70 boolean mLog = false; 71 72 /* If set, this will keep connections alive after a request has been 73 * processed. 74 */ 75 boolean keepAlive = true; 76 77 /* If set, this will cause response data to be sent in 'chunked' format */ 78 boolean chunked = false; 79 int maxChunkSize = 1024; 80 81 /* If set, this will indicate a new redirection host */ 82 String redirectHost = null; 83 84 /* If set, this indicates the reason for redirection */ 85 int redirectCode = -1; 86 87 /* Set the number of connections the server will accept before shutdown */ 88 int acceptLimit = 100; 89 90 /* Count of number of accepted connections */ 91 int acceptedConnections = 0; 92 93 public Support_TestWebServer() { 94 } 95 96 /** 97 * @param servePath the path to the dynamic web test data 98 * @param contentType the type of the dynamic web test data 99 */ 100 public int initServer(String servePath, String contentType) throws Exception { 101 Support_TestWebData.initDynamicTestWebData(servePath, contentType); 102 return initServer(); 103 } 104 105 public int initServer() throws Exception { 106 mTimeout = DEFAULT_TIMEOUT; 107 mLog = false; 108 keepAlive = true; 109 if (acceptT == null) { 110 acceptT = new AcceptThread(); 111 mPort = acceptT.init(); 112 acceptT.start(); 113 } 114 return mPort; 115 } 116 117 /** 118 * Print to the log file (if logging enabled) 119 * @param s String to send to the log 120 */ 121 protected void log(String s) { 122 if (mLog) { 123 Logger.global.fine(s); 124 } 125 } 126 127 /** 128 * Set the server to be an HTTP/1.0 or HTTP/1.1 server. 129 * This should be called prior to any requests being sent 130 * to the server. 131 * @param set True for the server to be HTTP/1.1, false for HTTP/1.0 132 */ 133 public void setHttpVersion11(boolean set) { 134 http11 = set; 135 if (set) { 136 HTTP_VERSION_STRING = "HTTP/1.1"; 137 } else { 138 HTTP_VERSION_STRING = "HTTP/1.0"; 139 } 140 } 141 142 /** 143 * Call this to determine whether server connection should remain open 144 * @param value Set true to keep connections open after a request 145 * completes 146 */ 147 public void setKeepAlive(boolean value) { 148 keepAlive = value; 149 } 150 151 /** 152 * Call this to indicate whether chunked data should be used 153 * @param value Set true to make server respond with chunk encoded 154 * content data. 155 */ 156 public void setChunked(boolean value) { 157 chunked = value; 158 } 159 160 /** 161 * Sets the maximum byte count of any chunk if the server is using 162 * the "chunked" transfer encoding. 163 */ 164 public void setMaxChunkSize(int maxChunkSize) { 165 this.maxChunkSize = maxChunkSize; 166 } 167 168 /** 169 * Call this to specify the maximum number of sockets to accept 170 * @param limit The number of sockets to accept 171 */ 172 public void setAcceptLimit(int limit) { 173 acceptLimit = limit; 174 } 175 176 /** 177 * Call this to indicate redirection port requirement. 178 * When this value is set, the server will respond to a request with 179 * a redirect code with the Location response header set to the value 180 * specified. 181 * @param redirect The location to be redirected to 182 * @param code The code to send when redirecting 183 */ 184 public void setRedirect(String redirect, int code) { 185 redirectHost = redirect; 186 redirectCode = code; 187 log("Server will redirect output to "+redirect+" code "+code); 188 } 189 190 /** 191 * Returns a map from recently-requested paths (like "/index.html") to a 192 * snapshot of the request data. 193 */ 194 public Map<String, Request> pathToRequest() { 195 return pathToRequest; 196 } 197 198 public int getNumAcceptedConnections() { 199 return acceptedConnections; 200 } 201 202 /** 203 * Cause the thread accepting connections on the server socket to close 204 */ 205 public void close() { 206 /* Stop the Accept thread */ 207 if (acceptT != null) { 208 log("Closing AcceptThread"+acceptT); 209 acceptT.close(); 210 acceptT = null; 211 } 212 } 213 /** 214 * The AcceptThread is responsible for initiating worker threads 215 * to handle incoming requests from clients. 216 */ 217 class AcceptThread extends Thread { 218 219 ServerSocket ss = null; 220 boolean running = false; 221 222 /** 223 * @param port the port to use, or 0 to let the OS choose. 224 * Hard-coding ports is evil, so always pass 0! 225 */ 226 public int init() throws IOException { 227 ss = new ServerSocket(0); 228 ss.setSoTimeout(5000); 229 ss.setReuseAddress(true); 230 return ss.getLocalPort(); 231 } 232 233 /** 234 * Main thread responding to new connections 235 */ 236 public synchronized void run() { 237 running = true; 238 while (running) { 239 try { 240 Socket s = ss.accept(); 241 acceptedConnections++; 242 if (acceptedConnections >= acceptLimit) { 243 running = false; 244 } 245 new Thread(new Worker(s), "additional worker").start(); 246 } catch (SocketException e) { 247 log(e.getMessage()); 248 } catch (IOException e) { 249 log(e.getMessage()); 250 } 251 } 252 log("AcceptThread terminated" + this); 253 } 254 255 // Close this socket 256 public void close() { 257 try { 258 running = false; 259 /* Stop server socket from processing further. Currently 260 this does not cause the SocketException from ss.accept 261 therefore the acceptLimit functionality has been added 262 to circumvent this limitation */ 263 ss.close(); 264 } catch (IOException e) { 265 /* We are shutting down the server, so we expect 266 * things to die. Don't propagate. 267 */ 268 log("IOException caught by server socket close"); 269 } 270 } 271 } 272 273 // Size of buffer for reading from the connection 274 final static int BUF_SIZE = 2048; 275 276 /* End of line byte sequence */ 277 static final byte[] EOL = {(byte)'\r', (byte)'\n' }; 278 279 /** 280 * An immutable snapshot of an HTTP request. 281 */ 282 public static class Request { 283 private final String path; 284 private final Map<String, String> headers; 285 // TODO: include posted content? 286 287 public Request(String path, Map<String, String> headers) { 288 this.path = path; 289 this.headers = new LinkedHashMap<String, String>(headers); 290 } 291 292 public String getPath() { 293 return path; 294 } 295 296 public Map<String, String> getHeaders() { 297 return headers; 298 } 299 } 300 301 /** 302 * The worker thread handles all interactions with a current open 303 * connection. If pipelining is turned on, this will allow this 304 * thread to continuously operate on numerous requests before the 305 * connection is closed. 306 */ 307 class Worker implements Support_HttpConstants, Runnable { 308 309 /* buffer to use to hold request data */ 310 byte[] buf; 311 312 /* Socket to client we're handling */ 313 private Socket s; 314 315 /* Reference to current request method ID */ 316 private int requestMethod; 317 318 /* Reference to current requests test file/data */ 319 private String testID; 320 321 /* The requested path, such as "/test1" */ 322 private String path; 323 324 /* Reference to test number from testID */ 325 private int testNum; 326 327 /* Reference to whether new request has been initiated yet */ 328 private boolean readStarted; 329 330 /* Indicates whether current request has any data content */ 331 private boolean hasContent = false; 332 333 /* Request headers are stored here */ 334 private Map<String, String> headers = new LinkedHashMap<String, String>(); 335 336 /* Create a new worker thread */ 337 Worker(Socket s) { 338 this.buf = new byte[BUF_SIZE]; 339 this.s = s; 340 } 341 342 public synchronized void run() { 343 try { 344 handleClient(); 345 } catch (Exception e) { 346 log("Exception during handleClient in the TestWebServer: " + e.getMessage()); 347 } 348 log(this+" terminated"); 349 } 350 351 /** 352 * Zero out the buffer from last time 353 */ 354 private void clearBuffer() { 355 for (int i = 0; i < BUF_SIZE; i++) { 356 buf[i] = 0; 357 } 358 } 359 360 /** 361 * Utility method to read a line of data from the input stream 362 * @param is Inputstream to read 363 * @return number of bytes read 364 */ 365 private int readOneLine(InputStream is) { 366 367 int read = 0; 368 369 clearBuffer(); 370 try { 371 log("Reading one line: started ="+readStarted+" avail="+is.available()); 372 StringBuilder log = new StringBuilder(); 373 while ((!readStarted) || (is.available() > 0)) { 374 int data = is.read(); 375 // We shouldn't get EOF but we need tdo check 376 if (data == -1) { 377 log("EOF returned"); 378 return -1; 379 } 380 381 buf[read] = (byte)data; 382 383 log.append((char)data); 384 385 readStarted = true; 386 if (buf[read++]==(byte)'\n') { 387 log(log.toString()); 388 return read; 389 } 390 } 391 } catch (IOException e) { 392 log("IOException from readOneLine"); 393 } 394 return read; 395 } 396 397 /** 398 * Read a chunk of data 399 * @param is Stream from which to read data 400 * @param length Amount of data to read 401 * @return number of bytes read 402 */ 403 private int readData(InputStream is, int length) { 404 int read = 0; 405 int count; 406 // At the moment we're only expecting small data amounts 407 byte[] buf = new byte[length]; 408 409 try { 410 while (is.available() > 0) { 411 count = is.read(buf, read, length-read); 412 read += count; 413 } 414 } catch (IOException e) { 415 log("IOException from readData"); 416 } 417 return read; 418 } 419 420 /** 421 * Read the status line from the input stream extracting method 422 * information. 423 * @param is Inputstream to read 424 * @return number of bytes read 425 */ 426 private int parseStatusLine(InputStream is) { 427 int index; 428 int nread = 0; 429 430 log("Parse status line"); 431 // Check for status line first 432 nread = readOneLine(is); 433 // Bomb out if stream closes prematurely 434 if (nread == -1) { 435 requestMethod = UNKNOWN_METHOD; 436 return -1; 437 } 438 439 if (buf[0] == (byte)'G' && 440 buf[1] == (byte)'E' && 441 buf[2] == (byte)'T' && 442 buf[3] == (byte)' ') { 443 requestMethod = GET_METHOD; 444 log("GET request"); 445 index = 4; 446 } else if (buf[0] == (byte)'H' && 447 buf[1] == (byte)'E' && 448 buf[2] == (byte)'A' && 449 buf[3] == (byte)'D' && 450 buf[4] == (byte)' ') { 451 requestMethod = HEAD_METHOD; 452 log("HEAD request"); 453 index = 5; 454 } else if (buf[0] == (byte)'P' && 455 buf[1] == (byte)'O' && 456 buf[2] == (byte)'S' && 457 buf[3] == (byte)'T' && 458 buf[4] == (byte)' ') { 459 requestMethod = POST_METHOD; 460 log("POST request"); 461 index = 5; 462 } else { 463 // Unhandled request 464 requestMethod = UNKNOWN_METHOD; 465 return -1; 466 } 467 468 // A valid method we understand 469 if (requestMethod > UNKNOWN_METHOD) { 470 // Read file name 471 int i = index; 472 while (buf[i] != (byte)' ') { 473 // There should be HTTP/1.x at the end 474 if ((buf[i] == (byte)'\n') || (buf[i] == (byte)'\r')) { 475 requestMethod = UNKNOWN_METHOD; 476 return -1; 477 } 478 i++; 479 } 480 481 path = new String(buf, 0, index, i-index); 482 testID = path.substring(1); 483 484 return nread; 485 } 486 return -1; 487 } 488 489 /** 490 * Read a header from the input stream 491 * @param is Inputstream to read 492 * @return number of bytes read 493 */ 494 private int parseHeader(InputStream is) { 495 int index = 0; 496 int nread = 0; 497 log("Parse a header"); 498 // Check for status line first 499 nread = readOneLine(is); 500 // Bomb out if stream closes prematurely 501 if (nread == -1) { 502 requestMethod = UNKNOWN_METHOD; 503 return -1; 504 } 505 // Read header entry 'Header: data' 506 int i = index; 507 while (buf[i] != (byte)':') { 508 // There should be an entry after the header 509 510 if ((buf[i] == (byte)'\n') || (buf[i] == (byte)'\r')) { 511 return UNKNOWN_METHOD; 512 } 513 i++; 514 } 515 516 String headerName = new String(buf, 0, i); 517 i++; // Over ':' 518 while (buf[i] == ' ') { 519 i++; 520 } 521 String headerValue = new String(buf, i, nread - i - 2); // drop \r\n 522 523 headers.put(headerName, headerValue); 524 return nread; 525 } 526 527 /** 528 * Read all headers from the input stream 529 * @param is Inputstream to read 530 * @return number of bytes read 531 */ 532 private int readHeaders(InputStream is) { 533 int nread = 0; 534 log("Read headers"); 535 // Headers should be terminated by empty CRLF line 536 while (true) { 537 int headerLen = 0; 538 headerLen = parseHeader(is); 539 if (headerLen == -1) 540 return -1; 541 nread += headerLen; 542 if (headerLen <= 2) { 543 return nread; 544 } 545 } 546 } 547 548 /** 549 * Read content data from the input stream 550 * @param is Inputstream to read 551 * @return number of bytes read 552 */ 553 private int readContent(InputStream is) { 554 int nread = 0; 555 log("Read content"); 556 String lengthString = headers.get(requestHeaders[REQ_CONTENT_LENGTH]); 557 int length = new Integer(lengthString).intValue(); 558 559 // Read content 560 length = readData(is, length); 561 return length; 562 } 563 564 /** 565 * The main loop, reading requests. 566 */ 567 void handleClient() throws IOException { 568 InputStream is = new BufferedInputStream(s.getInputStream()); 569 PrintStream ps = new PrintStream(s.getOutputStream()); 570 int nread = 0; 571 572 /* we will only block in read for this many milliseconds 573 * before we fail with java.io.InterruptedIOException, 574 * at which point we will abandon the connection. 575 */ 576 s.setSoTimeout(mTimeout); 577 s.setTcpNoDelay(true); 578 579 do { 580 nread = parseStatusLine(is); 581 if (requestMethod != UNKNOWN_METHOD) { 582 583 // If status line found, read any headers 584 nread = readHeaders(is); 585 586 pathToRequest().put(path, new Request(path, headers)); 587 588 // Then read content (if any) 589 // TODO handle chunked encoding from the client 590 if (headers.get(requestHeaders[REQ_CONTENT_LENGTH]) != null) { 591 nread = readContent(is); 592 } 593 } else { 594 if (nread > 0) { 595 /* we don't support this method */ 596 ps.print(HTTP_VERSION_STRING + " " + HTTP_BAD_METHOD + 597 " unsupported method type: "); 598 ps.write(buf, 0, 5); 599 ps.write(EOL); 600 ps.flush(); 601 } else { 602 } 603 if (!keepAlive || nread <= 0) { 604 headers.clear(); 605 readStarted = false; 606 607 log("SOCKET CLOSED"); 608 s.close(); 609 return; 610 } 611 } 612 613 // Reset test number prior to outputing data 614 testNum = -1; 615 616 // Write out the data 617 printStatus(ps); 618 printHeaders(ps); 619 620 // Write line between headers and body 621 psWriteEOL(ps); 622 623 // Write the body 624 if (redirectCode == -1) { 625 switch (requestMethod) { 626 case GET_METHOD: 627 if ((testNum < -1) || (testNum > Support_TestWebData.tests.length - 1)) { 628 send404(ps); 629 } else { 630 sendFile(ps); 631 } 632 break; 633 case HEAD_METHOD: 634 // Nothing to do 635 break; 636 case POST_METHOD: 637 // Post method write body data 638 if ((testNum > 0) || (testNum < Support_TestWebData.tests.length - 1)) { 639 sendFile(ps); 640 } 641 642 break; 643 default: 644 break; 645 } 646 } else { // Redirecting 647 switch (redirectCode) { 648 case 301: 649 // Seems 301 needs a body by neon (although spec 650 // says SHOULD). 651 psPrint(ps, Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_301]); 652 break; 653 case 302: 654 // 655 psPrint(ps, Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_302]); 656 break; 657 case 303: 658 psPrint(ps, Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_303]); 659 break; 660 case 307: 661 psPrint(ps, Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_307]); 662 break; 663 default: 664 break; 665 } 666 } 667 668 ps.flush(); 669 670 // Reset for next request 671 readStarted = false; 672 headers.clear(); 673 674 } while (keepAlive); 675 676 log("SOCKET CLOSED"); 677 s.close(); 678 } 679 680 // Print string to log and output stream 681 void psPrint(PrintStream ps, String s) throws IOException { 682 log(s); 683 ps.print(s); 684 } 685 686 // Print bytes to log and output stream 687 void psWrite(PrintStream ps, byte[] bytes, int offset, int count) throws IOException { 688 log(new String(bytes)); 689 ps.write(bytes, offset, count); 690 } 691 692 // Print CRLF to log and output stream 693 void psWriteEOL(PrintStream ps) throws IOException { 694 log("CRLF"); 695 ps.write(EOL); 696 } 697 698 699 // Print status to log and output stream 700 void printStatus(PrintStream ps) throws IOException { 701 // Handle redirects first. 702 if (redirectCode != -1) { 703 log("REDIRECTING TO "+redirectHost+" status "+redirectCode); 704 psPrint(ps, HTTP_VERSION_STRING + " " + redirectCode +" Moved permanently"); 705 psWriteEOL(ps); 706 psPrint(ps, "Location: " + redirectHost); 707 psWriteEOL(ps); 708 return; 709 } 710 711 712 if (testID.startsWith("test")) { 713 testNum = Integer.valueOf(testID.substring(4))-1; 714 } 715 716 if ((testNum < -1) || (testNum > Support_TestWebData.tests.length - 1)) { 717 psPrint(ps, HTTP_VERSION_STRING + " " + HTTP_NOT_FOUND + " not found"); 718 psWriteEOL(ps); 719 } else { 720 psPrint(ps, HTTP_VERSION_STRING + " " + HTTP_OK+" OK"); 721 psWriteEOL(ps); 722 } 723 724 log("Status sent"); 725 } 726 /** 727 * Create the server response and output to the stream 728 * @param ps The PrintStream to output response headers and data to 729 */ 730 void printHeaders(PrintStream ps) throws IOException { 731 if ((testNum < -1) || (testNum > Support_TestWebData.tests.length - 1)) { 732 // 404 status already sent 733 return; 734 } 735 SimpleDateFormat df = new SimpleDateFormat("EE, dd MMM yyyy HH:mm:ss"); 736 737 psPrint(ps,"Server: TestWebServer"+mPort); 738 psWriteEOL(ps); 739 psPrint(ps, "Date: " + df.format(new Date())); 740 psWriteEOL(ps); 741 psPrint(ps, "Connection: " + ((keepAlive) ? "Keep-Alive" : "Close")); 742 psWriteEOL(ps); 743 744 // Yuk, if we're not redirecting, we add the file details 745 if (redirectCode == -1) { 746 747 if (testNum == -1) { 748 if (!Support_TestWebData.test0DataAvailable) { 749 log("testdata was not initilaized"); 750 return; 751 } 752 if (chunked) { 753 psPrint(ps, "Transfer-Encoding: chunked"); 754 } else { 755 psPrint(ps, "Content-length: " 756 + Support_TestWebData.test0Data.length); 757 } 758 psWriteEOL(ps); 759 760 psPrint(ps, "Last Modified: " + (new Date( 761 Support_TestWebData.test0Params.testLastModified))); 762 psWriteEOL(ps); 763 764 psPrint(ps, "Content-type: " 765 + Support_TestWebData.test0Params.testType); 766 psWriteEOL(ps); 767 768 if (Support_TestWebData.testParams[testNum].testExp > 0) { 769 long exp; 770 exp = Support_TestWebData.testParams[testNum].testExp; 771 psPrint(ps, "expires: " 772 + df.format(exp) + " GMT"); 773 psWriteEOL(ps); 774 } 775 } else if (!Support_TestWebData.testParams[testNum].testDir) { 776 if (chunked) { 777 psPrint(ps, "Transfer-Encoding: chunked"); 778 } else { 779 psPrint(ps, "Content-length: "+Support_TestWebData.testParams[testNum].testLength); 780 } 781 psWriteEOL(ps); 782 783 psPrint(ps,"Last Modified: " + (new 784 Date(Support_TestWebData.testParams[testNum].testLastModified))); 785 psWriteEOL(ps); 786 787 psPrint(ps, "Content-type: " + Support_TestWebData.testParams[testNum].testType); 788 psWriteEOL(ps); 789 790 if (Support_TestWebData.testParams[testNum].testExp > 0) { 791 long exp; 792 exp = Support_TestWebData.testParams[testNum].testExp; 793 psPrint(ps, "expires: " 794 + df.format(exp) + " GMT"); 795 psWriteEOL(ps); 796 } 797 } else { 798 psPrint(ps, "Content-type: text/html"); 799 psWriteEOL(ps); 800 } 801 } else { 802 // Content-length of 301, 302, 303, 307 are the same. 803 psPrint(ps, "Content-length: "+(Support_TestWebData.testServerResponse[Support_TestWebData.REDIRECT_301]).length()); 804 psWriteEOL(ps); 805 psWriteEOL(ps); 806 } 807 log("Headers sent"); 808 809 } 810 811 /** 812 * Sends the 404 not found message 813 * @param ps The PrintStream to write to 814 */ 815 void send404(PrintStream ps) throws IOException { 816 ps.println("Not Found\n\n"+ 817 "The requested resource was not found.\n"); 818 } 819 820 /** 821 * Sends the data associated with the headers 822 * @param ps The PrintStream to write to 823 */ 824 void sendFile(PrintStream ps) throws IOException { 825 if (testNum == -1) { 826 if (!Support_TestWebData.test0DataAvailable) { 827 log("test data was not initialized"); 828 return; 829 } 830 sendFile(ps, Support_TestWebData.test0Data); 831 } else { 832 sendFile(ps, Support_TestWebData.tests[testNum]); 833 } 834 } 835 836 void sendFile(PrintStream ps, byte[] bytes) throws IOException { 837 if (chunked) { 838 int offset = 0; 839 while (offset < bytes.length) { 840 int chunkSize = Math.min(bytes.length - offset, maxChunkSize); 841 psPrint(ps, Integer.toHexString(chunkSize)); 842 psWriteEOL(ps); 843 psWrite(ps, bytes, offset, chunkSize); 844 psWriteEOL(ps); 845 offset += chunkSize; 846 } 847 psPrint(ps, "0"); 848 psWriteEOL(ps); 849 psWriteEOL(ps); 850 } else { 851 psWrite(ps, bytes, 0, bytes.length); 852 } 853 } 854 } 855} 856