001package com.pusher.client;
002
003import com.pusher.client.channel.Channel;
004import com.pusher.client.channel.ChannelEventListener;
005import com.pusher.client.channel.PresenceChannel;
006import com.pusher.client.channel.PresenceChannelEventListener;
007import com.pusher.client.channel.PrivateChannel;
008import com.pusher.client.channel.PrivateChannelEventListener;
009import com.pusher.client.channel.PrivateEncryptedChannel;
010import com.pusher.client.channel.PrivateEncryptedChannelEventListener;
011import com.pusher.client.channel.PusherEvent;
012import com.pusher.client.channel.SubscriptionEventListener;
013import com.pusher.client.channel.impl.ChannelManager;
014import com.pusher.client.channel.impl.InternalChannel;
015import com.pusher.client.channel.impl.PresenceChannelImpl;
016import com.pusher.client.channel.impl.PrivateChannelImpl;
017import com.pusher.client.channel.impl.PrivateEncryptedChannelImpl;
018import com.pusher.client.connection.Connection;
019import com.pusher.client.connection.ConnectionEventListener;
020import com.pusher.client.connection.ConnectionState;
021import com.pusher.client.connection.impl.InternalConnection;
022import com.pusher.client.user.User;
023import com.pusher.client.user.impl.InternalUser;
024import com.pusher.client.util.Factory;
025
026/**
027 * This class is the main entry point for accessing Pusher.
028 *
029 * <p>
030 * By creating a new {@link Pusher} instance and calling {@link
031 * Pusher#connect()} a connection to Pusher is established.
032 * </p>
033 *
034 * <p>
035 * Subscriptions for data are represented by
036 * {@link com.pusher.client.channel.Channel} objects, or subclasses thereof.
037 * Subscriptions are created by calling {@link Pusher#subscribe(String)},
038 * {@link Pusher#subscribePrivate(String)},
039 * {@link Pusher#subscribePresence(String)} or one of the overloads.
040 * </p>
041 */
042public class Pusher implements Client {
043
044    private final PusherOptions pusherOptions;
045    private final InternalConnection connection;
046    private final ChannelManager channelManager;
047    private final Factory factory;
048    private final InternalUser user;
049
050    /**
051     * Creates a new instance of Pusher.
052     *
053     * <p>
054     * Note that if you use this constructor you will not be able to subscribe
055     * to private or presence channels because no {@link ChannelAuthorizer} has been
056     * set. If you want to use private or presence channels:
057     * <ul>
058     * <li>Create an implementation of the {@link ChannelAuthorizer} interface, or use
059     * the {@link com.pusher.client.util.HttpChannelAuthorizer} provided.</li>
060     * <li>Create an instance of {@link PusherOptions} and set the authorizer on
061     * it by calling {@link PusherOptions#setChannelAuthorizer(ChannelAuthorizer)}.</li>
062     * <li>Use the {@link #Pusher(String, PusherOptions)} constructor to create
063     * an instance of Pusher.</li>
064     * </ul>
065     *
066     * <p>
067     * The {@link com.pusher.client.example.PrivateChannelExampleApp} and
068     * {@link com.pusher.client.example.PresenceChannelExampleApp} example
069     * applications show how to do this.
070     * </p>
071     *
072     * @param apiKey Your Pusher API key.
073     */
074    public Pusher(final String apiKey) {
075        this(apiKey, new PusherOptions());
076    }
077
078    /**
079     * Creates a new instance of Pusher.
080     *
081     * @param apiKey        Your Pusher API key.
082     * @param pusherOptions Options for the Pusher client library to use.
083     */
084    public Pusher(final String apiKey, final PusherOptions pusherOptions) {
085        this(apiKey, pusherOptions, new Factory());
086    }
087
088    /**
089     * Creates a new Pusher instance using the provided Factory, package level
090     * access for unit tests only.
091     */
092    Pusher(final String apiKey, final PusherOptions pusherOptions, final Factory factory) {
093        if (apiKey == null || apiKey.length() == 0) {
094            throw new IllegalArgumentException("API Key cannot be null or empty");
095        }
096
097        if (pusherOptions == null) {
098            throw new IllegalArgumentException("PusherOptions cannot be null");
099        }
100
101        this.pusherOptions = pusherOptions;
102        this.factory = factory;
103        connection = factory.getConnection(apiKey, this.pusherOptions, this::handleEvent);
104        channelManager = factory.getChannelManager();
105        user = factory.newUser(connection, pusherOptions.getUserAuthenticator());
106        channelManager.setConnection(connection);
107    }
108
109    private void handleEvent(PusherEvent event) {
110        user.handleEvent(event);
111        channelManager.handleEvent(event);
112    }
113
114    /* Connection methods */
115
116    /**
117     * Gets the underlying {@link Connection} object that is being used by this
118     * instance of {@linkplain Pusher}.
119     *
120     * @return The {@link Connection} object.
121     */
122    public Connection getConnection() {
123        return connection;
124    }
125
126    /**
127     * Connects to Pusher. Any {@link ConnectionEventListener}s that have
128     * already been registered using the
129     * {@link Connection#bind(ConnectionState, ConnectionEventListener)} method
130     * will receive connection events.
131     *
132     * <p>Calls are ignored (a connection is not attempted) if the {@link Connection#getState()} is not {@link com.pusher.client.connection.ConnectionState#DISCONNECTED} or  {@link com.pusher.client.connection.ConnectionState#DISCONNECTING}.</p>
133     */
134    public void connect() {
135        connect(null);
136    }
137
138    /**
139     * Binds a {@link ConnectionEventListener} to the specified events and then
140     * connects to Pusher. This is equivalent to binding a
141     * {@link ConnectionEventListener} using the
142     * {@link Connection#bind(ConnectionState, ConnectionEventListener)} method
143     * before connecting.
144     *
145     * <p>Calls are ignored (a connection is not attempted) if the {@link Connection#getState()} is not {@link com.pusher.client.connection.ConnectionState#DISCONNECTED}.</p>
146     *
147     * @param eventListener    A {@link ConnectionEventListener} that will receive connection
148     *                         events. This can be null if you are not interested in
149     *                         receiving connection events, in which case you should call
150     *                         {@link #connect()} instead of this method.
151     * @param connectionStates An optional list of {@link ConnectionState}s to bind your
152     *                         {@link ConnectionEventListener} to before connecting to
153     *                         Pusher. If you do not specify any {@link ConnectionState}s
154     *                         then your {@link ConnectionEventListener} will be bound to all
155     *                         connection events. This is equivalent to calling
156     *                         {@link #connect(ConnectionEventListener, ConnectionState...)}
157     *                         with {@link ConnectionState#ALL}.
158     * @throws IllegalArgumentException If the {@link ConnectionEventListener} is null and at least
159     *                                  one connection state has been specified.
160     */
161    public void connect(final ConnectionEventListener eventListener, ConnectionState... connectionStates) {
162        if (eventListener != null) {
163            if (connectionStates.length == 0) {
164                connectionStates = new ConnectionState[]{ConnectionState.ALL};
165            }
166
167            for (final ConnectionState state : connectionStates) {
168                connection.bind(state, eventListener);
169            }
170        } else {
171            if (connectionStates.length > 0) {
172                throw new IllegalArgumentException("Cannot bind to connection states with a null connection event listener");
173            }
174        }
175
176        connection.connect();
177    }
178
179    /**
180     * Disconnect from Pusher.
181     *
182     * <p>
183     * Calls are ignored if the {@link Connection#getState()}, retrieved from {@link Pusher#getConnection}, is
184     * {@link com.pusher.client.connection.ConnectionState#DISCONNECTING} or  {@link com.pusher.client.connection.ConnectionState#DISCONNECTED}.
185     * </p>
186     */
187    public void disconnect() {
188        if (connection.getState() != ConnectionState.DISCONNECTING && connection.getState() != ConnectionState.DISCONNECTED) {
189            connection.disconnect();
190        }
191    }
192
193    /**
194     * @return The {@link com.pusher.client.user.User} associated with this Pusher connection.
195     */
196    public User user() {
197        return user;
198    }
199
200    /**
201     * Signs in on the Pusher connection as the current user.
202     *
203     * <p>
204     * Requires {@link PusherOptions#setUserAuthenticator} to have been called.
205     * </p>
206     *
207     * @throws IllegalStateException if no {@link UserAuthenticator} has been set.
208     */
209    public void signin() {
210        throwExceptionIfNoUserAuthenticatorHasBeenSet();
211        user.signin();
212    }
213
214    /* Subscription methods */
215
216    /**
217     * Subscribes to a public {@link Channel}.
218     * <p>
219     * Note that subscriptions should be registered only once with a Pusher
220     * instance. Subscriptions are persisted over disconnection and
221     * re-registered with the server automatically on reconnection. This means
222     * that subscriptions may also be registered before connect() is called,
223     * they will be initiated on connection.
224     *
225     * @param channelName The name of the {@link Channel} to subscribe to.
226     * @return The {@link Channel} object representing your subscription.
227     */
228    public Channel subscribe(final String channelName) {
229        return subscribe(channelName, null);
230    }
231
232    /**
233     * Binds a {@link ChannelEventListener} to the specified events and then
234     * subscribes to a public {@link Channel}.
235     *
236     * @param channelName The name of the {@link Channel} to subscribe to.
237     * @param listener    A {@link ChannelEventListener} to receive events. This can be
238     *                    null if you don't want to bind a listener at subscription
239     *                    time, in which case you should call {@link #subscribe(String)}
240     *                    instead of this method.
241     * @param eventNames  An optional list of event names to bind your
242     *                    {@link ChannelEventListener} to before subscribing.
243     * @return The {@link Channel} object representing your subscription.
244     * @throws IllegalArgumentException If any of the following are true:
245     *                                  <ul>
246     *                                  <li>The channel name is null.</li>
247     *                                  <li>You are already subscribed to this channel.</li>
248     *                                  <li>The channel name starts with "private-". If you want to
249     *                                  subscribe to a private channel, call
250     *                                  {@link #subscribePrivate(String, PrivateChannelEventListener, String...)}
251     *                                  instead of this method.</li>
252     *                                  <li>At least one of the specified event names is null.</li>
253     *                                  <li>You have specified at least one event name and your
254     *                                  {@link ChannelEventListener} is null.</li>
255     *                                  </ul>
256     */
257    public Channel subscribe(final String channelName, final ChannelEventListener listener, final String... eventNames) {
258        final InternalChannel channel = factory.newPublicChannel(channelName);
259        channelManager.subscribeTo(channel, listener, eventNames);
260
261        return channel;
262    }
263
264    /**
265     * Subscribes to a {@link com.pusher.client.channel.PrivateChannel} which
266     * requires authentication.
267     *
268     * @param channelName The name of the channel to subscribe to.
269     * @return A new {@link com.pusher.client.channel.PrivateChannel}
270     * representing the subscription.
271     * @throws IllegalStateException if a {@link com.pusher.client.ChannelAuthorizer} has not been set
272     *                               for the {@link Pusher} instance via
273     *                               {@link #Pusher(String, PusherOptions)}.
274     */
275    public PrivateChannel subscribePrivate(final String channelName) {
276        return subscribePrivate(channelName, null);
277    }
278
279    /**
280     * Subscribes to a {@link com.pusher.client.channel.PrivateChannel} which
281     * requires authentication.
282     *
283     * @param channelName The name of the channel to subscribe to.
284     * @param listener    A listener to be informed of both Pusher channel protocol events and subscription data events.
285     * @param eventNames  An optional list of names of events to be bound to on the channel. The equivalent of calling {@link com.pusher.client.channel.Channel#bind(String, SubscriptionEventListener)} one or more times.
286     * @return A new {@link com.pusher.client.channel.PrivateChannel} representing the subscription.
287     * @throws IllegalStateException if a {@link com.pusher.client.ChannelAuthorizer} has not been set for the {@link Pusher} instance via {@link #Pusher(String, PusherOptions)}.
288     */
289    public PrivateChannel subscribePrivate(
290            final String channelName,
291            final PrivateChannelEventListener listener,
292            final String... eventNames
293    ) {
294        throwExceptionIfNoChannelAuthorizerHasBeenSet();
295
296        final PrivateChannelImpl channel = factory.newPrivateChannel(
297                connection,
298                channelName,
299                pusherOptions.getChannelAuthorizer()
300        );
301        channelManager.subscribeTo(channel, listener, eventNames);
302
303        return channel;
304    }
305
306    /**
307     * Subscribes to a {@link com.pusher.client.channel.PrivateEncryptedChannel} which
308     * requires authentication.
309     *
310     * @param channelName The name of the channel to subscribe to.
311     * @param listener    A listener to be informed of both Pusher channel protocol events and
312     *                    subscription data events.
313     * @param eventNames  An optional list of names of events to be bound to on the channel.
314     *                    The equivalent of calling
315     *                    {@link com.pusher.client.channel.Channel#bind(String, SubscriptionEventListener)}
316     *                    one or more times.
317     * @return A new {@link com.pusher.client.channel.PrivateEncryptedChannel} representing
318     * the subscription.
319     * @throws IllegalStateException if a {@link com.pusher.client.ChannelAuthorizer} has not been set for
320     *                               the {@link Pusher} instance via {@link #Pusher(String, PusherOptions)}.
321     */
322    public PrivateEncryptedChannel subscribePrivateEncrypted(
323            final String channelName,
324            final PrivateEncryptedChannelEventListener listener,
325            final String... eventNames
326    ) {
327        throwExceptionIfNoChannelAuthorizerHasBeenSet();
328
329        final PrivateEncryptedChannelImpl channel = factory.newPrivateEncryptedChannel(
330                connection,
331                channelName,
332                pusherOptions.getChannelAuthorizer()
333        );
334        channelManager.subscribeTo(channel, listener, eventNames);
335
336        return channel;
337    }
338
339    /**
340     * Subscribes to a {@link com.pusher.client.channel.PresenceChannel} which
341     * requires authentication.
342     *
343     * @param channelName The name of the channel to subscribe to.
344     * @return A new {@link com.pusher.client.channel.PresenceChannel}
345     * representing the subscription.
346     * @throws IllegalStateException if a {@link com.pusher.client.ChannelAuthorizer} has not been set
347     *                               for the {@link Pusher} instance via
348     *                               {@link #Pusher(String, PusherOptions)}.
349     */
350    public PresenceChannel subscribePresence(final String channelName) {
351        return subscribePresence(channelName, null);
352    }
353
354    /**
355     * Subscribes to a {@link com.pusher.client.channel.PresenceChannel} which
356     * requires authentication.
357     *
358     * @param channelName The name of the channel to subscribe to.
359     * @param listener    A listener to be informed of Pusher channel protocol, including presence-specific events, and subscription data events.
360     * @param eventNames  An optional list of names of events to be bound to on the channel. The equivalent of calling {@link com.pusher.client.channel.Channel#bind(String, SubscriptionEventListener)} one or more times.
361     * @return A new {@link com.pusher.client.channel.PresenceChannel} representing the subscription.
362     * @throws IllegalStateException if a {@link com.pusher.client.ChannelAuthorizer} has not been set for the {@link Pusher} instance via {@link #Pusher(String, PusherOptions)}.
363     */
364    public PresenceChannel subscribePresence(
365            final String channelName,
366            final PresenceChannelEventListener listener,
367            final String... eventNames
368    ) {
369        throwExceptionIfNoChannelAuthorizerHasBeenSet();
370
371        final PresenceChannelImpl channel = factory.newPresenceChannel(
372                connection,
373                channelName,
374                pusherOptions.getChannelAuthorizer()
375        );
376        channelManager.subscribeTo(channel, listener, eventNames);
377
378        return channel;
379    }
380
381    /**
382     * Unsubscribes from a channel using via the name of the channel.
383     *
384     * @param channelName the name of the channel to be unsubscribed from.
385     */
386    public void unsubscribe(final String channelName) {
387        channelManager.unsubscribeFrom(channelName);
388    }
389
390    /* implementation detail */
391
392    private void throwExceptionIfNoChannelAuthorizerHasBeenSet() {
393        if (pusherOptions.getChannelAuthorizer() == null) {
394            throw new IllegalStateException(
395                    "Cannot subscribe to a private or presence channel because no ChannelAuthorizer has been set. Call PusherOptions.setChannelAuthorizer() before connecting to Pusher"
396            );
397        }
398    }
399
400    private void throwExceptionIfNoUserAuthenticatorHasBeenSet() {
401        if (pusherOptions.getUserAuthenticator() == null) {
402            throw new IllegalStateException(
403                    "Cannot sign in because no UserAuthenticator has been set. Call PusherOptions.setUserAuthenticator() before connecting to Pusher"
404            );
405        }
406    }
407
408    /**
409     * @param channelName The name of the public channel to be retrieved
410     * @return A public channel, or null if it could not be found
411     * @throws IllegalArgumentException if you try to retrieve a private or presence channel.
412     */
413    public Channel getChannel(String channelName) {
414        return channelManager.getChannel(channelName);
415    }
416
417    /**
418     * @param channelName The name of the private channel to be retrieved
419     * @return A private channel, or null if it could not be found
420     * @throws IllegalArgumentException if you try to retrieve a public or presence channel.
421     */
422    public PrivateChannel getPrivateChannel(String channelName) {
423        return channelManager.getPrivateChannel(channelName);
424    }
425
426    /**
427     * @param channelName The name of the private encrypted channel to be retrieved
428     * @return A private encrypted channel, or null if it could not be found
429     * @throws IllegalArgumentException if you try to retrieve a public or presence channel.
430     */
431    public PrivateEncryptedChannel getPrivateEncryptedChannel(String channelName) {
432        return channelManager.getPrivateEncryptedChannel(channelName);
433    }
434
435    /**
436     * @param channelName The name of the presence channel to be retrieved
437     * @return A presence channel, or null if it could not be found
438     * @throws IllegalArgumentException if you try to retrieve a public or private channel.
439     */
440    public PresenceChannel getPresenceChannel(String channelName) {
441        return channelManager.getPresenceChannel(channelName);
442    }
443}