ScalatraのAtmosphereはWebSocketで文字列しか送信できない

ScalatraでWebSocketをやるためのサポートとしてはAtmosphereが組み込まれていますが、Scalatra側の問題で文字列しか送信できないようになってしまっているという話。

trait ScalatraBroadcaster extends Broadcaster {

  private[this] val logger: Logger = Logger[ScalatraBroadcaster]
  protected var _resources: ConcurrentLinkedQueue[AtmosphereResource]
  protected var _wireFormat: WireFormat
  protected implicit var _actorSystem: ActorSystem

  def broadcast[T <: OutboundMessage](msg: T, clientFilter: ClientFilter)(implicit executionContext: ExecutionContext): Future[T] = {
    val selectedResources = _resources.asScala filter clientFilter
    logger.trace("Sending %s to %s".format(msg, selectedResources.map(_.uuid)))
    broadcast(_wireFormat.render(msg), selectedResources.toSet.asJava).map(_ => msg)
  }

}
trait WireFormat {
  def name: String
  def supportsAck: Boolean
  def parseInMessage(message: String): InboundMessage
  def parseOutMessage(message: String): OutboundMessage
  def render(message: OutboundMessage): String
}

これがScalatra+Atmosphereでメッセージを送信する場合に最終的に呼ばれるメソッドbroadcastの定義なんですが、継承しているインタフェースのBroadcaster.broadcastの第一引数はObject(Any)なのに対し、WireFormat.renderの型はStringに固定されてしまっています。

しかも、OutboundMessageにBinaryMessageって型があるのに、デフォルト実装では対応なし(空文字の送信になる)。そこはあきらめて、自作WireFormatをAtmosphereSupportを使用しているクラスでimplicit defしてあげましょう。

class BinaryWireFormat extends JacksonSimpleWireformat {
  override def render(message: OutboundMessage) = message match {
    case TextMessage(text) => text
    case JsonMessage(json) => renderJson(json)
    case BinaryMessage(binary) => Base64.getEncoder.encodeToString(binary)
    case _ => ""
  }
}

class MyScalatraServlet extends AtmosphereSupport {
  protected override implicit def wireFormat: WireFormat = new BinaryWireFormat

  atmosphere("/hoge") {
    new AtmosphereClient {
      def receive = {
        case Connected => println("connect")
        case Disconnected(disconnector, Some(error)) => println("disconnect")
        case Error(Some(error)) => println(error)
        case TextMessage(text) => send(new BinaryMessage(/* some Array[Byte] */))
        case JsonMessage(json) => send("not supported")
      }
    }
  }
}

atmosphereのソースも読んでみたが、renderの戻り値型をAnyに変更してArray[Byte]とかを突っ込んだ場合にどんな動きをするかはわからなかった。そこが分かれば、pull requestを出してもいいかもしれない。