/* * ============================================================================= * * Copyright (c) 2017, Daniel Fernandez (http://github.com/danielfernandez) * * 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.github.danielfernandez.matchday.web.controller; import com.github.danielfernandez.matchday.business.dataviews.MatchInfo; import com.github.danielfernandez.matchday.business.dataviews.MatchStatus; import com.github.danielfernandez.matchday.business.entities.MatchComment; import com.github.danielfernandez.matchday.business.repository.MatchCommentRepository; import com.github.danielfernandez.matchday.business.repository.MatchInfoRepository; import com.github.danielfernandez.matchday.business.repository.MatchStatusRepository; import com.github.danielfernandez.matchday.util.TimestampUtils; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.thymeleaf.spring5.context.webflux.IReactiveDataDriverContextVariable; import org.thymeleaf.spring5.context.webflux.IReactiveSSEDataDriverContextVariable; import org.thymeleaf.spring5.context.webflux.ReactiveDataDriverContextVariable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Controller public class MatchController { private final MatchInfoRepository matchInfoRepository; private final MatchStatusRepository matchStatusRepository; private final MatchCommentRepository matchCommentRepository; public MatchController( final MatchInfoRepository matchInfoRepository, final MatchStatusRepository matchStatusRepository, final MatchCommentRepository matchCommentRepository) { super(); this.matchInfoRepository = matchInfoRepository; this.matchStatusRepository = matchStatusRepository; this.matchCommentRepository = matchCommentRepository; } @RequestMapping("/match/{matchId}") public String match(@PathVariable String matchId, final Model model) { // Get the matchInfo in order to have basic info about the match final Mono<MatchInfo> matchInfo = this.matchInfoRepository.getMatchInfo(matchId); // This timestamp will allow us to render comments previous to this date server-side, and // then create an EventSource at the browser that will retrieve comments after this date // as Server-Sent Events (SSE) rendered in JSON final String timestamp = TimestampUtils.computeISO8601Timestamp(); // Get the stream of MatchComment objects final Flux<MatchComment> commentStream = this.matchCommentRepository .findByMatchIdAndTimestampLessThanEqualOrderByTimestampDesc(matchId, timestamp); // Create a data-driver context variable that sets Thymeleaf in data-driven mode, // rendering HTML (iterations) as items are produced in a reactive-friendly manner. // This object also works as wrapper that avoids Spring WebFlux trying to resolve // it completely before rendering the HTML. final IReactiveDataDriverContextVariable commentDataDriver = new ReactiveDataDriverContextVariable(commentStream, 1); // buffers size = 1 // Add all attributes to the model model.addAttribute("matchId", matchId); // Integer model.addAttribute("match", matchInfo); // Mono, will be resolved before rendering model.addAttribute("commentsTimestamp", timestamp); // String model.addAttribute("commentStream", commentDataDriver); // Flux wrapped in a DataDriver to avoid resolution // Return the template name (templates/match.html) return "match"; } @RequestMapping( value = "/match/{matchId}/statusStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public String matchStatusStream(@PathVariable String matchId, final Model model) { // Get the stream of MatchStatus objects, based on a tailable cursor. // See https://docs.mongodb.com/manual/core/tailable-cursors/ final Flux<MatchStatus> statusStream = this.matchStatusRepository.tailMatchStatusStreamByMatchId(matchId); // Create a data-driver context variable that sets Thymeleaf in data-driven mode // in order to produce (render) Server-Sent Events as the Flux produces values. // This object also works as wrapper that avoids Spring WebFlux trying to resolve // it completely before rendering the HTML. final IReactiveSSEDataDriverContextVariable statusDataDriver = new ReactiveDataDriverContextVariable(statusStream, 1); // buffers size = 1 // Add the stream as a model attribute model.addAttribute("statusStream", statusDataDriver); // Flux wrapped in a DataDriver to avoid resolution // Will use the same "match" template, but only a fragment: the matchStatus block. return "match :: #matchStatus"; } @ResponseBody // The response payload for this request will be rendered in JSON, not HTML @RequestMapping( value = "/match/{matchId}/commentStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<MatchComment> matchCommentStream( @PathVariable String matchId, @RequestParam String timestamp) { // Get the stream of MatchComment objects after the timestamp, based on a tailable cursor. // See https://docs.mongodb.com/manual/core/tailable-cursors/ return this.matchCommentRepository.findByMatchIdAndTimestampGreaterThan(matchId, timestamp); } }