Home | History | Annotate | Download | only in smack
      1 /**
      2  * $RCSfile$
      3  * $Revision$
      4  * $Date$
      5  *
      6  * Copyright 2009 Jive Software.
      7  *
      8  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      9  * you may not use this file except in compliance with the License.
     10  * You may obtain a copy of the License at
     11  *
     12  *     http://www.apache.org/licenses/LICENSE-2.0
     13  *
     14  * Unless required by applicable law or agreed to in writing, software
     15  * distributed under the License is distributed on an "AS IS" BASIS,
     16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     17  * See the License for the specific language governing permissions and
     18  * limitations under the License.
     19  */
     20 
     21 package org.jivesoftware.smack;
     22 
     23 import java.io.IOException;
     24 import java.io.PipedReader;
     25 import java.io.PipedWriter;
     26 import java.io.Writer;
     27 import java.util.concurrent.ExecutorService;
     28 import java.util.concurrent.Executors;
     29 import java.util.concurrent.ThreadFactory;
     30 
     31 import org.jivesoftware.smack.Connection;
     32 import org.jivesoftware.smack.ConnectionCreationListener;
     33 import org.jivesoftware.smack.ConnectionListener;
     34 import org.jivesoftware.smack.PacketCollector;
     35 import org.jivesoftware.smack.Roster;
     36 import org.jivesoftware.smack.XMPPException;
     37 import org.jivesoftware.smack.packet.Packet;
     38 import org.jivesoftware.smack.packet.Presence;
     39 import org.jivesoftware.smack.packet.XMPPError;
     40 import org.jivesoftware.smack.util.StringUtils;
     41 
     42 import com.kenai.jbosh.BOSHClient;
     43 import com.kenai.jbosh.BOSHClientConfig;
     44 import com.kenai.jbosh.BOSHClientConnEvent;
     45 import com.kenai.jbosh.BOSHClientConnListener;
     46 import com.kenai.jbosh.BOSHClientRequestListener;
     47 import com.kenai.jbosh.BOSHClientResponseListener;
     48 import com.kenai.jbosh.BOSHException;
     49 import com.kenai.jbosh.BOSHMessageEvent;
     50 import com.kenai.jbosh.BodyQName;
     51 import com.kenai.jbosh.ComposableBody;
     52 
     53 /**
     54  * Creates a connection to a XMPP server via HTTP binding.
     55  * This is specified in the XEP-0206: XMPP Over BOSH.
     56  *
     57  * @see Connection
     58  * @author Guenther Niess
     59  */
     60 public class BOSHConnection extends Connection {
     61 
     62     /**
     63      * The XMPP Over Bosh namespace.
     64      */
     65     public static final String XMPP_BOSH_NS = "urn:xmpp:xbosh";
     66 
     67     /**
     68      * The BOSH namespace from XEP-0124.
     69      */
     70     public static final String BOSH_URI = "http://jabber.org/protocol/httpbind";
     71 
     72     /**
     73      * The used BOSH client from the jbosh library.
     74      */
     75     private BOSHClient client;
     76 
     77     /**
     78      * Holds the initial configuration used while creating the connection.
     79      */
     80     private final BOSHConfiguration config;
     81 
     82     // Some flags which provides some info about the current state.
     83     private boolean connected = false;
     84     private boolean authenticated = false;
     85     private boolean anonymous = false;
     86     private boolean isFirstInitialization = true;
     87     private boolean wasAuthenticated = false;
     88     private boolean done = false;
     89 
     90     /**
     91      * The Thread environment for sending packet listeners.
     92      */
     93     private ExecutorService listenerExecutor;
     94 
     95     // The readerPipe and consumer thread are used for the debugger.
     96     private PipedWriter readerPipe;
     97     private Thread readerConsumer;
     98 
     99     /**
    100      * The BOSH equivalent of the stream ID which is used for DIGEST authentication.
    101      */
    102     protected String authID = null;
    103 
    104     /**
    105      * The session ID for the BOSH session with the connection manager.
    106      */
    107     protected String sessionID = null;
    108 
    109     /**
    110      * The full JID of the authenticated user.
    111      */
    112     private String user = null;
    113 
    114     /**
    115      * The roster maybe also called buddy list holds the list of the users contacts.
    116      */
    117     private Roster roster = null;
    118 
    119 
    120     /**
    121      * Create a HTTP Binding connection to a XMPP server.
    122      *
    123      * @param https true if you want to use SSL
    124      *             (e.g. false for http://domain.lt:7070/http-bind).
    125      * @param host the hostname or IP address of the connection manager
    126      *             (e.g. domain.lt for http://domain.lt:7070/http-bind).
    127      * @param port the port of the connection manager
    128      *             (e.g. 7070 for http://domain.lt:7070/http-bind).
    129      * @param filePath the file which is described by the URL
    130      *             (e.g. /http-bind for http://domain.lt:7070/http-bind).
    131      * @param xmppDomain the XMPP service name
    132      *             (e.g. domain.lt for the user alice (at) domain.lt)
    133      */
    134     public BOSHConnection(boolean https, String host, int port, String filePath, String xmppDomain) {
    135         super(new BOSHConfiguration(https, host, port, filePath, xmppDomain));
    136         this.config = (BOSHConfiguration) getConfiguration();
    137     }
    138 
    139     /**
    140      * Create a HTTP Binding connection to a XMPP server.
    141      *
    142      * @param config The configuration which is used for this connection.
    143      */
    144     public BOSHConnection(BOSHConfiguration config) {
    145         super(config);
    146         this.config = config;
    147     }
    148 
    149     public void connect() throws XMPPException {
    150         if (connected) {
    151             throw new IllegalStateException("Already connected to a server.");
    152         }
    153         done = false;
    154         try {
    155             // Ensure a clean starting state
    156             if (client != null) {
    157                 client.close();
    158                 client = null;
    159             }
    160             saslAuthentication.init();
    161             sessionID = null;
    162             authID = null;
    163 
    164             // Initialize BOSH client
    165             BOSHClientConfig.Builder cfgBuilder = BOSHClientConfig.Builder
    166                     .create(config.getURI(), config.getServiceName());
    167             if (config.isProxyEnabled()) {
    168                 cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort());
    169             }
    170             client = BOSHClient.create(cfgBuilder.build());
    171 
    172             // Create an executor to deliver incoming packets to listeners.
    173             // We'll use a single thread with an unbounded queue.
    174             listenerExecutor = Executors
    175                     .newSingleThreadExecutor(new ThreadFactory() {
    176                         public Thread newThread(Runnable runnable) {
    177                             Thread thread = new Thread(runnable,
    178                                     "Smack Listener Processor ("
    179                                             + connectionCounterValue + ")");
    180                             thread.setDaemon(true);
    181                             return thread;
    182                         }
    183                     });
    184             client.addBOSHClientConnListener(new BOSHConnectionListener(this));
    185             client.addBOSHClientResponseListener(new BOSHPacketReader(this));
    186 
    187             // Initialize the debugger
    188             if (config.isDebuggerEnabled()) {
    189                 initDebugger();
    190                 if (isFirstInitialization) {
    191                     if (debugger.getReaderListener() != null) {
    192                         addPacketListener(debugger.getReaderListener(), null);
    193                     }
    194                     if (debugger.getWriterListener() != null) {
    195                         addPacketSendingListener(debugger.getWriterListener(), null);
    196                     }
    197                 }
    198             }
    199 
    200             // Send the session creation request
    201             client.send(ComposableBody.builder()
    202                     .setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
    203                     .setAttribute(BodyQName.createWithPrefix(XMPP_BOSH_NS, "version", "xmpp"), "1.0")
    204                     .build());
    205         } catch (Exception e) {
    206             throw new XMPPException("Can't connect to " + getServiceName(), e);
    207         }
    208 
    209         // Wait for the response from the server
    210         synchronized (this) {
    211             long endTime = System.currentTimeMillis() +
    212                            SmackConfiguration.getPacketReplyTimeout() * 6;
    213             while ((!connected) && (System.currentTimeMillis() < endTime)) {
    214                 try {
    215                     wait(Math.abs(endTime - System.currentTimeMillis()));
    216                 }
    217                 catch (InterruptedException e) {}
    218             }
    219         }
    220 
    221         // If there is no feedback, throw an remote server timeout error
    222         if (!connected && !done) {
    223             done = true;
    224             String errorMessage = "Timeout reached for the connection to "
    225                     + getHost() + ":" + getPort() + ".";
    226             throw new XMPPException(
    227                     errorMessage,
    228                     new XMPPError(XMPPError.Condition.remote_server_timeout, errorMessage));
    229         }
    230     }
    231 
    232     public String getConnectionID() {
    233         if (!connected) {
    234             return null;
    235         } else if (authID != null) {
    236             return authID;
    237         } else {
    238             return sessionID;
    239         }
    240     }
    241 
    242     public Roster getRoster() {
    243         if (roster == null) {
    244             return null;
    245         }
    246         if (!config.isRosterLoadedAtLogin()) {
    247             roster.reload();
    248         }
    249         // If this is the first time the user has asked for the roster after calling
    250         // login, we want to wait for the server to send back the user's roster.
    251         // This behavior shields API users from having to worry about the fact that
    252         // roster operations are asynchronous, although they'll still have to listen
    253         // for changes to the roster. Note: because of this waiting logic, internal
    254         // Smack code should be wary about calling the getRoster method, and may
    255         // need to access the roster object directly.
    256         if (!roster.rosterInitialized) {
    257             try {
    258                 synchronized (roster) {
    259                     long waitTime = SmackConfiguration.getPacketReplyTimeout();
    260                     long start = System.currentTimeMillis();
    261                     while (!roster.rosterInitialized) {
    262                         if (waitTime <= 0) {
    263                             break;
    264                         }
    265                         roster.wait(waitTime);
    266                         long now = System.currentTimeMillis();
    267                         waitTime -= now - start;
    268                         start = now;
    269                     }
    270                 }
    271             } catch (InterruptedException ie) {
    272                 // Ignore.
    273             }
    274         }
    275         return roster;
    276     }
    277 
    278     public String getUser() {
    279         return user;
    280     }
    281 
    282     public boolean isAnonymous() {
    283         return anonymous;
    284     }
    285 
    286     public boolean isAuthenticated() {
    287         return authenticated;
    288     }
    289 
    290     public boolean isConnected() {
    291         return connected;
    292     }
    293 
    294     public boolean isSecureConnection() {
    295         // TODO: Implement SSL usage
    296         return false;
    297     }
    298 
    299     public boolean isUsingCompression() {
    300         // TODO: Implement compression
    301         return false;
    302     }
    303 
    304     public void login(String username, String password, String resource)
    305             throws XMPPException {
    306         if (!isConnected()) {
    307             throw new IllegalStateException("Not connected to server.");
    308         }
    309         if (authenticated) {
    310             throw new IllegalStateException("Already logged in to server.");
    311         }
    312         // Do partial version of nameprep on the username.
    313         username = username.toLowerCase().trim();
    314 
    315         String response;
    316         if (config.isSASLAuthenticationEnabled()
    317                 && saslAuthentication.hasNonAnonymousAuthentication()) {
    318             // Authenticate using SASL
    319             if (password != null) {
    320                 response = saslAuthentication.authenticate(username, password, resource);
    321             } else {
    322                 response = saslAuthentication.authenticate(username, resource, config.getCallbackHandler());
    323             }
    324         } else {
    325             // Authenticate using Non-SASL
    326             response = new NonSASLAuthentication(this).authenticate(username, password, resource);
    327         }
    328 
    329         // Set the user.
    330         if (response != null) {
    331             this.user = response;
    332             // Update the serviceName with the one returned by the server
    333             config.setServiceName(StringUtils.parseServer(response));
    334         } else {
    335             this.user = username + "@" + getServiceName();
    336             if (resource != null) {
    337                 this.user += "/" + resource;
    338             }
    339         }
    340 
    341         // Create the roster if it is not a reconnection.
    342         if (this.roster == null) {
    343             if (this.rosterStorage == null) {
    344                 this.roster = new Roster(this);
    345             } else {
    346                 this.roster = new Roster(this, rosterStorage);
    347             }
    348         }
    349 
    350         // Set presence to online.
    351         if (config.isSendPresence()) {
    352             sendPacket(new Presence(Presence.Type.available));
    353         }
    354 
    355         // Indicate that we're now authenticated.
    356         authenticated = true;
    357         anonymous = false;
    358 
    359         if (config.isRosterLoadedAtLogin()) {
    360             this.roster.reload();
    361         }
    362         // Stores the autentication for future reconnection
    363         config.setLoginInfo(username, password, resource);
    364 
    365         // If debugging is enabled, change the the debug window title to include
    366         // the
    367         // name we are now logged-in as.l
    368         if (config.isDebuggerEnabled() && debugger != null) {
    369             debugger.userHasLogged(user);
    370         }
    371     }
    372 
    373     public void loginAnonymously() throws XMPPException {
    374     	if (!isConnected()) {
    375             throw new IllegalStateException("Not connected to server.");
    376         }
    377         if (authenticated) {
    378             throw new IllegalStateException("Already logged in to server.");
    379         }
    380 
    381         String response;
    382         if (config.isSASLAuthenticationEnabled() &&
    383                 saslAuthentication.hasAnonymousAuthentication()) {
    384             response = saslAuthentication.authenticateAnonymously();
    385         }
    386         else {
    387             // Authenticate using Non-SASL
    388             response = new NonSASLAuthentication(this).authenticateAnonymously();
    389         }
    390 
    391         // Set the user value.
    392         this.user = response;
    393         // Update the serviceName with the one returned by the server
    394         config.setServiceName(StringUtils.parseServer(response));
    395 
    396         // Anonymous users can't have a roster.
    397         roster = null;
    398 
    399         // Set presence to online.
    400         if (config.isSendPresence()) {
    401             sendPacket(new Presence(Presence.Type.available));
    402         }
    403 
    404         // Indicate that we're now authenticated.
    405         authenticated = true;
    406         anonymous = true;
    407 
    408         // If debugging is enabled, change the the debug window title to include the
    409         // name we are now logged-in as.
    410         // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
    411         // will be null
    412         if (config.isDebuggerEnabled() && debugger != null) {
    413             debugger.userHasLogged(user);
    414         }
    415     }
    416 
    417     public void sendPacket(Packet packet) {
    418         if (!isConnected()) {
    419             throw new IllegalStateException("Not connected to server.");
    420         }
    421         if (packet == null) {
    422             throw new NullPointerException("Packet is null.");
    423         }
    424         if (!done) {
    425             // Invoke interceptors for the new packet that is about to be sent.
    426             // Interceptors
    427             // may modify the content of the packet.
    428             firePacketInterceptors(packet);
    429 
    430             try {
    431                 send(ComposableBody.builder().setPayloadXML(packet.toXML())
    432                         .build());
    433             } catch (BOSHException e) {
    434                 e.printStackTrace();
    435                 return;
    436             }
    437 
    438             // Process packet writer listeners. Note that we're using the
    439             // sending
    440             // thread so it's expected that listeners are fast.
    441             firePacketSendingListeners(packet);
    442         }
    443     }
    444 
    445     public void disconnect(Presence unavailablePresence) {
    446         if (!connected) {
    447             return;
    448         }
    449         shutdown(unavailablePresence);
    450 
    451         // Cleanup
    452         if (roster != null) {
    453             roster.cleanup();
    454             roster = null;
    455         }
    456         sendListeners.clear();
    457         recvListeners.clear();
    458         collectors.clear();
    459         interceptors.clear();
    460 
    461         // Reset the connection flags
    462         wasAuthenticated = false;
    463         isFirstInitialization = true;
    464 
    465         // Notify connection listeners of the connection closing if done hasn't already been set.
    466         for (ConnectionListener listener : getConnectionListeners()) {
    467             try {
    468                 listener.connectionClosed();
    469             }
    470             catch (Exception e) {
    471                 // Catch and print any exception so we can recover
    472                 // from a faulty listener and finish the shutdown process
    473                 e.printStackTrace();
    474             }
    475         }
    476     }
    477 
    478     /**
    479      * Closes the connection by setting presence to unavailable and closing the
    480      * HTTP client. The shutdown logic will be used during a planned disconnection or when
    481      * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
    482      * BOSH packet reader and {@link Roster} will not be removed; thus
    483      * connection's state is kept.
    484      *
    485      * @param unavailablePresence the presence packet to send during shutdown.
    486      */
    487     protected void shutdown(Presence unavailablePresence) {
    488         setWasAuthenticated(authenticated);
    489         authID = null;
    490         sessionID = null;
    491         done = true;
    492         authenticated = false;
    493         connected = false;
    494         isFirstInitialization = false;
    495 
    496         try {
    497             client.disconnect(ComposableBody.builder()
    498                     .setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
    499                     .setPayloadXML(unavailablePresence.toXML())
    500                     .build());
    501             // Wait 150 ms for processes to clean-up, then shutdown.
    502             Thread.sleep(150);
    503         }
    504         catch (Exception e) {
    505             // Ignore.
    506         }
    507 
    508         // Close down the readers and writers.
    509         if (readerPipe != null) {
    510             try {
    511                 readerPipe.close();
    512             }
    513             catch (Throwable ignore) { /* ignore */ }
    514             reader = null;
    515         }
    516         if (reader != null) {
    517             try {
    518                 reader.close();
    519             }
    520             catch (Throwable ignore) { /* ignore */ }
    521             reader = null;
    522         }
    523         if (writer != null) {
    524             try {
    525                 writer.close();
    526             }
    527             catch (Throwable ignore) { /* ignore */ }
    528             writer = null;
    529         }
    530 
    531         // Shut down the listener executor.
    532         if (listenerExecutor != null) {
    533             listenerExecutor.shutdown();
    534         }
    535         readerConsumer = null;
    536     }
    537 
    538     /**
    539      * Sets whether the connection has already logged in the server.
    540      *
    541      * @param wasAuthenticated true if the connection has already been authenticated.
    542      */
    543     private void setWasAuthenticated(boolean wasAuthenticated) {
    544         if (!this.wasAuthenticated) {
    545             this.wasAuthenticated = wasAuthenticated;
    546         }
    547     }
    548 
    549     /**
    550      * Send a HTTP request to the connection manager with the provided body element.
    551      *
    552      * @param body the body which will be sent.
    553      */
    554     protected void send(ComposableBody body) throws BOSHException {
    555         if (!connected) {
    556             throw new IllegalStateException("Not connected to a server!");
    557         }
    558         if (body == null) {
    559             throw new NullPointerException("Body mustn't be null!");
    560         }
    561         if (sessionID != null) {
    562             body = body.rebuild().setAttribute(
    563                     BodyQName.create(BOSH_URI, "sid"), sessionID).build();
    564         }
    565         client.send(body);
    566     }
    567 
    568     /**
    569      * Processes a packet after it's been fully parsed by looping through the
    570      * installed packet collectors and listeners and letting them examine the
    571      * packet to see if they are a match with the filter.
    572      *
    573      * @param packet the packet to process.
    574      */
    575     protected void processPacket(Packet packet) {
    576         if (packet == null) {
    577             return;
    578         }
    579 
    580         // Loop through all collectors and notify the appropriate ones.
    581         for (PacketCollector collector : getPacketCollectors()) {
    582             collector.processPacket(packet);
    583         }
    584 
    585         // Deliver the incoming packet to listeners.
    586         listenerExecutor.submit(new ListenerNotification(packet));
    587     }
    588 
    589     /**
    590      * Initialize the SmackDebugger which allows to log and debug XML traffic.
    591      */
    592     protected void initDebugger() {
    593         // TODO: Maybe we want to extend the SmackDebugger for simplification
    594         //       and a performance boost.
    595 
    596         // Initialize a empty writer which discards all data.
    597         writer = new Writer() {
    598                 public void write(char[] cbuf, int off, int len) { /* ignore */}
    599                 public void close() { /* ignore */ }
    600                 public void flush() { /* ignore */ }
    601             };
    602 
    603         // Initialize a pipe for received raw data.
    604         try {
    605             readerPipe = new PipedWriter();
    606             reader = new PipedReader(readerPipe);
    607         }
    608         catch (IOException e) {
    609             // Ignore
    610         }
    611 
    612         // Call the method from the parent class which initializes the debugger.
    613         super.initDebugger();
    614 
    615         // Add listeners for the received and sent raw data.
    616         client.addBOSHClientResponseListener(new BOSHClientResponseListener() {
    617             public void responseReceived(BOSHMessageEvent event) {
    618                 if (event.getBody() != null) {
    619                     try {
    620                         readerPipe.write(event.getBody().toXML());
    621                         readerPipe.flush();
    622                     } catch (Exception e) {
    623                         // Ignore
    624                     }
    625                 }
    626             }
    627         });
    628         client.addBOSHClientRequestListener(new BOSHClientRequestListener() {
    629             public void requestSent(BOSHMessageEvent event) {
    630                 if (event.getBody() != null) {
    631                     try {
    632                         writer.write(event.getBody().toXML());
    633                     } catch (Exception e) {
    634                         // Ignore
    635                     }
    636                 }
    637             }
    638         });
    639 
    640         // Create and start a thread which discards all read data.
    641         readerConsumer = new Thread() {
    642             private Thread thread = this;
    643             private int bufferLength = 1024;
    644 
    645             public void run() {
    646                 try {
    647                     char[] cbuf = new char[bufferLength];
    648                     while (readerConsumer == thread && !done) {
    649                         reader.read(cbuf, 0, bufferLength);
    650                     }
    651                 } catch (IOException e) {
    652                     // Ignore
    653                 }
    654             }
    655         };
    656         readerConsumer.setDaemon(true);
    657         readerConsumer.start();
    658     }
    659 
    660     /**
    661      * Sends out a notification that there was an error with the connection
    662      * and closes the connection.
    663      *
    664      * @param e the exception that causes the connection close event.
    665      */
    666     protected void notifyConnectionError(Exception e) {
    667         // Closes the connection temporary. A reconnection is possible
    668         shutdown(new Presence(Presence.Type.unavailable));
    669         // Print the stack trace to help catch the problem
    670         e.printStackTrace();
    671         // Notify connection listeners of the error.
    672         for (ConnectionListener listener : getConnectionListeners()) {
    673             try {
    674                 listener.connectionClosedOnError(e);
    675             }
    676             catch (Exception e2) {
    677                 // Catch and print any exception so we can recover
    678                 // from a faulty listener
    679                 e2.printStackTrace();
    680             }
    681         }
    682     }
    683 
    684 
    685     /**
    686      * A listener class which listen for a successfully established connection
    687      * and connection errors and notifies the BOSHConnection.
    688      *
    689      * @author Guenther Niess
    690      */
    691     private class BOSHConnectionListener implements BOSHClientConnListener {
    692 
    693         private final BOSHConnection connection;
    694 
    695         public BOSHConnectionListener(BOSHConnection connection) {
    696             this.connection = connection;
    697         }
    698 
    699         /**
    700          * Notify the BOSHConnection about connection state changes.
    701          * Process the connection listeners and try to login if the
    702          * connection was formerly authenticated and is now reconnected.
    703          */
    704         public void connectionEvent(BOSHClientConnEvent connEvent) {
    705             try {
    706                 if (connEvent.isConnected()) {
    707                     connected = true;
    708                     if (isFirstInitialization) {
    709                         isFirstInitialization = false;
    710                         for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
    711                             listener.connectionCreated(connection);
    712                         }
    713                     }
    714                     else {
    715                         try {
    716                             if (wasAuthenticated) {
    717                                 connection.login(
    718                                         config.getUsername(),
    719                                         config.getPassword(),
    720                                         config.getResource());
    721                             }
    722                             for (ConnectionListener listener : getConnectionListeners()) {
    723                                  listener.reconnectionSuccessful();
    724                             }
    725                         }
    726                         catch (XMPPException e) {
    727                             for (ConnectionListener listener : getConnectionListeners()) {
    728                                 listener.reconnectionFailed(e);
    729                            }
    730                         }
    731                     }
    732                 }
    733                 else {
    734                     if (connEvent.isError()) {
    735                         try {
    736                             connEvent.getCause();
    737                         }
    738                         catch (Exception e) {
    739                             notifyConnectionError(e);
    740                         }
    741                     }
    742                     connected = false;
    743                 }
    744             }
    745             finally {
    746                 synchronized (connection) {
    747                     connection.notifyAll();
    748                 }
    749             }
    750         }
    751     }
    752 
    753     /**
    754      * This class notifies all listeners that a packet was received.
    755      */
    756     private class ListenerNotification implements Runnable {
    757 
    758         private Packet packet;
    759 
    760         public ListenerNotification(Packet packet) {
    761             this.packet = packet;
    762         }
    763 
    764         public void run() {
    765             for (ListenerWrapper listenerWrapper : recvListeners.values()) {
    766                 listenerWrapper.notifyListener(packet);
    767             }
    768         }
    769     }
    770 
    771 	@Override
    772 	public void setRosterStorage(RosterStorage storage)
    773 			throws IllegalStateException {
    774 		if(this.roster!=null){
    775 			throw new IllegalStateException("Roster is already initialized");
    776 		}
    777 		this.rosterStorage = storage;
    778 	}
    779 }
    780