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 33 package javax.obex; 34 35 import java.io.IOException; 36 import java.io.InputStream; 37 import java.io.OutputStream; 38 import java.io.DataInputStream; 39 import java.io.DataOutputStream; 40 import 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 */ 47 public 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, getMaxPacketSize()); 273 } 274 275 mPrivateOutputOpen = true; 276 277 return mPrivateOutput; 278 } 279 280 public int getMaxPacketSize() { 281 return mMaxPacketSize - 6 - getHeaderLength(); 282 } 283 284 public int getHeaderLength() { 285 // OPP may need it 286 byte[] headerArray = ObexHelper.createHeader(mRequestHeader, false); 287 return headerArray.length; 288 } 289 290 /** 291 * Open and return a data output stream for a connection. 292 * @return an output stream 293 * @throws IOException if an I/O error occurs 294 */ 295 public DataOutputStream openDataOutputStream() throws IOException { 296 return new DataOutputStream(openOutputStream()); 297 } 298 299 /** 300 * Closes the connection and ends the transaction 301 * @throws IOException if the operation has already ended or is closed 302 */ 303 public void close() throws IOException { 304 mInputOpen = false; 305 mPrivateInputOpen = false; 306 mPrivateOutputOpen = false; 307 mParent.setRequestInactive(); 308 } 309 310 /** 311 * Returns the headers that have been received during the operation. 312 * Modifying the object returned has no effect on the headers that are sent 313 * or retrieved. 314 * @return the headers received during this <code>Operation</code> 315 * @throws IOException if this <code>Operation</code> has been closed 316 */ 317 public HeaderSet getReceivedHeader() throws IOException { 318 ensureOpen(); 319 320 return mReplyHeader; 321 } 322 323 /** 324 * Specifies the headers that should be sent in the next OBEX message that 325 * is sent. 326 * @param headers the headers to send in the next message 327 * @throws IOException if this <code>Operation</code> has been closed or the 328 * transaction has ended and no further messages will be exchanged 329 * @throws IllegalArgumentException if <code>headers</code> was not created 330 * by a call to <code>ServerRequestHandler.createHeaderSet()</code> 331 * @throws NullPointerException if <code>headers</code> is <code>null</code> 332 */ 333 public void sendHeaders(HeaderSet headers) throws IOException { 334 ensureOpen(); 335 if (mOperationDone) { 336 throw new IOException("Operation has already exchanged all data"); 337 } 338 339 if (headers == null) { 340 throw new IOException("Headers may not be null"); 341 } 342 343 int[] headerList = headers.getHeaderList(); 344 if (headerList != null) { 345 for (int i = 0; i < headerList.length; i++) { 346 mRequestHeader.setHeader(headerList[i], headers.getHeader(headerList[i])); 347 } 348 } 349 } 350 351 /** 352 * Verifies that additional information may be sent. In other words, the 353 * operation is not done. 354 * @throws IOException if the operation is completed 355 */ 356 public void ensureNotDone() throws IOException { 357 if (mOperationDone) { 358 throw new IOException("Operation has completed"); 359 } 360 } 361 362 /** 363 * Verifies that the connection is open and no exceptions should be thrown. 364 * @throws IOException if an exception needs to be thrown 365 */ 366 public void ensureOpen() throws IOException { 367 mParent.ensureOpen(); 368 369 if (mExceptionMessage != null) { 370 throw new IOException(mExceptionMessage); 371 } 372 if (!mInputOpen) { 373 throw new IOException("Operation has already ended"); 374 } 375 } 376 377 /** 378 * Verifies that the connection is open and the proper data has been read. 379 * @throws IOException if an IO error occurs 380 */ 381 private void validateConnection() throws IOException { 382 ensureOpen(); 383 384 // to sure only one privateInput object exist. 385 if (mPrivateInput == null) { 386 startProcessing(); 387 } 388 } 389 390 /** 391 * Sends a request to the client of the specified type 392 * @param opCode the request code to send to the client 393 * @return <code>true</code> if there is more data to send; 394 * <code>false</code> if there is no more data to send 395 * @throws IOException if an IO error occurs 396 */ 397 private boolean sendRequest(int opCode) throws IOException { 398 boolean returnValue = false; 399 ByteArrayOutputStream out = new ByteArrayOutputStream(); 400 int bodyLength = -1; 401 byte[] headerArray = ObexHelper.createHeader(mRequestHeader, true); 402 if (mPrivateOutput != null) { 403 bodyLength = mPrivateOutput.size(); 404 } 405 406 /* 407 * Determine if there is space to add a body request. At present 408 * this method checks to see if there is room for at least a 17 409 * byte body header. This number needs to be at least 6 so that 410 * there is room for the header ID and length and the reply ID and 411 * length, but it is a waste of resources if we can't send much of 412 * the body. 413 */ 414 if ((ObexHelper.BASE_PACKET_LENGTH + headerArray.length) > mMaxPacketSize) { 415 int end = 0; 416 int start = 0; 417 // split & send the headerArray in multiple packets. 418 419 while (end != headerArray.length) { 420 //split the headerArray 421 end = ObexHelper.findHeaderEnd(headerArray, start, mMaxPacketSize 422 - ObexHelper.BASE_PACKET_LENGTH); 423 // can not split 424 if (end == -1) { 425 mOperationDone = true; 426 abort(); 427 mExceptionMessage = "Header larger then can be sent in a packet"; 428 mInputOpen = false; 429 430 if (mPrivateInput != null) { 431 mPrivateInput.close(); 432 } 433 434 if (mPrivateOutput != null) { 435 mPrivateOutput.close(); 436 } 437 throw new IOException("OBEX Packet exceeds max packet size"); 438 } 439 440 byte[] sendHeader = new byte[end - start]; 441 System.arraycopy(headerArray, start, sendHeader, 0, sendHeader.length); 442 if (!mParent.sendRequest(opCode, sendHeader, mReplyHeader, mPrivateInput)) { 443 return false; 444 } 445 446 if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { 447 return false; 448 } 449 450 start = end; 451 } 452 453 if (bodyLength > 0) { 454 return true; 455 } else { 456 return false; 457 } 458 } else { 459 out.write(headerArray); 460 } 461 462 if (bodyLength > 0) { 463 /* 464 * Determine if we can send the whole body or just part of 465 * the body. Remember that there is the 3 bytes for the 466 * response message and 3 bytes for the header ID and length 467 */ 468 if (bodyLength > (mMaxPacketSize - headerArray.length - 6)) { 469 returnValue = true; 470 471 bodyLength = mMaxPacketSize - headerArray.length - 6; 472 } 473 474 byte[] body = mPrivateOutput.readBytes(bodyLength); 475 476 /* 477 * Since this is a put request if the final bit is set or 478 * the output stream is closed we need to send the 0x49 479 * (End of Body) otherwise, we need to send 0x48 (Body) 480 */ 481 if ((mPrivateOutput.isClosed()) && (!returnValue) && (!mEndOfBodySent) 482 && ((opCode & 0x80) != 0)) { 483 out.write(0x49); 484 mEndOfBodySent = true; 485 } else { 486 out.write(0x48); 487 } 488 489 bodyLength += 3; 490 out.write((byte)(bodyLength >> 8)); 491 out.write((byte)bodyLength); 492 493 if (body != null) { 494 out.write(body); 495 } 496 } 497 498 if (mPrivateOutputOpen && bodyLength <= 0 && !mEndOfBodySent) { 499 // only 0x82 or 0x83 can send 0x49 500 if ((opCode & 0x80) == 0) { 501 out.write(0x48); 502 } else { 503 out.write(0x49); 504 mEndOfBodySent = true; 505 506 } 507 508 bodyLength = 3; 509 out.write((byte)(bodyLength >> 8)); 510 out.write((byte)bodyLength); 511 } 512 513 if (out.size() == 0) { 514 if (!mParent.sendRequest(opCode, null, mReplyHeader, mPrivateInput)) { 515 return false; 516 } 517 return returnValue; 518 } 519 if ((out.size() > 0) 520 && (!mParent.sendRequest(opCode, out.toByteArray(), mReplyHeader, mPrivateInput))) { 521 return false; 522 } 523 524 // send all of the output data in 0x48, 525 // send 0x49 with empty body 526 if ((mPrivateOutput != null) && (mPrivateOutput.size() > 0)) 527 returnValue = true; 528 529 return returnValue; 530 } 531 532 /** 533 * This method starts the processing thread results. It will send the 534 * initial request. If the response takes more then one packet, a thread 535 * will be started to handle additional requests 536 * @throws IOException if an IO error occurs 537 */ 538 private synchronized void startProcessing() throws IOException { 539 540 if (mPrivateInput == null) { 541 mPrivateInput = new PrivateInputStream(this); 542 } 543 boolean more = true; 544 545 if (mGetOperation) { 546 if (!mOperationDone) { 547 mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; 548 while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { 549 more = sendRequest(0x03); 550 } 551 552 if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { 553 mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput); 554 } 555 if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { 556 mOperationDone = true; 557 } 558 } 559 } else { 560 561 if (!mOperationDone) { 562 mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; 563 while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { 564 more = sendRequest(0x02); 565 566 } 567 } 568 569 if (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { 570 mParent.sendRequest(0x82, null, mReplyHeader, mPrivateInput); 571 } 572 573 if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { 574 mOperationDone = true; 575 } 576 } 577 } 578 579 /** 580 * Continues the operation since there is no data to read. 581 * @param sendEmpty <code>true</code> if the operation should send an empty 582 * packet or not send anything if there is no data to send 583 * @param inStream <code>true</code> if the stream is input stream or is 584 * output stream 585 * @throws IOException if an IO error occurs 586 */ 587 public synchronized boolean continueOperation(boolean sendEmpty, boolean inStream) 588 throws IOException { 589 590 if (mGetOperation) { 591 if ((inStream) && (!mOperationDone)) { 592 // to deal with inputstream in get operation 593 mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput); 594 /* 595 * Determine if that was not the last packet in the operation 596 */ 597 if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { 598 mOperationDone = true; 599 } 600 601 return true; 602 603 } else if ((!inStream) && (!mOperationDone)) { 604 // to deal with outputstream in get operation 605 606 if (mPrivateInput == null) { 607 mPrivateInput = new PrivateInputStream(this); 608 } 609 sendRequest(0x03); 610 return true; 611 612 } else if (mOperationDone) { 613 return false; 614 } 615 616 } else { 617 if ((!inStream) && (!mOperationDone)) { 618 // to deal with outputstream in put operation 619 if (mReplyHeader.responseCode == -1) { 620 mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; 621 } 622 sendRequest(0x02); 623 return true; 624 } else if ((inStream) && (!mOperationDone)) { 625 // How to deal with inputstream in put operation ? 626 return false; 627 628 } else if (mOperationDone) { 629 return false; 630 } 631 632 } 633 return false; 634 } 635 636 /** 637 * Called when the output or input stream is closed. 638 * @param inStream <code>true</code> if the input stream is closed; 639 * <code>false</code> if the output stream is closed 640 * @throws IOException if an IO error occurs 641 */ 642 public void streamClosed(boolean inStream) throws IOException { 643 if (!mGetOperation) { 644 if ((!inStream) && (!mOperationDone)) { 645 // to deal with outputstream in put operation 646 647 boolean more = true; 648 649 if ((mPrivateOutput != null) && (mPrivateOutput.size() <= 0)) { 650 byte[] headerArray = ObexHelper.createHeader(mRequestHeader, false); 651 if (headerArray.length <= 0) 652 more = false; 653 } 654 // If have not sent any data so send all now 655 if (mReplyHeader.responseCode == -1) { 656 mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; 657 } 658 659 while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { 660 more = sendRequest(0x02); 661 } 662 663 /* 664 * According to the IrOBEX specification, after the final put, you 665 * only have a single reply to send. so we don't need the while 666 * loop. 667 */ 668 while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { 669 670 sendRequest(0x82); 671 } 672 mOperationDone = true; 673 } else if ((inStream) && (mOperationDone)) { 674 // how to deal with input stream in put stream ? 675 mOperationDone = true; 676 } 677 } else { 678 if ((inStream) && (!mOperationDone)) { 679 680 // to deal with inputstream in get operation 681 // Have not sent any data so send it all now 682 683 if (mReplyHeader.responseCode == -1) { 684 mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; 685 } 686 687 while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { 688 if (!sendRequest(0x83)) { 689 break; 690 } 691 } 692 while (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE) { 693 mParent.sendRequest(0x83, null, mReplyHeader, mPrivateInput); 694 } 695 mOperationDone = true; 696 } else if ((!inStream) && (!mOperationDone)) { 697 // to deal with outputstream in get operation 698 // part of the data may have been sent in continueOperation. 699 700 boolean more = true; 701 702 if ((mPrivateOutput != null) && (mPrivateOutput.size() <= 0)) { 703 byte[] headerArray = ObexHelper.createHeader(mRequestHeader, false); 704 if (headerArray.length <= 0) 705 more = false; 706 } 707 708 if (mPrivateInput == null) { 709 mPrivateInput = new PrivateInputStream(this); 710 } 711 if ((mPrivateOutput != null) && (mPrivateOutput.size() <= 0)) 712 more = false; 713 714 mReplyHeader.responseCode = ResponseCodes.OBEX_HTTP_CONTINUE; 715 while ((more) && (mReplyHeader.responseCode == ResponseCodes.OBEX_HTTP_CONTINUE)) { 716 more = sendRequest(0x03); 717 } 718 sendRequest(0x83); 719 // parent.sendRequest(0x83, null, replyHeaders, privateInput); 720 if (mReplyHeader.responseCode != ResponseCodes.OBEX_HTTP_CONTINUE) { 721 mOperationDone = true; 722 } 723 } 724 } 725 } 726 } 727