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, 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