ClientOperation.java revision 05ff98bbefda39b9ff26f8bca132cfd0248745c6
1/* 2 * Copyright (c) 2008-2009, Motorola, Inc. 3 * 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * - Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * - Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * - Neither the name of the Motorola, Inc. nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 * POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33package javax.obex; 34 35import java.io.IOException; 36import java.io.InputStream; 37import java.io.OutputStream; 38import java.io.DataInputStream; 39import java.io.DataOutputStream; 40import java.io.ByteArrayOutputStream; 41 42/** 43 * This class implements the <code>Operation</code> interface. It will read and 44 * write data via puts and gets. 45 * @hide 46 */ 47public final class ClientOperation implements Operation, BaseStream { 48 49 private ClientSession mParent; 50 51 private boolean mInputOpen; 52 53 private PrivateInputStream mPrivateInput; 54 55 private boolean mPrivateInputOpen; 56 57 private PrivateOutputStream mPrivateOutput; 58 59 private boolean mPrivateOutputOpen; 60 61 private String mExceptionMessage; 62 63 private int mMaxPacketSize; 64 65 private boolean mOperationDone; 66 67 private boolean mGetOperation; 68 69 private HeaderSet mRequestHeader; 70 71 private HeaderSet mReplyHeader; 72 73 private boolean mEndOfBodySent; 74 75 /** 76 * Creates new OperationImpl to read and write data to a server 77 * @param maxSize the maximum packet size 78 * @param p the parent to this object 79 * @param type <code>true</code> if this is a get request; 80 * <code>false</code. if this is a put request 81 * @param header the header to set in the initial request 82 * @throws IOException if the an IO error occurred 83 */ 84 public ClientOperation(int maxSize, ClientSession p, HeaderSet header, boolean type) 85 throws IOException { 86 87 mParent = p; 88 mEndOfBodySent = false; 89 mInputOpen = true; 90 mOperationDone = false; 91 mMaxPacketSize = maxSize; 92 mGetOperation = type; 93 94 mPrivateInputOpen = false; 95 mPrivateOutputOpen = false; 96 mPrivateInput = null; 97 mPrivateOutput = null; 98 99 mReplyHeader = new HeaderSet(); 100 101 mRequestHeader = new HeaderSet(); 102 103 int[] headerList = header.getHeaderList(); 104 105 if (headerList != null) { 106 107 for (int i = 0; i < headerList.length; i++) { 108 mRequestHeader.setHeader(headerList[i], header.getHeader(headerList[i])); 109 } 110 } 111 112 if ((header).mAuthChall != null) { 113 mRequestHeader.mAuthChall = new byte[(header).mAuthChall.length]; 114 System.arraycopy((header).mAuthChall, 0, mRequestHeader.mAuthChall, 0, 115 (header).mAuthChall.length); 116 } 117 118 if ((header).mAuthResp != null) { 119 mRequestHeader.mAuthResp = new byte[(header).mAuthResp.length]; 120 System.arraycopy((header).mAuthResp, 0, mRequestHeader.mAuthResp, 0, 121 (header).mAuthResp.length); 122 123 } 124 } 125 126 /** 127 * Sends an ABORT message to the server. By calling this method, the 128 * corresponding input and output streams will be closed along with this 129 * object. 130 * @throws IOException if the transaction has already ended or if an OBEX 131 * server called this method 132 */ 133 public synchronized void abort() throws IOException { 134 ensureOpen(); 135 //no compatible with sun-ri 136 if ((mOperationDone) && (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE)) { 137 throw new IOException("Operation has already ended"); 138 } 139 140 mExceptionMessage = "Operation aborted"; 141 if ((!mOperationDone) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { 142 mOperationDone = true; 143 /* 144 * Since we are not sending any headers or returning any headers then 145 * we just need to write and read the same bytes 146 */ 147 mParent.sendRequest(ObexHelper.OBEX_OPCODE_ABORT, null, mReplyHeader, null); 148 149 if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_OK) { 150 throw new IOException("Invalid response code from server"); 151 } 152 153 mExceptionMessage = null; 154 } 155 156 close(); 157 } 158 159 /** 160 * Retrieves the response code retrieved from the server. Response codes are 161 * defined in the <code>ResponseCodes</code> interface. 162 * @return the response code retrieved from the server 163 * @throws IOException if an error occurred in the transport layer during 164 * the transaction; if this method is called on a 165 * <code>HeaderSet</code> object created by calling 166 * <code>createHeaderSet</code> in a <code>ClientSession</code> 167 * object 168 */ 169 public synchronized int getResponseCode() throws IOException { 170 //avoid dup validateConnection 171 if ((mReplyHeader.responseCode == -1) 172 || (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { 173 validateConnection(); 174 } 175 176 return mReplyHeader.responseCode; 177 } 178 179 /** 180 * This method will always return <code>null</code> 181 * @return <code>null</code> 182 */ 183 public String getEncoding() { 184 return null; 185 } 186 187 /** 188 * Returns the type of content that the resource connected to is providing. 189 * E.g. if the connection is via HTTP, then the value of the content-type 190 * header field is returned. 191 * @return the content type of the resource that the URL references, or 192 * <code>null</code> if not known 193 */ 194 public String getType() { 195 try { 196 return (String)mReplyHeader.getHeader(HeaderSet.TYPE); 197 } catch (IOException e) { 198 return null; 199 } 200 } 201 202 /** 203 * Returns the length of the content which is being provided. E.g. if the 204 * connection is via HTTP, then the value of the content-length header field 205 * is returned. 206 * @return the content length of the resource that this connection's URL 207 * references, or -1 if the content length is not known 208 */ 209 public long getLength() { 210 try { 211 Long temp = (Long)mReplyHeader.getHeader(HeaderSet.LENGTH); 212 213 if (temp == null) { 214 return -1; 215 } else { 216 return temp.longValue(); 217 } 218 } catch (IOException e) { 219 return -1; 220 } 221 } 222 223 /** 224 * Open and return an input stream for a connection. 225 * @return an input stream 226 * @throws IOException if an I/O error occurs 227 */ 228 public InputStream openInputStream() throws IOException { 229 230 ensureOpen(); 231 232 if (mPrivateInputOpen) 233 throw new IOException("no more input streams available"); 234 if (mGetOperation) { 235 // send the GET request here 236 validateConnection(); 237 } else { 238 if (mPrivateInput == null) { 239 mPrivateInput = new PrivateInputStream(this); 240 } 241 } 242 243 mPrivateInputOpen = true; 244 245 return mPrivateInput; 246 } 247 248 /** 249 * Open and return a data input stream for a connection. 250 * @return an input stream 251 * @throws IOException if an I/O error occurs 252 */ 253 public DataInputStream openDataInputStream() throws IOException { 254 return new DataInputStream(openInputStream()); 255 } 256 257 /** 258 * Open and return an output stream for a connection. 259 * @return an output stream 260 * @throws IOException if an I/O error occurs 261 */ 262 public OutputStream openOutputStream() throws IOException { 263 264 ensureOpen(); 265 ensureNotDone(); 266 267 if (mPrivateOutputOpen) 268 throw new IOException("no more output streams available"); 269 270 if (mPrivateOutput == null) { 271 // there are 3 bytes operation headers and 3 bytes body headers // 272 mPrivateOutput = new PrivateOutputStream(this, mMaxPacketSize - 6); 273 } 274 275 mPrivateOutputOpen = true; 276 277 return mPrivateOutput; 278 } 279 280 public int getMaxPacketSize() { 281 return mMaxPacketSize - 6; 282 } 283 284 /** 285 * Open and return a data output stream for a connection. 286 * @return an output stream 287 * @throws IOException if an I/O error occurs 288 */ 289 public DataOutputStream openDataOutputStream() throws IOException { 290 return new DataOutputStream(openOutputStream()); 291 } 292 293 /** 294 * Closes the connection and ends the transaction 295 * @throws IOException if the operation has already ended or is closed 296 */ 297 public void close() throws IOException { 298 mInputOpen = false; 299 mPrivateInputOpen = false; 300 mPrivateOutputOpen = false; 301 mParent.setRequestInactive(); 302 } 303 304 /** 305 * Returns the headers that have been received during the operation. 306 * Modifying the object returned has no effect on the headers that are sent 307 * or retrieved. 308 * @return the headers received during this <code>Operation</code> 309 * @throws IOException if this <code>Operation</code> has been closed 310 */ 311 public HeaderSet getReceivedHeader() throws IOException { 312 ensureOpen(); 313 314 return mReplyHeader; 315 } 316 317 /** 318 * Specifies the headers that should be sent in the next OBEX message that 319 * is sent. 320 * @param headers the headers to send in the next message 321 * @throws IOException if this <code>Operation</code> has been closed or the 322 * transaction has ended and no further messages will be exchanged 323 * @throws IllegalArgumentException if <code>headers</code> was not created 324 * by a call to <code>ServerRequestHandler.createHeaderSet()</code> 325 * @throws NullPointerException if <code>headers</code> is <code>null</code> 326 */ 327 public void sendHeaders(HeaderSet headers) throws IOException { 328 ensureOpen(); 329 if (mOperationDone) { 330 throw new IOException("Operation has already exchanged all data"); 331 } 332 333 if (headers == null) { 334 throw new IOException("Headers may not be null"); 335 } 336 337 int[] headerList = headers.getHeaderList(); 338 if (headerList != null) { 339 for (int i = 0; i < headerList.length; i++) { 340 mRequestHeader.setHeader(headerList[i], headers.getHeader(headerList[i])); 341 } 342 } 343 } 344 345 /** 346 * Verifies that additional information may be sent. In other words, the 347 * operation is not done. 348 * @throws IOException if the operation is completed 349 */ 350 public void ensureNotDone() throws IOException { 351 if (mOperationDone) { 352 throw new IOException("Operation has completed"); 353 } 354 } 355 356 /** 357 * Verifies that the connection is open and no exceptions should be thrown. 358 * @throws IOException if an exception needs to be thrown 359 */ 360 public void ensureOpen() throws IOException { 361 mParent.ensureOpen(); 362 363 if (mExceptionMessage != null) { 364 throw new IOException(mExceptionMessage); 365 } 366 if (!mInputOpen) { 367 throw new IOException("Operation has already ended"); 368 } 369 } 370 371 /** 372 * Verifies that the connection is open and the proper data has been read. 373 * @throws IOException if an IO error occurs 374 */ 375 private void validateConnection() throws IOException { 376 ensureOpen(); 377 378 // to sure only one privateInput object exist. 379 if (mPrivateInput == null) { 380 startProcessing(); 381 } 382 } 383 384 /** 385 * Sends a request to the client of the specified type 386 * @param opCode the request code to send to the client 387 * @return <code>true</code> if there is more data to send; 388 * <code>false</code> if there is no more data to send 389 * @throws IOException if an IO error occurs 390 */ 391 private boolean sendRequest(int opCode) throws IOException { 392 boolean returnValue = false; 393 ByteArrayOutputStream out = new ByteArrayOutputStream(); 394 int bodyLength = -1; 395 byte[] headerArray = ObexHelper.createHeader(mRequestHeader, true); 396 if (mPrivateOutput != null) { 397 bodyLength = mPrivateOutput.size(); 398 } 399 400 /* 401 * Determine if there is space to add a body request. At present 402 * this method checks to see if there is room for at least a 17 403 * byte body header. This number needs to be at least 6 so that 404 * there is room for the header ID and length and the reply ID and 405 * length, but it is a waste of resources if we can't send much of 406 * the body. 407 */ 408 if ((ObexHelper.BASE_PACKET_LENGTH + headerArray.length) > mMaxPacketSize) { 409 int end = 0; 410 int start = 0; 411 // split & send the headerArray in multiple packets. 412 413 while (end != headerArray.length) { 414 //split the headerArray 415 end = ObexHelper.findHeaderEnd(headerArray, start, mMaxPacketSize 416 - ObexHelper.BASE_PACKET_LENGTH); 417 // can not split 418 if (end == -1) { 419 mOperationDone = true; 420 abort(); 421 mExceptionMessage = "Header larger then can be sent in a packet"; 422 mInputOpen = false; 423 424 if (mPrivateInput != null) { 425 mPrivateInput.close(); 426 } 427 428 if (mPrivateOutput != null) { 429 mPrivateOutput.close(); 430 } 431 throw new IOException("OBEX Packet exceeds max packet size"); 432 } 433 434 byte[] sendHeader = new byte[end - start]; 435 System.arraycopy(headerArray, start, sendHeader, 0, sendHeader.length); 436 if (!mParent.sendRequest(opCode, sendHeader, mReplyHeader, mPrivateInput)) { 437 return false; 438 } 439 440 if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { 441 return false; 442 } 443 444 start = end; 445 } 446 447 if (bodyLength > 0) { 448 return true; 449 } else { 450 return false; 451 } 452 } else { 453 out.write(headerArray); 454 } 455 456 if (bodyLength > 0) { 457 /* 458 * Determine if we can send the whole body or just part of 459 * the body. Remember that there is the 3 bytes for the 460 * response message and 3 bytes for the header ID and length 461 */ 462 if (bodyLength > (mMaxPacketSize - headerArray.length - 6)) { 463 returnValue = true; 464 465 bodyLength = mMaxPacketSize - headerArray.length - 6; 466 } 467 468 byte[] body = mPrivateOutput.readBytes(bodyLength); 469 470 /* 471 * Since this is a put request if the final bit is set or 472 * the output stream is closed we need to send the 0x49 473 * (End of Body) otherwise, we need to send 0x48 (Body) 474 */ 475 if ((mPrivateOutput.isClosed()) && (!returnValue) && (!mEndOfBodySent) 476 && ((opCode & 0x80) != 0)) { 477 out.write(0x49); 478 mEndOfBodySent = true; 479 } else { 480 out.write(0x48); 481 } 482 483 bodyLength += 3; 484 out.write((byte)(bodyLength >> 8)); 485 out.write((byte)bodyLength); 486 487 if (body != null) { 488 out.write(body); 489 } 490 } 491 492 if (mPrivateOutputOpen && bodyLength <= 0 && !mEndOfBodySent) { 493 // only 0x82 or 0x83 can send 0x49 494 if ((opCode & 0x80) == 0) { 495 out.write(0x48); 496 } else { 497 out.write(0x49); 498 mEndOfBodySent = true; 499 500 } 501 502 bodyLength = 3; 503 out.write((byte)(bodyLength >> 8)); 504 out.write((byte)bodyLength); 505 } 506 507 if (out.size() == 0) { 508 if (!mParent.sendRequest(opCode, null, mReplyHeader, mPrivateInput)) { 509 return false; 510 } 511 return returnValue; 512 } 513 if ((out.size() > 0) 514 && (!mParent.sendRequest(opCode, out.toByteArray(), mReplyHeader, mPrivateInput))) { 515 return false; 516 } 517 518 // send all of the output data in 0x48, 519 // send 0x49 with empty body 520 if ((mPrivateOutput != null) && (mPrivateOutput.size() > 0)) 521 returnValue = true; 522 523 return returnValue; 524 } 525 526 /** 527 * This method starts the processing thread results. It will send the 528 * initial request. If the response takes more then one packet, a thread 529 * will be started to handle additional requests 530 * @throws IOException if an IO error occurs 531 */ 532 private synchronized void startProcessing() throws IOException { 533 534 if (mPrivateInput == null) { 535 mPrivateInput = new PrivateInputStream(this); 536 } 537 boolean more = true; 538 539 if (mGetOperation) { 540 if (!mOperationDone) { 541 mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; 542 while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { 543 more = sendRequest(0x03); 544 } 545 546 if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { 547 mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput); 548 } 549 if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { 550 mOperationDone = true; 551 } 552 } 553 } else { 554 555 if (!mOperationDone) { 556 mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; 557 while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { 558 more = sendRequest(0x02); 559 560 } 561 } 562 563 if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { 564 mParent.sendRequest(0x82, null, mReplyHeader, mPrivateInput); 565 } 566 567 if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { 568 mOperationDone = true; 569 } 570 } 571 } 572 573 /** 574 * Continues the operation since there is no data to read. 575 * @param sendEmpty <code>true</code> if the operation should send an empty 576 * packet or not send anything if there is no data to send 577 * @param inStream <code>true</code> if the stream is input stream or is 578 * output stream 579 * @throws IOException if an IO error occurs 580 */ 581 public synchronized boolean continueOperation(boolean sendEmpty, boolean inStream) 582 throws IOException { 583 584 if (mGetOperation) { 585 if ((inStream) && (!mOperationDone)) { 586 // to deal with inputstream in get operation 587 mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput); 588 /* 589 * Determine if that was not the last packet in the operation 590 */ 591 if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { 592 mOperationDone = true; 593 } 594 595 return true; 596 597 } else if ((!inStream) && (!mOperationDone)) { 598 // to deal with outputstream in get operation 599 600 if (mPrivateInput == null) { 601 mPrivateInput = new PrivateInputStream(this); 602 } 603 sendRequest(0x03); 604 return true; 605 606 } else if (mOperationDone) { 607 return false; 608 } 609 610 } else { 611 if ((!inStream) && (!mOperationDone)) { 612 // to deal with outputstream in put operation 613 if (mReplyHeader.responseCode == -1) { 614 mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; 615 } 616 sendRequest(0x02); 617 return true; 618 } else if ((inStream) && (!mOperationDone)) { 619 // How to deal with inputstream in put operation ? 620 return false; 621 622 } else if (mOperationDone) { 623 return false; 624 } 625 626 } 627 return false; 628 } 629 630 /** 631 * Called when the output or input stream is closed. 632 * @param inStream <code>true</code> if the input stream is closed; 633 * <code>false</code> if the output stream is closed 634 * @throws IOException if an IO error occurs 635 */ 636 public void streamClosed(boolean inStream) throws IOException { 637 if (!mGetOperation) { 638 if ((!inStream) && (!mOperationDone)) { 639 // to deal with outputstream in put operation 640 641 boolean more = true; 642 643 if ((mPrivateOutput != null) && (mPrivateOutput.size() <= 0)) { 644 byte[] headerArray = ObexHelper.createHeader(mRequestHeader, false); 645 if (headerArray.length <= 0) 646 more = false; 647 } 648 // If have not sent any data so send all now 649 if (mReplyHeader.responseCode == -1) { 650 mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; 651 } 652 653 while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { 654 more = sendRequest(0x02); 655 } 656 657 /* 658 * According to the IrOBEX specification, after the final put, you 659 * only have a single reply to send. so we don't need the while 660 * loop. 661 */ 662 while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { 663 664 sendRequest(0x82); 665 } 666 mOperationDone = true; 667 } else if ((inStream) && (mOperationDone)) { 668 // how to deal with input stream in put stream ? 669 mOperationDone = true; 670 } 671 } else { 672 if ((inStream) && (!mOperationDone)) { 673 674 // to deal with inputstream in get operation 675 // Have not sent any data so send it all now 676 677 if (mReplyHeader.responseCode == -1) { 678 mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; 679 } 680 681 while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { 682 if (!sendRequest(0x83)) { 683 break; 684 } 685 } 686 while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { 687 mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput); 688 } 689 mOperationDone = true; 690 } else if ((!inStream) && (!mOperationDone)) { 691 // to deal with outputstream in get operation 692 // part of the data may have been sent in continueOperation. 693 694 boolean more = true; 695 696 if ((mPrivateOutput != null) && (mPrivateOutput.size() <= 0)) { 697 byte[] headerArray = ObexHelper.createHeader(mRequestHeader, false); 698 if (headerArray.length <= 0) 699 more = false; 700 } 701 702 if (mPrivateInput == null) { 703 mPrivateInput = new PrivateInputStream(this); 704 } 705 if ((mPrivateOutput != null) && (mPrivateOutput.size() <= 0)) 706 more = false; 707 708 mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; 709 while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { 710 more = sendRequest(0x03); 711 } 712 sendRequest(0x83); 713 // parent.sendRequest(0x83, null, replyHeaders, privateInput); 714 if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { 715 mOperationDone = true; 716 } 717 } 718 } 719 } 720} 721