aboutsummaryrefslogtreecommitdiffstats
path: root/src/net/java/sip/communicator/impl/protocol/irc/IrcConnection.java
blob: 1cfad0d82263cb712e8229aaf7a5999e6de42921 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package net.java.sip.communicator.impl.protocol.irc;

import java.io.*;
import java.util.*;

import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;

import com.ircclouds.irc.api.*;
import com.ircclouds.irc.api.domain.messages.*;
import com.ircclouds.irc.api.listeners.*;
import com.ircclouds.irc.api.state.*;

/**
 * IRC Connection.
 *
 * TODO Show MOTD in Jitsi "System Room" or something similar, since the MOTD is
 * aimed directly at the local user.
 *
 * TODO Find out how irc-api responds to losing a connection (no response). Does
 * it use ping/pong messages to determine connectivity?
 * @author Danny van Heumen
 */
public class IrcConnection
{
    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(IrcConnection.class);

    /**
     * Set of characters with special meanings for IRC, such as: ',' used as
     * separator of list of items (channels, nicks, etc.), ' ' (space) separator
     * of command parameters, etc.
     */
    public static final Set<Character> SPECIAL_CHARACTERS;

    /**
     * Initialize set of special characters.
     */
    static
    {
        HashSet<Character> specials = new HashSet<Character>();
        specials.add('\0');
        specials.add('\n');
        specials.add('\r');
        specials.add(' ');
        specials.add(',');
        SPECIAL_CHARACTERS = Collections.unmodifiableSet(specials);
    }

    /**
     * Context.
     */
    private final IrcStack.PersistentContext context;

    /**
     * IRC client configuration for managing (more advanced) client behaviour
     * such as the use of periodic tasks for querying presence of contacts and
     * channel members.
     */
    private final ClientConfig config;

    /**
     * IRC Api instance.
     */
    private final IRCApi irc;

    /**
     * Connection state of a successful IRC connection.
     */
    private final IIRCState connectionState;

    /**
     * Callback to inform on connection interruptions.
     */
    private final IrcConnectionListener connectionListener;

    /**
     * Manager component that manages current IRC presence.
     */
    private final PresenceManager presence;

    /**
     * Manager component for server channel listing.
     */
    private final ServerChannelLister channelLister;

    /**
     * The local user's identity as it will be used in server-client
     * communication for sent messages.
     */
    private final IdentityManager identity;

    /**
     * The channel manager instance.
     */
    private final ChannelManager channel;

    /**
     * The message manager instance.
     */
    private final MessageManager message;

    /**
     * Constructor.
     *
     * @param context persistent context that crosses connections
     * @param config client configuration
     * @param irc the irc instance
     * @param params connection parameters
     * @param connectionListener listener for callback upon connection
     *            interruption
     * @throws Exception Throws IOException in case of connection problems.
     */
    IrcConnection(final IrcStack.PersistentContext context,
        final ClientConfig config, final IRCApi irc,
        final IServerParameters params,
        final IrcConnectionListener connectionListener)
        throws Exception
    {
        if (context == null)
        {
            throw new IllegalArgumentException("context cannot be null");
        }
        this.context = context;
        if (config == null)
        {
            throw new IllegalArgumentException("client config cannot be null");
        }
        this.config = config;
        if (irc == null)
        {
            throw new IllegalArgumentException("irc instance cannot be null");
        }
        this.irc = irc;
        this.connectionListener = connectionListener;

        // Install a listener for everything that is not directly related to a
        // specific chat room or operation.
        this.irc.addListener(new ServerListener());

        // Now actually connect to the IRC server.
        this.connectionState =
            connectSynchronized(this.context.provider, params, this.irc);

        // instantiate identity manager for the connection
        this.identity =
            new IdentityManager(this.irc, this.connectionState,
                this.context.provider);

        // instantiate message manager for the connection
        this.message =
            new MessageManager(this, this.irc, this.connectionState,
                this.context.provider, this.identity);

        // instantiate channel manager for the connection
        this.channel =
            new ChannelManager(this.irc, this.connectionState,
                this.context.provider, this.config);

        // instantiate presence manager for the connection
        this.presence =
            new PresenceManager(this.irc, this.connectionState,
                this.context.provider.getPersistentPresence(),
                this.config, this.context.nickWatchList);

        // instantiate server channel lister
        this.channelLister =
            new ServerChannelLister(this.irc, this.connectionState);
    }

    /**
     * Perform synchronized connect operation.
     *
     * @param provider Parent protocol provider
     * @param params Server connection parameters
     * @param irc IRC Api instance
     * @throws Exception exception thrown when connect fails
     */
    private static IIRCState connectSynchronized(
        final ProtocolProviderServiceIrcImpl provider,
        final IServerParameters params, final IRCApi irc) throws Exception
    {
        final Result<IIRCState, Exception> result =
            new Result<IIRCState, Exception>();
        synchronized (result)
        {
            // start connecting to the specified server ...
            irc.connect(params, new Callback<IIRCState>()
            {

                @Override
                public void onSuccess(final IIRCState state)
                {
                    synchronized (result)
                    {
                        LOGGER.trace("IRC connected successfully!");
                        result.setDone(state);
                        result.notifyAll();
                    }
                }

                @Override
                public void onFailure(final Exception e)
                {
                    synchronized (result)
                    {
                        LOGGER.trace("IRC connection FAILED!", e);
                        result.setDone(e);
                        result.notifyAll();
                    }
                }
            });

            provider.setCurrentRegistrationState(RegistrationState.REGISTERING,
                RegistrationStateChangeEvent.REASON_USER_REQUEST);

            while (!result.isDone())
            {
                LOGGER.trace("Waiting for the connection to be "
                    + "established ...");
                result.wait();
            }
        }

        // TODO Implement connection timeout and a way to recognize that
        // the timeout occurred.

        final Exception e = result.getException();
        if (e != null)
        {
            throw new IOException(e);
        }

        final IIRCState state = result.getValue();
        if (state == null)
        {
            throw new IOException(
                "Failed to connect to IRC server: connection state is null");
        }

        return state;
    }

    /**
     * Check whether or not a connection is established.
     *
     * @return true if connected, false otherwise.
     */
    public boolean isConnected()
    {
        return this.connectionState != null
            && this.connectionState.isConnected();
    }

    /**
     * Check whether the connection is a secure connection (TLS).
     *
     * @return true if connection is secure, false otherwise.
     */
    public boolean isSecureConnection()
    {
        return isConnected() && this.connectionState.getServer().isSSL();
    }

    /**
     * Disconnect.
     */
    void disconnect()
    {
        try
        {
            this.irc.disconnect();
        }
        catch (RuntimeException e)
        {
            // Disconnect might throw ChannelClosedException. Shouldn't be a
            // problem, but for now lets log it just to be sure.
            LOGGER.debug("exception occurred while disconnecting", e);
        }
    }

    /**
     * Get the IRC client library instance.
     *
     * @return returns the client instance
     */
    public IRCApi getClient()
    {
        return this.irc;
    }

    /**
     * Get the presence manager. (Guaranteed to be non-null.)
     *
     * @return returns the presence manager instance
     */
    public PresenceManager getPresenceManager()
    {
        return this.presence;
    }

    /**
     * Get the channel lister that facilitates server channel queries.
     * (Guaranteed to be non-null.)
     *
     * @return returns the channel lister instance
     */
    public ServerChannelLister getServerChannelLister()
    {
        return this.channelLister;
    }

    /**
     * Get the identity manager instance. (Guaranteed to be non-null.)
     *
     * @return returns the identity manager instance
     */
    public IdentityManager getIdentityManager()
    {
        return this.identity;
    }

    /**
     * Get the channel manager instance. (Guaranteed to be non-null.)
     *
     * @return returns the channel manager instance
     */
    public ChannelManager getChannelManager()
    {
        return this.channel;
    }

    /**
     * Get the message manager instance. (Guaranteed to be non-null.)
     *
     * @return returns the message manager instance
     */
    public MessageManager getMessageManager()
    {
        return this.message;
    }

    /**
     * A listener for server-level messages (any messages that are related to
     * the server, the connection, that are not related to any chatroom in
     * particular) or that are personal message from user to local user.
     */
    private final class ServerListener
        extends VariousMessageListenerAdapter
    {

        /**
         * Print out server notices for debugging purposes and for simply
         * keeping track of the connections.
         *
         * @param msg the server notice
         */
        @Override
        public void onServerNotice(final ServerNotice msg)
        {
            LOGGER.debug("NOTICE: " + msg.getText());
        }

        /**
         * Print out received errors for debugging purposes and may be for
         * expected errors that can be acted upon.
         *
         * @param msg the error message
         */
        @Override
        public void onError(final ErrorMessage msg)
        {
            if (LOGGER.isDebugEnabled())
            {
                LOGGER
                    .debug("ERROR: " + msg.getSource() + ": " + msg.getText());
            }

            // Errors signal fatal situation, so unregister and assume
            // connection lost.
            LOGGER.debug("Local user received ERROR message: removing server "
                + "listener.");
            IrcConnection.this.irc.deleteListener(this);

            // If listener is available, inform of connection interrupt.
            if (IrcConnection.this.connectionListener != null)
            {
                IrcConnection.this.connectionListener
                    .connectionInterrupted(IrcConnection.this);
            }
        }

        /**
         * User quit messages.
         *
         * User quit messages only need to be handled in case quitting users,
         * since that is the only clear signal of presence change we have.
         *
         * @param msg Quit message
         */
        @Override
        public void onUserQuit(final QuitMessage msg)
        {
            final String user = msg.getSource().getNick();
            if (!IrcConnection.this.connectionState.getNickname().equals(user))
            {
                return;
            }

            LOGGER.debug("Local user's QUIT message received: removing "
                + "server listener.");
            IrcConnection.this.irc.deleteListener(this);

            // If listener is available, inform of connection interrupt.
            if (IrcConnection.this.connectionListener != null)
            {
                IrcConnection.this.connectionListener
                    .connectionInterrupted(IrcConnection.this);
            }
        }
    }
}