├── .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 | 26 | org.jfarcand 27 | wcs 28 | 1.4 29 | 30 | ``` 31 | 32 | or a single artifact that contains all its dependencies 33 | 34 | ```xml 35 | 36 | org.jfarcand 37 | wcs-all 38 | 1.4 39 | 40 | ``` 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.sonatype.oss 5 | oss-parent 6 | 5 7 | 8 | 4.0.0 9 | org.jfarcand 10 | wcs 11 | WebSocket Client Library for Scala 12 | 1.6-SNAPSHOT 13 | jar 14 | 15 | A simple WebSocket Client for Scala 16 | 17 | http://github.com/jfarcand/wsc 18 | 19 | scm:git:git@github.com:jfarcand/WCS.git 20 | https://github.com/jfarcand/WCS.git 21 | scm:git:git@github.com:jfarcand/WCS.git 22 | 23 | 24 | github 25 | ihttps://github.com/jfarcand/WCS/issues 26 | 27 | 28 | 29 | 2.0.9 30 | 31 | 32 | 33 | jfarcand 34 | Jeanfrancois Arcand 35 | jfarcand@apache.org 36 | 37 | 38 | 39 | 40 | Apache License 2.0 41 | http://www.apache.org/licenses/LICENSE-2.0.html 42 | repo 43 | 44 | 45 | 46 | 47 | com.ning 48 | async-http-client 49 | 1.9.28 50 | 51 | 52 | 53 | 54 | org.scalatest 55 | scalatest_2.11 56 | 2.2.4 57 | test 58 | 59 | 60 | junit 61 | junit 62 | 4.8.1 63 | test 64 | 65 | 66 | ch.qos.logback 67 | logback-classic 68 | 0.9.26 69 | test 70 | 71 | 72 | org.eclipse.jetty 73 | jetty-servlet 74 | 7.6.0.v20120127 75 | test 76 | 77 | 78 | org.eclipse.jetty 79 | jetty-server 80 | 7.6.0.v20120127 81 | test 82 | 83 | 84 | org.eclipse.jetty 85 | jetty-websocket 86 | 7.6.0.v20120127 87 | test 88 | 89 | 90 | org.eclipse.jetty 91 | jetty-servlets 92 | 7.6.0.v20120127 93 | test 94 | 95 | 96 | org.scala-lang 97 | scala-compiler 98 | 2.11.7 99 | compile 100 | 101 | 102 | 103 | src/main/java 104 | src/test/java 105 | 106 | 107 | 108 | org.apache.maven.wagon 109 | wagon-ssh-external 110 | 1.0-beta-6 111 | 112 | 113 | install 114 | 115 | 116 | org.apache.maven.plugins 117 | maven-javadoc-plugin 118 | 2.8.1 119 | 120 | true 121 | 1.5 122 | UTF-8 123 | 1g 124 | 125 | http://java.sun.com/javase/6/docs/api/ 126 | 127 | 128 | 129 | 130 | attach-javadocs 131 | verify 132 | 133 | jar 134 | 135 | 136 | 137 | 138 | 139 | org.apache.maven.plugins 140 | maven-compiler-plugin 141 | 2.3.2 142 | 143 | ${source.property} 144 | ${target.property} 145 | 1024m 146 | 147 | 148 | 149 | org.apache.maven.plugins 150 | maven-shade-plugin 151 | 1.2.1 152 | 153 | 154 | package 155 | 156 | shade 157 | 158 | 159 | true 160 | all 161 | 162 | 163 | commons-codec:commons-codec 164 | commons-lang:commons-lang 165 | commons-logging:commons-logging 166 | junit:junit 167 | log4j:log4j 168 | commons-httpclient:commons-httpclient 169 | org.scala-lang:scala-library 170 | org.scala-lang:scala-compiler 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | org.apache.felix 184 | maven-bundle-plugin 185 | 2.3.4 186 | true 187 | 188 | META-INF 189 | 190 | $(replace;$(project.version);-SNAPSHOT;.$(tstamp;yyyyMMdd-HHmm)) 191 | 192 | jfarcand 193 | 194 | * 195 | 196 | 197 | org.jfarcand.*;version="$(replace;$(project.version);-SNAPSHOT;"")" 198 | 199 | 200 | 201 | 202 | 203 | osgi-bundle 204 | package 205 | 206 | bundle 207 | 208 | 209 | 210 | 211 | 212 | org.apache.maven.plugins 213 | maven-resources-plugin 214 | 2.4.3 215 | 216 | UTF-8 217 | 218 | 219 | 220 | org.apache.maven.plugins 221 | maven-release-plugin 222 | 2.1 223 | 224 | 225 | org.apache.maven.plugins 226 | maven-source-plugin 227 | 2.1.2 228 | 229 | 230 | attach-sources 231 | verify 232 | 233 | jar-no-fork 234 | 235 | 236 | 237 | 238 | 239 | 240 | org.scala-tools 241 | maven-scala-plugin 242 | 2.15.2 243 | 244 | 245 | 246 | compile 247 | testCompile 248 | 249 | 250 | 251 | 252 | 253 | -Xmx384m 254 | 255 | 256 | -deprecation 257 | 258 | 259 | 260 | run-scalatest 261 | org.scalatest.tools.Runner 262 | 263 | -p 264 | ${project.build.testOutputDirectory} 265 | 266 | 267 | -Xmx512m 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | target/site 277 | 278 | 279 | org.scala-tools 280 | maven-scala-plugin 281 | 2.15.2 282 | 283 | 284 | org.apache.maven.plugins 285 | maven-javadoc-plugin 286 | 2.8.1 287 | 288 | true 289 | 1.5 290 | UTF-8 291 | 1g 292 | 293 | http://java.sun.com/javase/6/docs/api/ 294 | 295 | ${sun.boot.class.path} 296 | com.google.doclava.Doclava 297 | false 298 | -J-Xmx1024m 299 | 300 | com.google.doclava 301 | doclava 302 | 1.0.3 303 | 304 | 305 | -hdf project.name "${project.name} ${project.version}" 306 | -d ${project.reporting.outputDirectory}/apidocs 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | sonatype-nexus-staging 315 | Sonatype Release 316 | http://oss.sonatype.org/service/local/staging/deploy/maven2 317 | 318 | 319 | sonatype-nexus-snapshots 320 | sonatype-nexus-snapshots 321 | ${distMgmtSnapshotsUrl} 322 | 323 | 324 | 325 | http://oss.sonatype.org/content/repositories/snapshots 326 | true 327 | 1.5 328 | 1.5 329 | 330 | 331 | 332 | -------------------------------------------------------------------------------- /src/main/java/org/jfarcand/wcs/Dummy.java: -------------------------------------------------------------------------------- 1 | package org.jfarcand.wcs; 2 | 3 | /** 4 | * Dummy class used for generating javadoc. 5 | */ 6 | public class Dummy { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/scala/org/jfarcand/wcs/BinaryListener.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 | package org.jfarcand.wcs 17 | 18 | /** 19 | * Listen to WebSocket binary message. 20 | */ 21 | trait BinaryListener extends MessageListener { 22 | /** 23 | * Called when a binary message is received. 24 | */ 25 | def onMessage(message: Array[Byte]) 26 | } -------------------------------------------------------------------------------- /src/main/scala/org/jfarcand/wcs/MessageListener.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 | package org.jfarcand.wcs 17 | 18 | /** 19 | * A listener for WebSocket events. 20 | */ 21 | trait MessageListener { 22 | /** 23 | * Called when the {@link WebSocket} is opened 24 | */ 25 | def onOpen {} 26 | /** 27 | * Called when the {@link WebSocket} is closed 28 | */ 29 | def onClose {} 30 | /** 31 | * Called when the {@link WebSocket} is closed with its assic 32 | */ 33 | def onClose(code: Int, reason : String) {} 34 | /** 35 | * Called when an unexpected error occurd on a {@link WebSocket} 36 | */ 37 | def onError(t: Throwable) {} 38 | /** 39 | * Called when a text message is received 40 | */ 41 | def onMessage(message: String) {} 42 | /** 43 | * Called when a binary message is received. 44 | */ 45 | def onMessage(message: Array[Byte]) {} 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/scala/org/jfarcand/wcs/Options.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 | package org.jfarcand.wcs 17 | 18 | /** 19 | * Configure the WebSocket class. 20 | */ 21 | class Options { 22 | 23 | /** 24 | * The maximum idle time of the websocket's connection before it gets closed 25 | */ 26 | var idleTimeout = 5 * 60000 27 | /** 28 | * The WebSocket message maximum size 29 | */ 30 | var maxMessageSize = 8192 31 | /** 32 | * The User Agent used by this library 33 | */ 34 | var userAgent = "wCS/1.0" 35 | /** 36 | * The WebSocket protocol. 37 | */ 38 | var protocol = null 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/org/jfarcand/wcs/TextListener.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 | package org.jfarcand.wcs 17 | 18 | /** 19 | * Listen to WebSocket text message 20 | */ 21 | trait TextListener extends MessageListener { 22 | /** 23 | * Called when a text message is received 24 | */ 25 | override def onMessage(message: String) 26 | } -------------------------------------------------------------------------------- /src/main/scala/org/jfarcand/wcs/WebSocketException.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 | package org.jfarcand.wcs 17 | 18 | /** 19 | * Wrapper around the underlying exception. 20 | */ 21 | class WebSocketException(msg: String, t: Throwable) extends Exception(msg, t) { 22 | } -------------------------------------------------------------------------------- /src/main/scala/org/jfarcand/wcs/Websocket.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 | package org.jfarcand.wcs 17 | 18 | import com.ning.http.client.providers.netty.NettyAsyncHttpProviderConfig 19 | import com.ning.http.client.{AsyncHttpClientConfig, AsyncHttpClient} 20 | import scala.Predef._ 21 | import com.ning.http.client.ws._ 22 | import collection.mutable.ListBuffer 23 | import org.slf4j.{Logger, LoggerFactory} 24 | 25 | /** 26 | * Simple WebSocket Fluid Client API 27 | *
  Websocket().open("ws://localhost".send("Hello").listener(new MyListener() {...}).close 
28 | */ 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 | --------------------------------------------------------------------------------