/*
* Copyright 2013-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.facebook.buck.httpserver;
import com.facebook.buck.event.external.events.BuckEventExternalInterface;
import com.facebook.buck.util.ObjectMappers;
import com.google.common.collect.Maps;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@SuppressWarnings("serial")
public class StreamingWebSocketServlet extends WebSocketServlet {
// This is threadsafe
private final Set<MyWebSocket> connections;
public StreamingWebSocketServlet() {
this.connections = Collections.newSetFromMap(Maps.<MyWebSocket, Boolean>newConcurrentMap());
}
@Override
public void configure(WebSocketServletFactory factory) {
// Most implementations of this method simply invoke factory.register(DispatchSocket.class);
// however, that requires DispatchSocket to have a no-arg constructor. That does not work for
// us because we would like all WebSockets created by this factory to have a reference to this
// parent class. This is why we override the default WebSocketCreator for the factory.
WebSocketCreator wrapperCreator = (req, resp) -> new MyWebSocket();
factory.setCreator(wrapperCreator);
}
public void tellClients(BuckEventExternalInterface event) {
if (connections.isEmpty()) {
return;
}
try {
String message = ObjectMappers.WRITER.writeValueAsString(event);
tellAll(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/** Sends the message to all WebSockets that are currently connected. */
private void tellAll(String message) {
for (MyWebSocket webSocket : connections) {
if (webSocket.isConnected()) {
webSocket.getRemote().sendStringByFuture(message);
}
}
}
/** This is the httpserver component of a WebSocket that maintains a session with one client. */
public class MyWebSocket extends WebSocketAdapter {
@Override
public void onWebSocketConnect(Session session) {
super.onWebSocketConnect(session);
connections.add(this);
// TODO(mbolin): Record all of the events for the last build that was started. For a fresh
// connection, replay all of the events to get the client caught up. Though must be careful,
// as this may not be a *new* connection from the client, but a *reconnection*, in which
// case we have to be careful about redrawing.
}
@Override
public void onWebSocketClose(int statusCode, String reason) {
super.onWebSocketClose(statusCode, reason);
connections.remove(this);
}
@Override
public void onWebSocketText(String message) {
super.onWebSocketText(message);
// TODO(mbolin): Handle requests from client instead of only pushing data down.
}
}
}