diff --git a/src/main/java/io/socket/client/IO.java b/src/main/java/io/socket/client/IO.java index 06912a04..99052528 100644 --- a/src/main/java/io/socket/client/IO.java +++ b/src/main/java/io/socket/client/IO.java @@ -94,5 +94,22 @@ public static class Options extends Manager.Options { * Whether to enable multiplexing. Default is true. */ public boolean multiplex = true; + + /** + *

+ * Retrieve new builder class that helps creating socket option as builder pattern. + * This method returns exactly same result as : + *

+ * + * SocketOptionBuilder builder = SocketOptionBuilder.builder(); + * + * + * @return builder class that helps creating socket option as builder pattern. + * @see SocketOptionBuilder#builder() + * @see SocketOptionBuilder#builder(Options) + */ + public static SocketOptionBuilder builder() { + return SocketOptionBuilder.builder(); + } } } diff --git a/src/main/java/io/socket/client/SocketOptionBuilder.java b/src/main/java/io/socket/client/SocketOptionBuilder.java new file mode 100644 index 00000000..91afbb80 --- /dev/null +++ b/src/main/java/io/socket/client/SocketOptionBuilder.java @@ -0,0 +1,208 @@ +package io.socket.client; + +import javax.net.ssl.HostnameVerifier; + + +/** + * Convenient builder class that helps creating + * {@link io.socket.client.IO.Options Client Option} object as builder pattern. + * Finally, you can get option object with call {@link #build()} method. + * + * @author junbong + */ +public class SocketOptionBuilder { + /** + * Construct new builder with default preferences. + * + * @return new builder object + * @see SocketOptionBuilder#builder(IO.Options) + */ + public static SocketOptionBuilder builder() { + return new SocketOptionBuilder(); + } + + + /** + * Construct this builder from specified option object. + * The option that returned from {@link #build()} method + * is not equals with given option. + * In other words, builder creates new option object + * and copy all preferences from given option. + * + * @param options option object which to copy preferences + * @return new builder object + */ + public static SocketOptionBuilder builder(IO.Options options) { + return new SocketOptionBuilder(options); + } + + + private final IO.Options options = new IO.Options(); + + + /** + * Construct new builder with default preferences. + */ + protected SocketOptionBuilder() { + this(null); + } + + + /** + * Construct this builder from specified option object. + * The option that returned from {@link #build()} method + * is not equals with given option. + * In other words, builder creates new option object + * and copy all preferences from given option. + * + * @param options option object which to copy preferences. Null-ok. + */ + protected SocketOptionBuilder(IO.Options options) { + if (options != null) { + this.setForceNew(options.forceNew) + .setMultiplex(options.multiplex) + .setReconnection(options.reconnection) + .setReconnectionAttempts(options.reconnectionAttempts) + .setReconnectionDelay(options.reconnectionDelay) + .setReconnectionDelayMax(options.reconnectionDelayMax) + .setRandomizationFactor(options.randomizationFactor) + .setTimeout(options.timeout) + .setTransports(options.transports) + .setUpgrade(options.upgrade) + .setRememberUpgrade(options.rememberUpgrade) + .setHost(options.host) + .setHostname(options.hostname) + .setHostnameVerifier(options.hostnameVerifier) + .setPort(options.port) + .setPolicyPort(options.policyPort) + .setSecure(options.secure) + .setPath(options.path) + .setQuery(options.query); + } + } + + + public SocketOptionBuilder setForceNew(boolean forceNew) { + this.options.forceNew = forceNew; + return this; + } + + + public SocketOptionBuilder setMultiplex(boolean multiplex) { + this.options.multiplex = multiplex; + return this; + } + + + public SocketOptionBuilder setReconnection(boolean reconnection) { + this.options.reconnection = reconnection; + return this; + } + + + public SocketOptionBuilder setReconnectionAttempts(int reconnectionAttempts) { + this.options.reconnectionAttempts = reconnectionAttempts; + return this; + } + + + public SocketOptionBuilder setReconnectionDelay(long reconnectionDelay) { + this.options.reconnectionDelay = reconnectionDelay; + return this; + } + + + public SocketOptionBuilder setReconnectionDelayMax(long reconnectionDelayMax) { + this.options.reconnectionDelayMax = reconnectionDelayMax; + return this; + } + + + public SocketOptionBuilder setRandomizationFactor(double randomizationFactor) { + this.options.randomizationFactor = randomizationFactor; + return this; + } + + + public SocketOptionBuilder setTimeout(long timeout) { + this.options.timeout = timeout; + return this; + } + + + public SocketOptionBuilder setTransports(String[] transports) { + this.options.transports = transports; + return this; + } + + + public SocketOptionBuilder setUpgrade(boolean upgrade) { + this.options.upgrade = upgrade; + return this; + } + + + public SocketOptionBuilder setRememberUpgrade(boolean rememberUpgrade) { + this.options.rememberUpgrade = rememberUpgrade; + return this; + } + + + public SocketOptionBuilder setHost(String host) { + this.options.host = host; + return this; + } + + + public SocketOptionBuilder setHostname(String hostname) { + this.options.hostname = hostname; + return this; + } + + + public SocketOptionBuilder setHostnameVerifier(HostnameVerifier hostnameVerifier) { + this.options.hostnameVerifier = hostnameVerifier; + return this; + } + + + public SocketOptionBuilder setPort(int port) { + this.options.port = port; + return this; + } + + + public SocketOptionBuilder setPolicyPort(int policyPort) { + this.options.policyPort = policyPort; + return this; + } + + + public SocketOptionBuilder setQuery(String query) { + this.options.query = query; + return this; + } + + + public SocketOptionBuilder setSecure(boolean secure) { + this.options.secure = secure; + return this; + } + + + public SocketOptionBuilder setPath(String path) { + this.options.path = path; + return this; + } + + + /** + * Finally retrieve {@link io.socket.client.IO.Options} object + * from this builder. + * + * @return option that built from this builder + */ + public IO.Options build() { + return this.options; + } +} diff --git a/src/test/java/io/socket/client/SocketOptionBuilderTest.java b/src/test/java/io/socket/client/SocketOptionBuilderTest.java new file mode 100644 index 00000000..4fce87d7 --- /dev/null +++ b/src/test/java/io/socket/client/SocketOptionBuilderTest.java @@ -0,0 +1,337 @@ +package io.socket.client; + +import io.socket.emitter.Emitter; +import org.junit.Test; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + + +/** + * This tests are exactly same as {@link ConnectionTest} one + * except creating IO.Options. + * + * @author junbong + */ +public class SocketOptionBuilderTest extends ConnectionTest { + private Socket socket; + + + @Override + @Test(timeout = TIMEOUT) + public void attemptReconnectsAfterAFailedReconnect() throws URISyntaxException, InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue<>(); + + final IO.Options opts = SocketOptionBuilder.builder() + .setForceNew(true) + .setReconnection(true) + .setTimeout(0) + .setReconnectionAttempts(2) + .setReconnectionDelay(10) + .build(); + + final Manager manager = new Manager(new URI(uri()), opts); + socket = manager.socket("/timeout"); + socket.once(Socket.EVENT_RECONNECT_FAILED, new Emitter.Listener() { + @Override + public void call(Object... args) { + final int[] reconnects = new int[]{0}; + Emitter.Listener reconnectCb = new Emitter.Listener() { + @Override + public void call(Object... args) { + reconnects[0]++; + } + }; + + manager.on(Manager.EVENT_RECONNECT_ATTEMPT, reconnectCb); + manager.on(Manager.EVENT_RECONNECT_FAILED, new Emitter.Listener() { + @Override + public void call(Object... args) { + values.offer(reconnects[0]); + } + }); + socket.connect(); + } + }); + socket.connect(); + assertThat((Integer) values.take(), is(2)); + socket.close(); + manager.close(); + } + + + @Override + @Test(timeout = TIMEOUT) + public void reconnectDelayShouldIncreaseEveryTime() throws URISyntaxException, InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue(); + IO.Options opts = SocketOptionBuilder.builder() + .setForceNew(true) + .setReconnection(true) + .setTimeout(0) + .setReconnectionAttempts(3) + .setReconnectionDelay(100) + .setRandomizationFactor(0.2) + .build(); + + final Manager manager = new Manager(new URI(uri()), opts); + socket = manager.socket("/timeout"); + + final int[] reconnects = new int[] {0}; + final boolean[] increasingDelay = new boolean[] {true}; + final long[] startTime = new long[] {0}; + final long[] prevDelay = new long[] {0}; + + socket.on(Socket.EVENT_CONNECT_ERROR, new Emitter.Listener() { + @Override + public void call(Object... args) { + startTime[0] = new Date().getTime(); + } + }); + socket.on(Socket.EVENT_RECONNECT_ATTEMPT, new Emitter.Listener() { + @Override + public void call(Object... args) { + reconnects[0]++; + long currentTime = new Date().getTime(); + long delay = currentTime - startTime[0]; + if (delay <= prevDelay[0]) { + increasingDelay[0] = false; + } + prevDelay[0] = delay; + } + }); + socket.on(Socket.EVENT_RECONNECT_FAILED, new Emitter.Listener() { + @Override + public void call(Object... args) { + values.offer(true); + } + }); + + socket.connect(); + values.take(); + assertThat(reconnects[0], is(3)); + assertThat(increasingDelay[0], is(true)); + socket.close(); + manager.close(); + } + + + @Override + @Test(timeout = TIMEOUT) + public void notReconnectWhenForceClosed() throws URISyntaxException, InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue(); + IO.Options opts = SocketOptionBuilder.builder() + .setForceNew(true) + .setTimeout(0) + .setReconnectionDelay(10) + .build(); + + socket = IO.socket(uri() + "/invalid", opts); + socket.on(Socket.EVENT_CONNECT_ERROR, new Emitter.Listener() { + @Override + public void call(Object... args) { + socket.on(Socket.EVENT_RECONNECT_ATTEMPT, new Emitter.Listener() { + @Override + public void call(Object... args) { + values.offer(false); + } + }); + socket.disconnect(); + new Timer().schedule(new TimerTask() { + @Override + public void run() { + values.offer(true); + } + }, 500); + } + }); + socket.connect(); + assertThat((Boolean)values.take(), is(true)); + } + + + @Override + @Test(timeout = TIMEOUT) + public void stopReconnectingWhenForceClosed() throws URISyntaxException, InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue(); + IO.Options opts = SocketOptionBuilder.builder() + .setForceNew(true) + .setTimeout(0) + .setReconnectionDelay(10) + .build(); + + socket = IO.socket(uri() + "/invalid", opts); + socket.once(Socket.EVENT_RECONNECT_ATTEMPT, new Emitter.Listener() { + @Override + public void call(Object... args) { + socket.on(Socket.EVENT_RECONNECT_ATTEMPT, new Emitter.Listener() { + @Override + public void call(Object... args) { + values.offer(false); + } + }); + socket.disconnect(); + // set a timer to let reconnection possibly fire + new Timer().schedule(new TimerTask() { + @Override + public void run() { + values.offer(true); + } + }, 500); + } + }); + socket.connect(); + assertThat((Boolean) values.take(), is(true)); + } + + + @Override + @Test(timeout = TIMEOUT) + public void reconnectAfterStoppingReconnection() throws URISyntaxException, InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue(); + IO.Options opts = SocketOptionBuilder.builder() + .setForceNew(true) + .setTimeout(0) + .setReconnectionDelay(10) + .build(); + + socket = client("/invalid", opts); + socket.once(Socket.EVENT_RECONNECT_ATTEMPT, new Emitter.Listener() { + @Override + public void call(Object... args) { + socket.once(Socket.EVENT_RECONNECT_ATTEMPT, new Emitter.Listener() { + @Override + public void call(Object... args) { + values.offer("done"); + } + }); + socket.disconnect(); + socket.connect(); + } + }); + socket.connect(); + values.take(); + socket.disconnect(); + } + + + @Override + @Test(timeout = TIMEOUT) + public void tryToReconnectTwiceAndFailWithIncorrectAddress() throws URISyntaxException, InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue(); + IO.Options opts = SocketOptionBuilder.builder() + .setForceNew(true) + .setReconnection(true) + .setReconnectionAttempts(2) + .setReconnectionDelay(10) + .build(); + + final Manager manager = new Manager(new URI("http://localhost:3940"), opts); + socket = manager.socket("/asd"); + final int[] reconnects = new int[] {0}; + Emitter.Listener cb = new Emitter.Listener() { + @Override + public void call(Object... objects) { + reconnects[0]++; + } + }; + + manager.on(Manager.EVENT_RECONNECT_ATTEMPT, cb); + + manager.on(Manager.EVENT_RECONNECT_FAILED, new Emitter.Listener() { + @Override + public void call(Object... objects) { + values.offer(reconnects[0]); + } + }); + + socket.open(); + assertThat((Integer)values.take(), is(2)); + socket.close(); + manager.close(); + } + + + @Override + @Test(timeout = TIMEOUT) + public void tryToReconnectTwiceAndFailWithImmediateTimeout() throws URISyntaxException, InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue(); + IO.Options opts = SocketOptionBuilder.builder() + .setForceNew(true) + .setTimeout(0) + .setReconnection(true) + .setReconnectionAttempts(2) + .setReconnectionDelay(10) + .build(); + + final Manager manager = new Manager(new URI(uri()), opts); + + final int[] reconnects = new int[] {0}; + Emitter.Listener reconnectCb = new Emitter.Listener() { + @Override + public void call(Object... objects) { + reconnects[0]++; + } + }; + + manager.on(Manager.EVENT_RECONNECT_ATTEMPT, reconnectCb); + manager.on(Manager.EVENT_RECONNECT_FAILED, new Emitter.Listener() { + @Override + public void call(Object... objects) { + socket.close(); + manager.close(); + values.offer(reconnects[0]); + } + }); + + socket = manager.socket("/timeout"); + socket.open(); + assertThat((Integer)values.take(), is(2)); + } + + + @Override + @Test(timeout = TIMEOUT) + public void notTryToReconnectWithIncorrectPortWhenReconnectionDisabled() throws URISyntaxException, InterruptedException { + final BlockingQueue values = new LinkedBlockingQueue(); + IO.Options opts = SocketOptionBuilder.builder() + .setForceNew(true) + .setReconnection(false) + .build(); + + final Manager manager = new Manager(new URI("http://localhost:9823"), opts); + Emitter.Listener cb = new Emitter.Listener() { + @Override + public void call(Object... objects) { + socket.close(); + throw new RuntimeException(); + } + }; + manager.on(Manager.EVENT_RECONNECT_ATTEMPT, cb); + manager.on(Manager.EVENT_CONNECT_ERROR, new Emitter.Listener() { + @Override + public void call(Object... objects) { + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + socket.close(); + manager.close(); + values.offer("done"); + } + }, 1000); + } + }); + + socket = manager.socket("/invalid"); + socket.open(); + values.take(); + } +}