001package com.pusher.client;
002
003import java.io.IOException;
004import java.io.InputStream;
005import java.net.Proxy;
006import java.util.Properties;
007
008/**
009 * Configuration for a {@link com.pusher.client.Pusher} instance.
010 */
011public class PusherOptions {
012
013    private static final String SRC_LIB_DEV_VERSION = "@version@";
014    private static final String LIB_DEV_VERSION = "0.0.0-dev";
015        public static final String LIB_VERSION = readVersionFromProperties();
016
017    private static final String URI_SUFFIX = "?client=java-client&protocol=5&version=" + LIB_VERSION;
018    private static final String WS_SCHEME = "ws";
019    private static final String WSS_SCHEME = "wss";
020
021    private static final int WS_PORT = 80;
022    private static final int WSS_PORT = 443;
023    private static final String PUSHER_DOMAIN = "pusher.com";
024
025    private static final long DEFAULT_ACTIVITY_TIMEOUT = 120000;
026    private static final long DEFAULT_PONG_TIMEOUT = 30000;
027
028    private static final int MAX_RECONNECTION_ATTEMPTS = 6; //Taken from the Swift lib
029    private static final int MAX_RECONNECT_GAP_IN_SECONDS = 30;
030
031    // Note that the primary cluster lives on a different domain
032    // (others are subdomains of pusher.com). This is not an oversight.
033    // Legacy reasons.
034    private String host = "ws.pusherapp.com";
035    private int wsPort = WS_PORT;
036    private int wssPort = WSS_PORT;
037    private boolean encrypted = true;
038    private long activityTimeout = DEFAULT_ACTIVITY_TIMEOUT;
039    private long pongTimeout = DEFAULT_PONG_TIMEOUT;
040    private Authorizer authorizer;
041    private Proxy proxy = Proxy.NO_PROXY;
042    private int maxReconnectionAttempts = MAX_RECONNECTION_ATTEMPTS;
043    private int maxReconnectGapInSeconds = MAX_RECONNECT_GAP_IN_SECONDS;
044
045    /**
046     * Gets whether an encrypted (SSL) connection should be used when connecting
047     * to Pusher.
048     *
049     * @return true if an encrypted connection should be used; otherwise false.
050     */
051    public boolean isEncrypted() {
052        return encrypted;
053    }
054
055    /**
056     * Sets whether an encrypted (SSL) connection should be used when connecting to
057     * Pusher.
058     *
059     * @param encrypted Whether to use an SSL connection
060     * @return this, for chaining
061     */
062    public PusherOptions setEncrypted(final boolean encrypted) {
063        this.encrypted = encrypted;
064        return this;
065    }
066
067    /**
068     * Gets the authorizer to be used when authenticating private and presence
069     * channels.
070     *
071     * @return the authorizer
072     */
073    public Authorizer getAuthorizer() {
074        return authorizer;
075    }
076
077    /**
078     * Sets the authorizer to be used when authenticating private and presence
079     * channels.
080     *
081     * @param authorizer
082     *            The authorizer to be used.
083     * @return this, for chaining
084     */
085    public PusherOptions setAuthorizer(final Authorizer authorizer) {
086        this.authorizer = authorizer;
087        return this;
088    }
089
090    /**
091     * The host to which connections will be made.
092     *
093     * Note that if you wish to connect to a standard Pusher cluster, the
094     * convenience method setCluster will set the host and ports correctly from
095     * a single argument.
096     *
097     * @param host The host
098     * @return this, for chaining
099     */
100    public PusherOptions setHost(final String host) {
101        this.host = host;
102        return this;
103    }
104
105    /**
106     * The port to which unencrypted connections will be made.
107     *
108     * Note that if you wish to connect to a standard Pusher cluster, the
109     * convenience method setCluster will set the host and ports correctly from
110     * a single argument.
111     *
112     * @param wsPort port number
113     * @return this, for chaining
114     */
115    public PusherOptions setWsPort(final int wsPort) {
116        this.wsPort = wsPort;
117        return this;
118    }
119
120    /**
121     * The port to which encrypted connections will be made.
122     *
123     * Note that if you wish to connect to a standard Pusher cluster, the
124     * convenience method setCluster will set the host and ports correctly from
125     * a single argument.
126     *
127     * @param wssPort port number
128     * @return this, for chaining
129     */
130    public PusherOptions setWssPort(final int wssPort) {
131        this.wssPort = wssPort;
132        return this;
133    }
134
135    public PusherOptions setCluster(final String cluster) {
136        host = "ws-" + cluster + "." + PUSHER_DOMAIN;
137        wsPort = WS_PORT;
138        wssPort = WSS_PORT;
139        return this;
140    }
141
142    /**
143     * The number of milliseconds of inactivity at which a "ping" will be
144     * triggered to check the connection.
145     *
146     * The default value is 120,000 (2 minutes). On some connections, where
147     * intermediate hops between the application and Pusher are aggressively
148     * culling connections they consider to be idle, a lower value may help
149     * preserve the connection.
150     *
151     * @param activityTimeout
152     *            time to consider connection idle, in milliseconds
153     * @return this, for chaining
154     */
155    public PusherOptions setActivityTimeout(final long activityTimeout) {
156        if (activityTimeout < 1000) {
157            throw new IllegalArgumentException(
158                    "Activity timeout must be at least 1,000ms (and is recommended to be much higher)");
159        }
160
161        this.activityTimeout = activityTimeout;
162        return this;
163    }
164
165    public long getActivityTimeout() {
166        return activityTimeout;
167    }
168
169    /**
170     * The number of milliseconds after a "ping" is sent that the client will
171     * wait to receive a "pong" response from the server before considering the
172     * connection broken and triggering a transition to the disconnected state.
173     *
174     * The default value is 30,000.
175     *
176     * @param pongTimeout
177     *            time to wait for pong response, in milliseconds
178     * @return this, for chaining
179     */
180    public PusherOptions setPongTimeout(final long pongTimeout) {
181        if (pongTimeout < 1000) {
182            throw new IllegalArgumentException(
183                    "Pong timeout must be at least 1,000ms (and is recommended to be much higher)");
184        }
185
186        this.pongTimeout = pongTimeout;
187        return this;
188    }
189
190    /**
191     * Number of reconnect attempts when websocket connection failed
192     * @param maxReconnectionAttempts
193         *                              number of max reconnection attempts, default = {@link #MAX_RECONNECTION_ATTEMPTS} 6
194     * @return this, for chaining
195     */
196    public PusherOptions setMaxReconnectionAttempts(int maxReconnectionAttempts) {
197        this.maxReconnectionAttempts = maxReconnectionAttempts;
198        return this;
199    }
200
201        /**
202         * The delay in two reconnection extends exponentially (1, 2, 4, .. seconds) This property sets the maximum in between two
203         * reconnection attempts.
204         * @param maxReconnectGapInSeconds
205         *                              time in seconds of the maximum gab between two reconnection attempts, default = {@link #MAX_RECONNECT_GAP_IN_SECONDS} 30s
206         * @return this, for chaining
207         */
208        public PusherOptions setMaxReconnectGapInSeconds(int maxReconnectGapInSeconds) {
209                this.maxReconnectGapInSeconds = maxReconnectGapInSeconds;
210                return this;
211        }
212
213        public long getPongTimeout() {
214        return pongTimeout;
215    }
216
217    /**
218     * Construct the URL for the WebSocket connection based on the options
219     * previous set on this object and the provided API key
220     *
221     * @param apiKey The API key
222     * @return the WebSocket URL
223     */
224    public String buildUrl(final String apiKey) {
225        return String.format("%s://%s:%s/app/%s%s", encrypted ? WSS_SCHEME : WS_SCHEME, host, encrypted ? wssPort
226                : wsPort, apiKey, URI_SUFFIX);
227    }
228
229    /**
230     *
231     * The default value is Proxy.NO_PROXY.
232     *
233     * @param proxy
234     *            Specify a proxy, e.g. <code>options.setProxy( new Proxy( Proxy.Type.HTTP, new InetSocketAddress( "proxyaddress", 80 ) ) )</code>;
235     * @return this, for chaining
236     */
237    public PusherOptions setProxy(Proxy proxy){
238        if (proxy == null) {
239          throw new IllegalArgumentException("proxy must not be null (instead use Proxy.NO_PROXY)");
240        }
241        this.proxy = proxy;
242        return this;
243    }
244
245    /**
246     * @return The proxy to be used when opening a websocket connection to Pusher.
247     */
248    public Proxy getProxy() {
249        return this.proxy;
250    }
251
252        /**
253         * @return the maximum reconnection attempts
254         */
255        public int getMaxReconnectionAttempts() {
256                return maxReconnectionAttempts;
257        }
258
259        /**
260         * @return the maximum reconnection gap in seconds
261         */
262        public int getMaxReconnectGapInSeconds() {
263                return maxReconnectGapInSeconds;
264        }
265
266        private static String readVersionFromProperties() {
267        InputStream inStream = null;
268        try {
269            final Properties p = new Properties();
270            inStream = PusherOptions.class.getResourceAsStream("/pusher.properties");
271            p.load(inStream);
272            String version = (String)p.get("version");
273
274            // If the properties file contents indicates the version is being run
275            // from source then replace with a dev indicator. Otherwise the Pusher
276            // Socket API will reject the connection.
277            if(version.equals(SRC_LIB_DEV_VERSION)) {
278                version = LIB_DEV_VERSION;
279            }
280
281            if (version != null && version.length() > 0) {
282                return version;
283            }
284        }
285        catch (final Exception e) {
286            // Fall back to fixed value
287        }
288        finally {
289            try {
290                if (inStream != null) {
291                    inStream.close();
292                }
293            }
294            catch (final IOException e) {
295                // Ignore problem closing stream
296            }
297        }
298        return "0.0.0";
299    }
300}