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