root/trunk/LogicMail/src/org/logicprobe/LogicMail/util/Connection.java

Revision 449, 19.3 KB (checked in by octorian, 4 weeks ago)
  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1/*-
2 * Copyright (c) 2006, Derek Konigsberg
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 *    notice, this list of conditions and the following disclaimer.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 *    notice, this list of conditions and the following disclaimer in the
13 *    documentation and/or other materials provided with the distribution.
14 * 3. Neither the name of the project nor the names of its
15 *    contributors may be used to endorse or promote products derived
16 *    from this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
27 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
29 * OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31
32/******************************************************************************
33 * This class has been modified to integrate J2ME socket connection code,
34 * since LogicMail has no need for a dual-use implementation.  The bulk of
35 * this code is a combination of Connection.java and the J2ME ConnectionImpl.java
36 * from Mail4ME, and is covered under the copyright shown below.  Any code not
37 * originating from Mail4ME is covered under the copyright shown at the top of
38 * this source file.
39 *
40 * Mail4ME - Mail for the Java 2 Micro Edition
41 *
42 * A lightweight, J2ME- (and also J2SE-) compatible package for sending and
43 * receiving Internet mail messages using the SMTP and POP3 protocols.
44 *
45 * Copyright (c) 2000-2002 Jorg Pleumann <joerg@pleumann.de>
46 *
47 * Mail4ME is part of the EnhydraME family of projects. See the following web
48 * sites for more information:
49 *
50 * -> http://mail4me.enhydra.org
51 * -> http://me.enhydra.org
52 *
53 * Mail4ME is distributed under the Enhydra Public License (EPL), which is
54 * discussed in great detail here:
55 *
56 * -> http://www.enhydra.org/software/license/index.html
57 *
58 * Have fun!
59 ******************************************************************************/
60package org.logicprobe.LogicMail.util;
61
62import net.rim.device.api.system.EventLogger;
63import net.rim.device.api.ui.UiApplication;
64import net.rim.device.api.ui.component.Dialog;
65import net.rim.device.api.util.DataBuffer;
66
67import org.logicprobe.LogicMail.AppInfo;
68import org.logicprobe.LogicMail.conf.GlobalConfig;
69import org.logicprobe.LogicMail.conf.MailSettings;
70
71import java.io.IOException;
72import java.io.InputStream;
73import java.io.OutputStream;
74
75import java.util.Vector;
76
77import javax.microedition.io.Connector;
78import javax.microedition.io.SocketConnection;
79import javax.microedition.io.StreamConnection;
80
81
82/**
83 * Is the abstract base class for socket connections used inside the SMTP,
84 * POP3 and IMAP protocols of the mail package. The rationale for using this class
85 * is the difference in how networking is handled in J2ME and J2SE: While J2SE
86 * has java.net.Socket, J2ME uses the Generic Connection Framework with its
87 * javax.io.Connector class as a central means to open sockets or HTTP
88 * connection. Unfortunately, both methods are totally incompatible with each
89 * other, so a common abstraction has to be found to make the mail package
90 * work in both environments.
91 * <p>
92 * The Connection class is such an abstraction. It provides abstract
93 * versions of methods that open and close sockets, read from them and write
94 * to them. It also provides a static factory method that is able to instantiate
95 * one of several sub-classes of Connection that match the different run-time
96 * environments. These sub-classes are all called ConnectionImpl, and they are
97 * loaded "by name" from the j2me, j2se, or http packages, respectively. This
98 * is the only way to get rid of compile-time dependencies on these classes.
99 */
100public class Connection {
101    /**
102     * Byte array holding carriage return and line feed
103     */
104    private static final byte[] CRLF = new byte[] { 13, 10 };
105
106    /**
107     * Holds a list of open connections
108     */
109    private static Vector openConnections = new Vector();
110    private String serverName;
111    private int serverPort;
112    private boolean useSSL;
113    private boolean deviceSide;
114    private StreamConnection socket;
115    private String localAddress;
116    private GlobalConfig globalConfig;
117    protected InputStream input;
118    protected OutputStream output;
119    private boolean useWiFi;
120    private int fakeAvailable = -1;
121
122    /**
123     * Provides a buffer used for incoming data.
124     */
125    private byte[] buffer = new byte[128];
126
127    /**
128     * Provides a dynamic buffer for building results
129     */
130    DataBuffer resultBuffer = new DataBuffer();
131
132    public Connection(String serverName, int serverPort, boolean useSSL,
133        boolean deviceSide) {
134        this.serverName = serverName;
135        this.serverPort = serverPort;
136        this.useSSL = useSSL;
137        this.deviceSide = deviceSide;
138        this.input = null;
139        this.output = null;
140        this.socket = null;
141        this.globalConfig = MailSettings.getInstance().getGlobalConfig();
142    }
143
144    /**
145     * Opens a connection.
146     */
147    public synchronized void open() throws IOException {
148        if ((input != null) || (output != null) || (socket != null)) {
149            close();
150        }
151
152        synchronized (openConnections) {
153            if (!openConnections.contains(this)) {
154                openConnections.addElement(this);
155            }
156        }
157
158        String protocolStr = (useSSL ? "ssl" : "socket");
159
160        // This parameter, which allows bypassing the MDS proxy, should probably
161        // be a global user configurable option
162        String paramStr = (deviceSide ? ";deviceside=true" : "");
163
164        useWiFi = false;
165
166        if (globalConfig.getWifiMode() == GlobalConfig.WIFI_PROMPT) {
167            UiApplication.getUiApplication().invokeAndWait(new Runnable() {
168                    public void run() {
169                        useWiFi = (Dialog.ask(Dialog.D_YES_NO,
170                                "Connect through WiFi?") == Dialog.YES);
171                    }
172                });
173        } else if (globalConfig.getWifiMode() == GlobalConfig.WIFI_ALWAYS) {
174            useWiFi = true;
175        }
176
177        if (useWiFi) {
178            paramStr = paramStr + ";interface=wifi";
179        }
180
181        String connectStr = protocolStr + "://" + serverName + ":" +
182            serverPort + paramStr;
183
184        if (EventLogger.getMinimumLevel() >= EventLogger.INFORMATION) {
185            String msg = "Opening connection:\r\n" + connectStr + "\r\n";
186            EventLogger.logEvent(AppInfo.GUID, msg.getBytes(),
187                EventLogger.INFORMATION);
188        }
189
190        socket = (StreamConnection) Connector.open(connectStr,
191                Connector.READ_WRITE, true);
192        input = socket.openDataInputStream();
193        output = socket.openDataOutputStream();
194        localAddress = ((SocketConnection) socket).getLocalAddress();
195
196        if (EventLogger.getMinimumLevel() >= EventLogger.INFORMATION) {
197            String msg = "Connection established:\r\n" + "Socket: " +
198                socket.getClass().toString() + "\r\n" + "Local address: " +
199                localAddress + "\r\n";
200            EventLogger.logEvent(AppInfo.GUID, msg.getBytes(),
201                EventLogger.INFORMATION);
202        }
203    }
204
205    /**
206     * Closes a connection.
207     */
208    public synchronized void close() throws IOException {
209        try {
210            if (input != null) {
211                input.close();
212                input = null;
213            }
214        } catch (Exception exp) {
215            input = null;
216        }
217
218        try {
219            if (output != null) {
220                output.close();
221                output = null;
222            }
223        } catch (Exception exp) {
224            output = null;
225        }
226
227        try {
228            if (socket != null) {
229                socket.close();
230                socket = null;
231            }
232        } catch (Exception exp) {
233            socket = null;
234        }
235
236        synchronized (openConnections) {
237            if (openConnections.contains(this)) {
238                openConnections.removeElement(this);
239            }
240        }
241
242        EventLogger.logEvent(AppInfo.GUID, "Connection closed".getBytes(),
243            EventLogger.INFORMATION);
244    }
245
246    /**
247     * Determine whether open connections exist
248     *
249     * @return True if there are open connections
250     */
251    public static boolean hasOpenConnections() {
252        boolean result;
253
254        synchronized (openConnections) {
255            result = !openConnections.isEmpty();
256        }
257
258        return result;
259    }
260
261    /**
262     * Close all open connections
263     */
264    public static void closeAllConnections() {
265        synchronized (openConnections) {
266            int size = openConnections.size();
267
268            for (int i = 0; i < size; i++) {
269                try {
270                    ((Connection) openConnections.elementAt(i)).close();
271                } catch (IOException e) {
272                }
273            }
274
275            openConnections.removeAllElements();
276        }
277    }
278
279    /**
280     * Determine whether we are currently connected
281     * @return True if connected
282     */
283    public boolean isConnected() {
284        if (socket != null) {
285            return true;
286        } else {
287            return false;
288        }
289    }
290
291    /**
292     * Get the local address to which this connection is bound
293     * @return Local address
294     */
295    public String getLocalAddress() {
296        return localAddress;
297    }
298
299    /**
300     * Get the server name used when this connection was created
301     * @return Server name
302     */
303    public String getServerName() {
304        return serverName;
305    }
306
307    /**
308     * Sends a string to the server. This method is used internally for
309     * all outgoing communication to the server. The main thing it does
310     * it terminate the line with a CR/LF. If there are occurrences of CR or
311     * LF inside the string, the method ensures that proper CR/LF sequences
312     * are sent for them, since this is what most Internet protocols expect.
313     *
314     * @see #receive
315     */
316    public synchronized void send(String s) throws IOException {
317        byte[] bytes = s.getBytes();
318        int length = bytes.length;
319
320        /**
321         * Special case for empty strings: Only CR/LF is sent.
322         */
323        if (s.length() == 0) {
324            if (globalConfig.getConnDebug()) {
325                EventLogger.logEvent(AppInfo.GUID, "[SEND]".getBytes(),
326                    EventLogger.DEBUG_INFO);
327            }
328
329            output.write(CRLF, 0, 2);
330        }
331        /**
332         * The usual case goes here.
333         */
334        else {
335            int i = 0;
336
337            while (i < length) {
338                int j = i;
339
340                /**
341                 * Find next occurrence of a line separator or the end of the
342                 * string.
343                 */
344                while ((j < length) && (bytes[j] != 0x0A) &&
345                        (bytes[j] != 0x0D)) {
346                    j++;
347                }
348
349                if (globalConfig.getConnDebug()) {
350                    EventLogger.logEvent(AppInfo.GUID,
351                        ("[SEND] " + s.substring(i, j)).getBytes(),
352                        EventLogger.DEBUG_INFO);
353                }
354
355                /**
356                 * Write the string up to there and terminate it properly.
357                 */
358                output.write((s.substring(i, j) + "\r\n").getBytes());
359
360                /**
361                 * If we stopped at a CR, ignore a possibly following LF.
362                 */
363                if ((j < (length - 1)) && (bytes[j] == 0x0D) &&
364                        (bytes[j + 1] == 0x0A)) {
365                    j++;
366                }
367
368                i = j + 1;
369            }
370        }
371
372        output.flush();
373    }
374
375    /**
376     * Sends a string to the server, terminating it with a CRLF.
377     * No cleanup is performed, as it is expected that the string
378     * is a prepared protocol command.
379     */
380    public synchronized void sendCommand(String s) throws IOException {
381        if (globalConfig.getConnDebug()) {
382            EventLogger.logEvent(AppInfo.GUID, ("[SEND CMD] " + s).getBytes(),
383                EventLogger.DEBUG_INFO);
384        }
385
386        if (s == null) {
387            output.write(CRLF, 0, 2);
388        } else {
389            output.write((s + "\r\n").getBytes());
390        }
391
392        output.flush();
393    }
394
395    /**
396     * Sends a string to the server. This method is used to bypass all
397     * the processing done by the normal send method, and is most useful
398     * for bulk transmissions.  It writes the provided string to the socket
399     * in a single command, followed by a flush.
400     *
401     * @see #send
402     */
403    public synchronized void sendRaw(String s) throws IOException {
404        byte[] bytes = s.getBytes();
405
406        if (globalConfig.getConnDebug()) {
407            EventLogger.logEvent(AppInfo.GUID,
408                ("[SEND RAW]\r\n" + s).getBytes(), EventLogger.DEBUG_INFO);
409        }
410
411        output.write(bytes, 0, bytes.length);
412
413        output.flush();
414    }
415
416    /**
417     * Returns the number of bytes available for reading.
418     * Used to poll the connection without blocking.
419     *
420     * @see InputStream#available()
421     */
422    public int available() throws IOException {
423        if (fakeAvailable == -1) {
424            return input.available();
425        } else {
426            return fakeAvailable;
427        }
428    }
429
430    /**
431     * Receives a string from the server. This method is used internally for
432     * incoming communication from the server. The main thing it does is
433     * ensuring that only complete lines are returned to the application, that is,
434     * lines that were terminated at least by a CR. LFs are ignored completely.
435     * Neither CRs nor LFs are returned as part of the result.
436     *
437     * @see #send
438     */
439    public synchronized String receive() throws IOException {
440        /**
441         * A note on how this method works and why it is designed the
442         * way it is: A previous implementation tried to read multiple
443         * bytes from the InputStream, searched a possible line terminator
444         * in them, and appended characters to a temporary result string.
445         * That didn't work very well, because the code had to deal
446         * with a large number of temporary strings that grew as more
447         * characters belonging to a line were read. The result was
448         * a high level of heap fragmentation, which made applications
449         * unstable on MIDP devices (that don't provide compacting GC,
450         * that is).
451         *
452         * This new implementation seems to work better: Is reads
453         * characters from the InputStream one-by-one, putting them
454         * in a buffer of fixed size that is large enough to hold a
455         * "normal" line belonging to an e-mail. When a terminator
456         * is found, a new String object is created from that buffer.
457         * Since the buffer is of fixed size and used during the whole
458         * lifetime of the Connection object, and the String is the
459         * actual String that makes it into the final Message object,
460         * there are no temporary Strings needed -- at least not in the
461         * common case: If a mail server tends to send whole messages
462         * without line terminators (and thus disregards for example
463         * the POP3 specification that dictates a maximum response of
464         * 512 characters), temporary Strings will again be necessary,
465         * but there's not much one can about that.
466         */
467        boolean stop = false;
468        resultBuffer.reset();
469
470        int actualAvailable = input.available();
471        int readBytes = 0;
472        int count;
473
474        /**
475         * The "stop" flag will be set as soon as we have received
476         * a complete line, that is, a line terminated by CR/LF. Until
477         * this is the case, the following block is repeated.
478         */
479        while (!stop) {
480            count = 0;
481
482            /**
483             * The inner block reads single bytes from the InputStream
484             * until, again, a line terminator is read or the buffer is
485             * full.
486             */
487            while (true) {
488                int actual = input.read(buffer, count, 1);
489
490                /**
491                 * If -1 is returned, the InputStream is already closed,
492                 * probably because the connection is broken, or the server
493                 * didn't like is. Try to close the connection, but ignore
494                 * any errors that might result from this.
495                 */
496                if (actual == -1) {
497                    EventLogger.logEvent(AppInfo.GUID,
498                        "Unable to read from socket, closing connection".getBytes(),
499                        EventLogger.INFORMATION);
500
501                    try {
502                        close();
503                    } catch (IOException e) {
504                    }
505
506                    throw new IOException("Connection closed");
507                }
508                /**
509                 * If no bytes have been received, we wait a little
510                 * while (by yielding processing time to other threads).
511                 */
512                else if (actual == 0) {
513                    try {
514                        Thread.yield();
515                    } catch (Exception e) {
516                    }
517                }
518                /**
519                 * If a byte has been read, examine it and put it in the
520                 * buffer.
521                 */
522
523                // Note: We really should look for CRLF, and not use this
524                // approach which screws up on mid-line LFs. (DK)
525                else {
526                    byte b = buffer[count];
527                    readBytes++;
528
529                    /**
530                     * Ignore all CRs.
531                     */
532                    if (b == 0x0D) {
533                        /* Ignore CRs */
534                    }
535                    /**
536                     * Take LF as line separator.
537                     */
538                    else if (b == 0x0A) {
539                        stop = true;
540
541                        break;
542                    }
543                    /**
544                     * Everything else makes it into the buffer.
545                     */
546                    else {
547                        count++;
548
549                        if (count == buffer.length) {
550                            break;
551                        }
552                    }
553                }
554            }
555
556            resultBuffer.write(buffer, 0, count);
557        }
558
559        String result = new String(resultBuffer.toArray());
560
561        if (globalConfig.getConnDebug()) {
562            EventLogger.logEvent(AppInfo.GUID,
563                ("[RECV] " + result).getBytes(),
564                EventLogger.DEBUG_INFO);
565        }
566
567        if (actualAvailable > readBytes) {
568            fakeAvailable = actualAvailable - readBytes;
569        } else {
570            fakeAvailable = -1;
571        }
572
573        return result;
574    }
575}
Note: See TracBrowser for help on using the browser.