package com.twasyl.slideshowfx.server;
import com.twasyl.slideshowfx.server.service.AbstractSlideshowFXService;
import com.twasyl.slideshowfx.server.service.ISlideshowFXServices;
import com.twasyl.slideshowfx.utils.ResourceHelper;
import com.twasyl.slideshowfx.utils.TemplateProcessor;
import com.twasyl.slideshowfx.utils.beans.Wrapper;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.eventbus.Message;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.ServerWebSocket;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonObject;
import io.vertx.core.shareddata.LocalMap;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import static com.twasyl.slideshowfx.server.service.AbstractSlideshowFXService.JSON_KEY_DATA;
import static com.twasyl.slideshowfx.server.service.AbstractSlideshowFXService.JSON_KEY_SERVICE;
/**
* This class represents an embedded server which is provided in SlideshowFX.
*
* @author Thierry Wasylczenko
* @version 1.0
* @since SlideshowFX 1.0
*/
public class SlideshowFXServer {
private static final Logger LOGGER = Logger.getLogger(SlideshowFXServer.class.getName());
private static volatile SlideshowFXServer singleton;
private Vertx vertx;
private HttpServer httpServer;
private Router router;
/**
* The context path of the server for accessing the server's application and resources.
*/
public static final String CONTEXT_PATH = "/slideshowfx";
/**
* The shared data that contains all tokens that can be found in templates.
*/
public static final String SHARED_DATA_TEMPLATE_TOKENS = "template.tokens";
/**
* The name of the key for retrieving the host the server is listening on and stored in {@link #SHARED_DATA_TEMPLATE_TOKENS}
*/
public static final String SHARED_DATA_SERVER_HOST_TOKEN = "template.tokens.server.host";
/**
* The name of the key for retrieving the port the server is listening on and stored in {@link #SHARED_DATA_TEMPLATE_TOKENS}
*/
public static final String SHARED_DATA_SERVER_PORT_TOKEN = "template.tokens.server.port";
private String host;
private int port;
private String twitterHashtag;
private final Set<ServerWebSocket> websockets = new HashSet<>();
private SlideshowFXServer(String host, int port, String twitterHashtag) {
this.host = host;
this.port = port;
this.twitterHashtag = twitterHashtag;
}
/**
* Deploy a {@link io.vertx.core.Verticle} identified by the given class.
*
* @param clazz The class of the Verticle to deploy.
*/
public void deploy(Class<? extends AbstractVerticle> clazz) {
try {
this.vertx.deployVerticle(clazz.newInstance(), result -> {
if (result.succeeded()) {
LOGGER.log(Level.FINE, "Verticle has been deployed successfully. Result: " + result.result());
} else if (result.failed()) {
LOGGER.log(Level.WARNING, "Verticle hasn't been deployed properly. Result: " + result.result(), result.cause());
}
});
} catch (InstantiationException | IllegalAccessException e) {
LOGGER.log(Level.SEVERE, "Verticle hasn't been deployed properly.", e);
}
}
/**
* Get the host of the server.
* @return The host of the server.
*/
public String getHost() { return host; }
/**
* Get the port of the server
* @return The port of the server.
*/
public int getPort() { return port; }
/**
* Get the Twitter hashtag to look on Twitter.
* @return The Twitter hashtag to look on Twitter.
*/
public String getTwitterHashtag() { return this.twitterHashtag; }
/**
* Get the HTTP server that has been created with this server.
* @return The HTTP server created by this server.
*/
public HttpServer getHttpServer() { return httpServer; }
/**
* Get the {@link Router} that has been created with this server.
* @return The Router created by this server.
*/
public Router getRouter() { return this.router; }
public Set<ServerWebSocket> getWebSockets() {
return this.websockets;
}
/**
* Starts the embedded server.
* @param services List of classes extending {@link ISlideshowFXServices} that have to be started with the
* server
*/
public void start(Class<? extends ISlideshowFXServices> ... services) {
if (this.vertx == null) {
this.vertx = Vertx.vertx();
/*
* Add some useful resources that can be shared by multiple Verticles.
* One of them is a map named "template.tokens" which contains tokens that can be found in a template.
*/
LocalMap<String, String> templateTokens = this.vertx.sharedData().getLocalMap(SHARED_DATA_TEMPLATE_TOKENS);
templateTokens.put(SHARED_DATA_SERVER_HOST_TOKEN, "slideshowfx_server_ip");
templateTokens.put(SHARED_DATA_SERVER_PORT_TOKEN, "slideshowfx_server_port");
this.httpServer = this.vertx.createHttpServer();
this.httpServer.websocketHandler(this.buildWebSocketHandler());
this.router = Router.router(this.vertx);
this.buildRouter();
this.httpServer.requestHandler(this.router::accept);
if(services != null && services.length > 0) {
Arrays.stream(services).forEach(service -> this.vertx.deployVerticle(service.getName()));
}
this.httpServer.listen(SlideshowFXServer.getSingleton().getPort(), SlideshowFXServer.getSingleton().getHost());
} else {
LOGGER.log(Level.INFO, "Server already started");
}
}
/**
* Stops the embedded server.
*/
public void stop() {
if(this.vertx != null) {
this.vertx.close();
}
this.vertx = null;
this.httpServer = null;
this.router = null;
this.websockets.clear();
singleton = null;
}
/**
* <p>Call a service using the EventBus of Vert.x. The request must be a JSON object with a field named {@code service}
* representing the service to call, and a JSON object named {@code data} containing the data the service will
* consume.</p>
* <p>The response's format will be a JSON object with:</p>
* <ul>
* <li>the <b>service</b> key which indicates the name of the service which is called, as a string ;</li>
* <li>the <b>status</b> key which represents the HTML return code of the service. 200 if everything went well,
* ..., as an integer ;</li>
* <li>the <b>content</b> key which is the response's content returned by the service, as a JSON structured
* (an object, an array, etc) depending on the service</li>
* </ul>
* @param request The JSON object corresponding to the service to call.
* @return The response corresponding to the request.
* @throws java.lang.IllegalArgumentException If the request is invalid.
*/
public JsonObject callService(String request) throws IllegalArgumentException {
final Wrapper<JsonObject> response = new Wrapper<>();
try {
final JsonObject jsonRequest = new JsonObject(request);
final String service = jsonRequest.getString(JSON_KEY_SERVICE);
final JsonObject data = jsonRequest.getJsonObject(JSON_KEY_DATA);
if(service == null) throw new IllegalArgumentException("The service in the request must be present");
if(service.trim().isEmpty()) throw new IllegalArgumentException("The service in the request can not be empty");
if(data == null) throw new IllegalArgumentException("The data in the request must be present");
final Thread thread = new Thread(new Runnable() {
private Boolean continueProcess = false;
@Override
public void run() {
SlideshowFXServer.this.vertx.eventBus().send(service, data, (AsyncResult<Message<JsonObject>> ar) -> {
response.setValue(ar.result().body());
continueProcess = true;
});
while(!continueProcess) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
LOGGER.log(Level.SEVERE, "Can not wait result when calling a service", e);
}
}
}
});
thread.start();
thread.join();
} catch(DecodeException e) {
throw new IllegalArgumentException("The request is invalid", e);
} catch(ClassCastException e) {
LOGGER.log(Level.SEVERE, "An error occurred", e);
} catch (InterruptedException e) {
LOGGER.log(Level.SEVERE, "An error occurred", e);
}
return response.getValue();
}
/**
* Build the {@link Router} that will handle shared routes.
*/
private void buildRouter() {
final BodyHandler handler = BodyHandler.create();
handler.setMergeFormAttributes(false);
router.route().handler(handler);
// Get the main page
router.get(CONTEXT_PATH).handler(routingContext -> {
final LocalMap<String, String> templateTokens = this.vertx.sharedData().getLocalMap(SlideshowFXServer.SHARED_DATA_TEMPLATE_TOKENS);
final Configuration configuration = TemplateProcessor.getHtmlConfiguration();
final Map tokenValues = new HashMap();
tokenValues.put(templateTokens.get(SlideshowFXServer.SHARED_DATA_SERVER_HOST_TOKEN).toString(), this.getHost());
tokenValues.put(templateTokens.get(SlideshowFXServer.SHARED_DATA_SERVER_PORT_TOKEN).toString(), this.getPort() + "");
try (final StringWriter writer = new StringWriter()) {
final Template template = configuration.getTemplate("slideshowfx.html");
template.process(tokenValues, writer);
writer.flush();
routingContext.response().setStatusCode(200).setChunked(true).write(writer.toString()).end();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Error when a client tried to access the chat", e);
routingContext.response().setStatusCode(500).end();
} catch (TemplateException e) {
LOGGER.log(Level.WARNING, "Error when processing the chat template", e);
routingContext.response().setStatusCode(500).end();
}
});
router.get(CONTEXT_PATH.concat("/images/logo.svg")).handler(routingContext -> {
try (final InputStream in = ResourceHelper.getInputStream("/com/twasyl/slideshowfx/images/logo.svg")) {
byte[] imageBuffer = new byte[1028];
int numberOfBytesRead;
Buffer buffer = Buffer.buffer();
while ((numberOfBytesRead = in.read(imageBuffer)) != -1) {
buffer.appendBytes(imageBuffer, 0, numberOfBytesRead);
}
routingContext.response().headers().set("Content-Type", "image/svg+xml");
routingContext.response().setChunked(true).write(buffer).end();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Can not send check images", e);
}
});
}
/**
* Build the WebSocket handler for handling shared WebSocket calls.
* @return
*/
private Handler<ServerWebSocket> buildWebSocketHandler() {
final Handler<ServerWebSocket> handler = serverWebSocket -> {
if (CONTEXT_PATH.equals(serverWebSocket.path())) {
// Add the textHandlerID to the list of WebSocket clients
this.websockets.add(serverWebSocket);
// When the socket is closed, remove it from the list of clients
serverWebSocket.endHandler(event -> {
this.websockets.remove(serverWebSocket);
});
/*
* When data are received, get the content which is expected to be a JSON object with two fields:
* <ul>
* <li>service: representing the service to call using the EventBus</li>
* <li>data: representing a JSON object containing the data that is expected by the service.</li>
* </ul>
*/
serverWebSocket.handler(buffer -> {
final String bufferString = new String(buffer.getBytes());
final JsonObject request = new JsonObject(bufferString);
// Inject source caller so the service, if it needs to, can send something to all WebSocket clients
// but exclude the "sender"
final JsonObject data = request.getJsonObject(AbstractSlideshowFXService.JSON_KEY_DATA);
data.put(AbstractSlideshowFXService.JSON_KEY_ORIGIN, serverWebSocket.textHandlerID());
this.vertx.eventBus().send(request.getString(JSON_KEY_SERVICE), data, asyncResult -> {
final JsonObject json = (JsonObject) asyncResult.result().body();
serverWebSocket.write(Buffer.buffer(json.encode()));
});
});
} else {
serverWebSocket.reject();
}
};
return handler;
}
/**
* Return the latest created server.
*
* @return The latest created server or null if no server was created.
*/
public static SlideshowFXServer getSingleton() { return singleton; }
/**
* <p>Creates an instance of the {@link SlideshowFXServer}. The created instance will act as singleton and if a previous
* instance exists, it is stopped by the method.</p>
* <p>This method doesn't start the server. If you want to start it, please use {@link #start(Class[])})}.</p>
*
* @param host The hostname the server will listen on.
* @param port The port the server will listen on.
* @param twitterHashtag A Twitter hashtag if needed.
* @return The instance of the {@link SlideshowFXServer} that has been created.
*/
public static SlideshowFXServer create(final String host, final int port, final String twitterHashtag) {
if(singleton != null && singleton.vertx != null) {
singleton.stop();
}
singleton = new SlideshowFXServer(host, port, twitterHashtag);
return singleton;
}
}