├── .gitignore
├── README.md
├── pom.xml
└── src
├── main
├── java
│ └── org
│ │ └── jfarcand
│ │ └── wcs
│ │ └── Dummy.java
└── scala
│ └── org
│ └── jfarcand
│ └── wcs
│ ├── BinaryListener.scala
│ ├── MessageListener.scala
│ ├── Options.scala
│ ├── TextListener.scala
│ ├── WebSocketException.scala
│ └── Websocket.scala
└── test
└── scala
└── org
└── jfarcand
└── wcs
└── test
├── BaseTest.scala
├── WSSTest.scala
└── WebSocketTest.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | project/project
3 | project/target
4 | target
5 | tmp
6 | .history
7 | dist
8 | /.idea
9 | /*.iml
10 | META-INF
11 | /out
12 | /.idea_modules
13 | /.classpath
14 | /.project
15 | /RUNNING_PID
16 | /.settings
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### wCS: An Asynchronous WebSocket Client Library for Scala.
2 |
3 | A really simple WebSocket library that works with [node.js](http://nodejs.org/), [Atmosphere](https://github.com/Atmosphere/atmosphere) or any WebSocket server! As simply as
4 |
5 | ```java
6 | WebSocket().open("ws://localhost")
7 | .listener(new TextListener {
8 | override def onMessage(message: String) {
9 | // Do something
10 | }
11 | })
12 | .send("Hello World")
13 | .send("WebSockets are cool!")
14 | .listener(new BinaryListener {
15 | override def onMessage(message: Array[Byte]) {
16 | // Do something
17 | }
18 | })
19 | .send("Hello World".getBytes)
20 | ```
21 |
22 | Download using Maven
23 |
24 | ```xml
25 |
Websocket().open("ws://localhost".send("Hello").listener(new MyListener() {...}).close28 | */ 29 | object WebSocket { 30 | val listeners: ListBuffer[WebSocketListener] = ListBuffer[WebSocketListener]() 31 | var nettyConfig: NettyAsyncHttpProviderConfig = new NettyAsyncHttpProviderConfig 32 | var config: AsyncHttpClientConfig.Builder = new AsyncHttpClientConfig.Builder 33 | var asyncHttpClient: AsyncHttpClient = null 34 | 35 | def apply(o: Options): WebSocket = { 36 | if (o != null) { 37 | nettyConfig.setWebSocketMaxBufferSize(o.maxMessageSize) 38 | nettyConfig.setWebSocketMaxFrameSize(o.maxMessageSize) 39 | config.setRequestTimeout(o.idleTimeout).setUserAgent(o.userAgent).setAsyncHttpClientProviderConfig(nettyConfig) 40 | } 41 | 42 | try { 43 | config.setFollowRedirect(true) 44 | asyncHttpClient = new AsyncHttpClient(config.build) 45 | } catch { 46 | case t: IllegalStateException => { 47 | config = new AsyncHttpClientConfig.Builder 48 | } 49 | } 50 | new WebSocket(o, None, false, asyncHttpClient, listeners) 51 | } 52 | 53 | def apply(): WebSocket = { 54 | apply(new Options) 55 | } 56 | } 57 | 58 | case class WebSocket(o: Options, 59 | webSocket: Option[com.ning.http.client.ws.WebSocket], 60 | isOpen: Boolean, 61 | asyncHttpClient: AsyncHttpClient, 62 | listeners: ListBuffer[WebSocketListener]) { 63 | 64 | val logger: Logger = LoggerFactory.getLogger(classOf[WebSocket]) 65 | 66 | /** 67 | * Open a WebSocket connection. 68 | */ 69 | def open(s: String): WebSocket = { 70 | 71 | if (!s.startsWith("ws://") && !s.startsWith("wss://")) throw new RuntimeException("Invalid Protocol. Only WebSocket ws:// or wss:// supported" + s) 72 | 73 | val b = new WebSocketUpgradeHandler.Builder 74 | 75 | listeners.foreach(l => { 76 | b.addWebSocketListener(l) 77 | }) 78 | 79 | listeners.clear 80 | 81 | logger.trace("Opening to {}", s) 82 | new WebSocket(o, Some(asyncHttpClient.prepareGet(s).execute(b.build).get), true, asyncHttpClient, listeners) 83 | } 84 | 85 | /** 86 | * Close a WebSocket connection. The WebSocket providers will not get closed. To close it, use {@link #shutDown} 87 | */ 88 | def close: WebSocket = { 89 | logger.trace("Closing") 90 | 91 | webSocket.foreach(_.close) 92 | this 93 | } 94 | 95 | /** 96 | * Shutdown the underlying WebSocket provider ({@link AsyncHttpClient}) 97 | */ 98 | def shutDown { 99 | asyncHttpClient.closeAsynchronously 100 | } 101 | 102 | /** 103 | * Add a {@link MessageListener} 104 | */ 105 | def listener(l: MessageListener): WebSocket = { 106 | 107 | var wrapper: WebSocketListener = null 108 | 109 | if (classOf[TextListener].isAssignableFrom(l.getClass)) { 110 | wrapper = new TextListenerWrapper(l) { 111 | override def onOpen(w: com.ning.http.client.ws.WebSocket) { 112 | super.onOpen(w) 113 | } 114 | } 115 | } else { 116 | wrapper = new BinaryListenerWrapper(l) { 117 | override def onOpen(w: com.ning.http.client.ws.WebSocket) { 118 | super.onOpen(w) 119 | } 120 | } 121 | } 122 | 123 | if (isOpen) { 124 | webSocket.get.addWebSocketListener(wrapper) 125 | l.onOpen 126 | } else { 127 | listeners.append(wrapper) 128 | } 129 | 130 | this 131 | } 132 | 133 | /** 134 | * Remove a {@link MessageListener} 135 | */ 136 | def removeListener(l: MessageListener): WebSocket = { 137 | var wrapper: WebSocketListener = null 138 | 139 | if (classOf[TextListener].isAssignableFrom(l.getClass)) { 140 | wrapper = new TextListenerWrapper(l) { 141 | override def onOpen(w: com.ning.http.client.ws.WebSocket) { 142 | super.onOpen(w) 143 | } 144 | } 145 | } else { 146 | wrapper = new BinaryListenerWrapper(l) { 147 | override def onOpen(w: com.ning.http.client.ws.WebSocket) { 148 | super.onOpen(w) 149 | } 150 | } 151 | } 152 | if (isOpen) { 153 | webSocket.get.removeWebSocketListener(wrapper) 154 | } else { 155 | listeners -= wrapper 156 | } 157 | this 158 | } 159 | 160 | /** 161 | * Send a text message. 162 | */ 163 | def send(s: String): WebSocket = { 164 | if (!isOpen) throw new WebSocketException("Not Connected", null) 165 | 166 | logger.trace("Sending to {}", s) 167 | 168 | webSocket.get.sendMessage(s) 169 | this 170 | } 171 | 172 | /** 173 | * Send a byte message. 174 | */ 175 | def send(s: Array[Byte]): WebSocket = { 176 | if (!isOpen) throw new WebSocketException("Not Connected", null) 177 | 178 | logger.trace("Sending to {}", s) 179 | 180 | webSocket.get.sendMessage(s) 181 | this 182 | } 183 | } 184 | 185 | private class TextListenerWrapper(l: MessageListener) extends WebSocketTextListener with WebSocketCloseCodeReasonListener { 186 | 187 | override def onOpen(w: com.ning.http.client.ws.WebSocket) { 188 | l.onOpen 189 | } 190 | 191 | override def onClose(w: com.ning.http.client.ws.WebSocket) { 192 | l.onClose 193 | } 194 | 195 | override def onError(t: Throwable) { 196 | l.onError(t) 197 | } 198 | 199 | override def onMessage(s: String) { 200 | l.onMessage(s) 201 | } 202 | 203 | override def onClose(w: com.ning.http.client.ws.WebSocket, code: Int, reason: String) { 204 | l.onClose(code, reason) 205 | } 206 | 207 | override def hashCode() = l.hashCode 208 | 209 | override def equals(o: Any): Boolean = { 210 | o match { 211 | case t: TextListenerWrapper => t.hashCode.equals(this.hashCode) 212 | case _ => false 213 | } 214 | } 215 | } 216 | 217 | private class BinaryListenerWrapper(l: MessageListener) extends WebSocketByteListener with WebSocketCloseCodeReasonListener { 218 | 219 | override def onOpen(w: com.ning.http.client.ws.WebSocket) { 220 | l.onOpen 221 | } 222 | 223 | override def onClose(w: com.ning.http.client.ws.WebSocket) { 224 | l.onClose 225 | } 226 | 227 | override def onClose(w: com.ning.http.client.ws.WebSocket, code: Int, reason: String) { 228 | l.onClose(code, reason) 229 | } 230 | 231 | override def onError(t: Throwable) { 232 | l.onError(t) 233 | } 234 | 235 | override def onMessage(s: Array[Byte]) { 236 | l.onMessage(s) 237 | } 238 | 239 | override def hashCode() = l.hashCode 240 | 241 | override def equals(o: Any): Boolean = { 242 | o match { 243 | case t: BinaryListenerWrapper => t.hashCode.equals(this.hashCode) 244 | case _ => false 245 | } 246 | } 247 | } 248 | 249 | -------------------------------------------------------------------------------- /src/test/scala/org/jfarcand/wcs/test/BaseTest.scala: -------------------------------------------------------------------------------- 1 | package org.jfarcand.wcs.test 2 | 3 | import org.eclipse.jetty.server.handler.HandlerWrapper 4 | import org.eclipse.jetty.websocket.WebSocketFactory 5 | import org.slf4j.{ LoggerFactory, Logger } 6 | import org.eclipse.jetty.server.nio.SelectChannelConnector 7 | import java.net.ServerSocket 8 | import javax.servlet.http.{ HttpServletResponse, HttpServletRequest } 9 | import org.eclipse.jetty.server.{ Request, Server } 10 | 11 | import org.scalatest.{ FlatSpec, BeforeAndAfterAll } 12 | import org.scalatest.Matchers 13 | 14 | trait BaseTest extends FlatSpec with BeforeAndAfterAll with Matchers { 15 | 16 | protected val server: Server = new Server() 17 | protected final val log: Logger = LoggerFactory.getLogger(classOf[BaseTest]) 18 | protected var port1: Int = 0 19 | private var _connector: SelectChannelConnector = null 20 | 21 | override protected def beforeAll(): Unit = { 22 | port1 = findFreePort 23 | _connector = new SelectChannelConnector 24 | _connector.setPort(port1) 25 | server.addConnector(_connector) 26 | val _wsHandler: BaseTest#WebSocketHandler = getWebSocketHandler 27 | server.setHandler(_wsHandler) 28 | server.start() 29 | log.info("Local HTTP server started successfully") 30 | } 31 | 32 | override protected def afterAll(): Unit = server.stop() 33 | 34 | abstract class WebSocketHandler extends HandlerWrapper with WebSocketFactory.Acceptor { 35 | 36 | def getWebSocketFactory: WebSocketFactory = { 37 | _webSocketFactory 38 | } 39 | 40 | override def handle(target: String, baseRequest: Request, request: HttpServletRequest, response: HttpServletResponse): Unit = { 41 | if (_webSocketFactory.acceptWebSocket(request, response) || response.isCommitted) return 42 | super.handle(target, baseRequest, request, response) 43 | } 44 | 45 | def checkOrigin(request: HttpServletRequest, origin: String): Boolean = { 46 | true 47 | } 48 | 49 | private final val _webSocketFactory: WebSocketFactory = new WebSocketFactory(this, 32 * 1024) 50 | } 51 | 52 | protected def findFreePort: Int = { 53 | var socket: ServerSocket = null 54 | try { 55 | socket = new ServerSocket(0) 56 | socket.getLocalPort 57 | } finally { 58 | if (socket != null) { 59 | socket.close() 60 | } 61 | } 62 | } 63 | 64 | protected def getTargetUrl: String = { 65 | "ws://127.0.0.1:" + port1 66 | } 67 | 68 | def getWebSocketHandler: BaseTest#WebSocketHandler 69 | } -------------------------------------------------------------------------------- /src/test/scala/org/jfarcand/wcs/test/WSSTest.scala: -------------------------------------------------------------------------------- 1 | package org.jfarcand.wcs.test 2 | 3 | import java.util.concurrent.CountDownLatch 4 | 5 | import org.jfarcand.wcs.{MessageListener, WebSocket} 6 | import org.scalatest.{Matchers, FlatSpec} 7 | 8 | class WSSTest extends FlatSpec with Matchers { 9 | 10 | it should "receive three messages" in { 11 | val w = WebSocket() 12 | val latch: CountDownLatch = new CountDownLatch(3) 13 | var messages: Seq[String] = Seq() 14 | w open "wss://stream.pushbullet.com/websocket/wHDLQQ4cWz7uH89MjpKh47dcc8AhyFrd" listener new MessageListener { 15 | override def onMessage(message: String): Unit = { 16 | println(message) 17 | messages = messages ++ Seq(message) 18 | latch.countDown() 19 | } 20 | override def onOpen: Unit = println("open connection") 21 | override def onClose: Unit = println("close connection") 22 | override def onClose(code: Int, reason: String): Unit = println(s"close connection, code ${code.toString}. reason: $reason") 23 | override def onError(t: Throwable): Unit = t.printStackTrace() 24 | override def onMessage(message: Array[Byte]): Unit = onMessage(new String(message)) 25 | } 26 | latch.await() 27 | assert(messages.size == 3) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/scala/org/jfarcand/wcs/test/WebSocketTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 Jeanfrancois Arcand 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package org.jfarcand.wcs.test 18 | 19 | import java.io.IOException 20 | import java.util.concurrent.atomic.AtomicReference 21 | import java.util.concurrent.{CountDownLatch, TimeUnit} 22 | import javax.servlet.http.HttpServletRequest 23 | 24 | import org.jfarcand.wcs._ 25 | import org.junit.runner.RunWith 26 | import org.scalatest.junit.JUnitRunner 27 | import org.scalatest.{Matchers => ShouldMatchers} 28 | 29 | @RunWith(classOf[JUnitRunner]) 30 | class WebSocketTest extends BaseTest with ShouldMatchers { 31 | 32 | private final class EchoTextWebSocket extends org.eclipse.jetty.websocket.WebSocket with org.eclipse.jetty.websocket.WebSocket.OnTextMessage with org.eclipse.jetty.websocket.WebSocket.OnBinaryMessage { 33 | private var connection: org.eclipse.jetty.websocket.WebSocket.Connection = null 34 | 35 | def onOpen(connection: org.eclipse.jetty.websocket.WebSocket.Connection): Unit = { 36 | this.connection = connection 37 | connection.setMaxTextMessageSize(1000) 38 | } 39 | 40 | def onClose(i: Int, s: String): Unit = connection.close() 41 | 42 | def onMessage(s: String): Unit = { 43 | try { 44 | connection.sendMessage(s) 45 | } catch { 46 | case e: IOException => 47 | try { 48 | connection.sendMessage("FAIL") 49 | } catch { 50 | case e1: IOException => e1.printStackTrace() 51 | } 52 | } 53 | } 54 | 55 | def onMessage(data: Array[Byte], offset: Int, length: Int): Unit = { 56 | try { 57 | connection.sendMessage(data, offset, length) 58 | } catch { 59 | case e: IOException => 60 | try { 61 | connection.sendMessage("FAIL") 62 | } catch { 63 | case e1: IOException => e1.printStackTrace() 64 | } 65 | } 66 | } 67 | } 68 | 69 | def getWebSocketHandler: BaseTest#WebSocketHandler = new WebSocketHandler { 70 | def doWebSocketConnect(httpServletRequest: HttpServletRequest, s: String): org.eclipse.jetty.websocket.WebSocket = { 71 | new EchoTextWebSocket 72 | } 73 | } 74 | 75 | it should "send a text message" in { 76 | val w = WebSocket() 77 | 78 | var s = "" 79 | val latch: CountDownLatch = new CountDownLatch(1) 80 | w.open(getTargetUrl).listener(new TextListener { 81 | 82 | override def onMessage(message: String) { 83 | s = message 84 | latch.countDown() 85 | } 86 | 87 | }).send("foo") 88 | 89 | latch.await() 90 | assert(s === "foo") 91 | } 92 | 93 | it should "send a byte message" in { 94 | val w = WebSocket() 95 | 96 | var s = "" 97 | val latch: CountDownLatch = new CountDownLatch(1) 98 | w.open(getTargetUrl).listener(new BinaryListener { 99 | 100 | override def onMessage(message: Array[Byte]) { 101 | s = new String(message) 102 | latch.countDown() 103 | } 104 | 105 | }).send("foo".getBytes) 106 | 107 | latch.await() 108 | assert(s === "foo") 109 | } 110 | 111 | it should "wait for an open event" in { 112 | val w = WebSocket() 113 | 114 | var s: Boolean = false 115 | val latch: CountDownLatch = new CountDownLatch(1) 116 | w.listener(new TextListener { 117 | 118 | override def onOpen { 119 | s = true 120 | latch.countDown() 121 | } 122 | 123 | }).open(getTargetUrl) 124 | 125 | latch.await() 126 | assert(s) 127 | } 128 | 129 | it should "wait for an open event with listener added after the open" in { 130 | val w = WebSocket() 131 | 132 | var s: Boolean = false 133 | val latch: CountDownLatch = new CountDownLatch(1) 134 | w.open(getTargetUrl).listener(new TextListener() { 135 | 136 | override def onOpen { 137 | s = true 138 | latch.countDown() 139 | } 140 | 141 | }) 142 | 143 | latch.await() 144 | assert(s) 145 | } 146 | 147 | it should "wait for an close event with code" in { 148 | var w = WebSocket() 149 | 150 | val s: AtomicReference[String] = new AtomicReference[String] 151 | val latch: CountDownLatch = new CountDownLatch(1) 152 | w = w.listener(new TextListener { 153 | 154 | override def onMessage(message: String) { 155 | w.close 156 | } 157 | 158 | override def onClose(code :Int, reason : String) { 159 | s.set(code + "-" + reason) 160 | latch.countDown() 161 | } 162 | 163 | }).open(getTargetUrl).send("foo") 164 | 165 | latch.await() 166 | assert(s.get() == "1000-Normal closure; the connection successfully completed whatever purpose for which it was created.") 167 | } 168 | 169 | 170 | it should "wait for an close event" in { 171 | var w = WebSocket() 172 | 173 | var s: Boolean = false 174 | val latch: CountDownLatch = new CountDownLatch(1) 175 | w = w.listener(new TextListener { 176 | 177 | override def onMessage(message: String) { 178 | w.close 179 | } 180 | 181 | override def onClose { 182 | s = true 183 | latch.countDown() 184 | } 185 | 186 | }).open(getTargetUrl).send("foo") 187 | 188 | latch.await() 189 | assert(s) 190 | } 191 | 192 | it should "open with an Option" in { 193 | val o = new Options 194 | o.userAgent = "test/1.1" 195 | var w = WebSocket(o) 196 | 197 | var s: Boolean = false 198 | val latch: CountDownLatch = new CountDownLatch(1) 199 | w = w.listener(new TextListener { 200 | 201 | override def onMessage(message: String) { 202 | w.close 203 | } 204 | 205 | override def onClose { 206 | s = true 207 | latch.countDown() 208 | } 209 | 210 | }).open(getTargetUrl).send("foo") 211 | 212 | latch.await() 213 | assert(s) 214 | } 215 | 216 | it should "remove a listener" in { 217 | var w = WebSocket() 218 | 219 | var s: Boolean = false 220 | val latch: CountDownLatch = new CountDownLatch(1) 221 | val t = new TextListener { 222 | 223 | override def onMessage(message: String) { 224 | s = true 225 | latch.countDown() 226 | } 227 | 228 | override def onClose { 229 | } 230 | 231 | } 232 | w = w.listener(t).removeListener(t).open(getTargetUrl).send("foo") 233 | 234 | latch.await(1, TimeUnit.SECONDS) 235 | assert(!s) 236 | } 237 | 238 | it should "remove a listener after open" in { 239 | var w = WebSocket() 240 | 241 | var s: Boolean = false 242 | val latch: CountDownLatch = new CountDownLatch(1) 243 | val t = new TextListener { 244 | 245 | override def onMessage(message: String) { 246 | s = true 247 | latch.countDown() 248 | } 249 | 250 | override def onClose { 251 | } 252 | 253 | } 254 | w = w.listener(t).open(getTargetUrl).removeListener(t).send("foo") 255 | 256 | latch.await(1, TimeUnit.SECONDS) 257 | assert(!s) 258 | } 259 | 260 | it should "remove a listener after receiving a message" in { 261 | var w = WebSocket() 262 | 263 | val latch: CountDownLatch = new CountDownLatch(2) 264 | val t = new TextListener { 265 | 266 | override def onMessage(message: String) { 267 | w.removeListener(this) 268 | latch.countDown() 269 | } 270 | 271 | override def onClose { 272 | } 273 | 274 | } 275 | w = w.listener(t).open(getTargetUrl).send("foo").send("foo") 276 | 277 | latch.await(1, TimeUnit.SECONDS) 278 | assert(latch.getCount == 1) 279 | } 280 | } 281 | 282 | --------------------------------------------------------------------------------