package com.twasyl.slideshowfx.server.service; import com.twasyl.slideshowfx.server.SlideshowFXServer; import com.twasyl.slideshowfx.server.beans.quiz.Quiz; import com.twasyl.slideshowfx.server.beans.quiz.QuizResult; import com.twasyl.slideshowfx.server.bus.EventBus; import com.twasyl.slideshowfx.utils.TemplateProcessor; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.eventbus.Message; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.core.shareddata.LocalMap; import io.vertx.ext.web.Router; import java.io.IOException; import java.io.StringWriter; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import static com.twasyl.slideshowfx.server.SlideshowFXServer.*; import static com.twasyl.slideshowfx.server.service.IServicesCode.*; /** * This class provides the quiz services. * * @author Thierry Wasylczenko * @version 1.0 * @since SlideshowFX 1.0 */ public class QuizService extends AbstractSlideshowFXService { private static final Logger LOGGER = Logger.getLogger(QuizService.class.getName()); public static final String SERVICE_QUIZ_ON_RESULT = "service.quiz.onResult"; private final String url = "/slideshowfx/quiz"; private Quiz currentQuiz = null; /** * The results of all quiz. The key of this Map represents the ID of the {@link Quiz}, the value the {@link QuizResult} which * contains all correct and wrong answers. */ private final Map<Long, QuizResult> results = new HashMap<>(); @Override public void start() { this.updatedRouteMatcher(); this.register(SERVICE_QUIZ_START, this.buildStartQuizHandler()) .register(SERVICE_QUIZ_STOP, this.buildStopQuizHandler()) .register(SERVICE_QUIZ_CURRENT, buildGetCurrentQuizHandler()); } private void updatedRouteMatcher() { final Router router = SlideshowFXServer.getSingleton().getRouter(); // URL for answering a quiz router.post(this.url.concat("/:quizid/answer")).handler(routingContext -> { int statusCode = 500; try { if (currentQuiz != null && currentQuiz.getId() == Long.parseLong(routingContext.request().getParam("quizid"))) { final String stringAnswer = routingContext.request().getFormAttribute("answer"); final JsonObject jsonAnswer = new JsonObject(new String(Base64.getDecoder().decode(stringAnswer))); final JsonArray answersArray = jsonAnswer.getJsonArray("answers"); final Long[] answers = new Long[answersArray.size()]; int index = 0; for (Object object : answersArray) { answers[index++] = ((Number) object).longValue(); } boolean isCorrect = currentQuiz.checkAnswers(answers); final QuizResult result = results.get(currentQuiz.getId()); if (result != null) { if (isCorrect) result.addCorrectAnswer(); else result.addWrongAnswer(); } statusCode = 200; } else { statusCode = 406; routingContext.response().setStatusCode(406).end(); } } finally { routingContext.response().setStatusCode(statusCode).end(); } }); // Get the JavaScript resources router.get("/slideshowfx/quiz/js/quizService.js").handler(routingContext -> { final LocalMap<String, String> templateTokens = this.vertx.sharedData().getLocalMap(SHARED_DATA_TEMPLATE_TOKENS); final Configuration configuration = TemplateProcessor.getJsConfiguration(); final Map tokenValues = new HashMap(); tokenValues.put(templateTokens.get(SHARED_DATA_SERVER_HOST_TOKEN).toString(), SlideshowFXServer.getSingleton().getHost()); tokenValues.put(templateTokens.get(SHARED_DATA_SERVER_PORT_TOKEN).toString(), SlideshowFXServer.getSingleton().getPort() + ""); try (final StringWriter writer = new StringWriter()) { final Template template = configuration.getTemplate("quizService.js"); template.process(tokenValues, writer); writer.flush(); routingContext.response().putHeader("Content-Type", "application/javascript").setStatusCode(200).setChunked(true).write(Buffer.buffer(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(); } }); } /** * Build a handler that will start the quiz. * @return The handler for starting a quiz. */ private Handler<Message<JsonObject>> buildStartQuizHandler() { final Handler<Message<JsonObject>> handler = message -> { final JsonObject object = message.body(); final String quizString = new String(Base64.getDecoder().decode(object.getString("encoded-quiz"))); QuizService.this.currentQuiz = Quiz.build(quizString); // Add the Quiz to the results. If the Quiz already exists, it won't be erased if (!QuizService.this.results.containsKey(QuizService.this.currentQuiz.getId())) { final QuizResult quizResult = new QuizResult(); quizResult.setQuiz(QuizService.this.currentQuiz); QuizService.this.results.put(QuizService.this.currentQuiz.getId(), quizResult); } final JsonObject encodedQuiz = new JsonObject() .put("encoded-quiz", Base64.getEncoder().encodeToString(this.currentQuiz.toJSON().encode().getBytes())); final JsonObject reply = this.buildResponse(SERVICE_QUIZ_START, RESPONSE_CODE_QUIZ_STARTED, encodedQuiz); this.sendResponseToWebSocketClients(reply); EventBus.getInstance().broadcast(SERVICE_QUIZ_ON_RESULT, QuizService.this.results.get(QuizService.this.currentQuiz.getId())); message.reply(reply); }; return handler; } private Handler<Message<JsonObject>> buildStopQuizHandler() { final Handler<Message<JsonObject>> handler = message -> { final JsonObject object = message.body(); final Long quizId = object.getLong("id"); // Ensure the ID is equal to the current quiz if(this.currentQuiz != null && this.currentQuiz.getId() == quizId) { this.currentQuiz = null; final JsonObject reply = this.buildResponse(SERVICE_QUIZ_STOP, RESPONSE_CODE_QUIZ_STOPPED, "The quiz has been stopped"); this.sendResponseToWebSocketClients(reply); } message.reply(this.buildResponse(SERVICE_QUIZ_STOP, RESPONSE_CODE_QUIZ_STOPPED, "Quiz stopped")); }; return handler; } private Handler<Message<JsonObject>> buildGetCurrentQuizHandler() { final Handler<Message<JsonObject>> handler = message -> { if(this.currentQuiz != null) { message.reply(this.buildResponse(SERVICE_QUIZ_CURRENT, RESPONSE_CODE_QUIZ_RETRIEVED, this.currentQuiz.toJSON())); } else { message.reply(this.buildResponse(SERVICE_QUIZ_CURRENT, RESPONSE_CODE_QUIZ_NOT_ACTIVE, "No quiz active")); } }; return handler; } }