diff --git a/play-framework/websockets/app/actors/Messenger.java b/play-framework/websockets/app/actors/Messenger.java new file mode 100644 index 0000000000..1c9335b82e --- /dev/null +++ b/play-framework/websockets/app/actors/Messenger.java @@ -0,0 +1,111 @@ +package actors; + +import akka.actor.AbstractActor; +import akka.actor.ActorRef; +import akka.actor.PoisonPill; +import akka.actor.Props; +import akka.event.Logging; +import akka.event.LoggingAdapter; +import akka.http.javadsl.Http; +import akka.http.javadsl.marshallers.jackson.Jackson; +import akka.http.javadsl.model.HttpMessage; +import akka.http.javadsl.model.HttpRequest; +import akka.http.javadsl.model.HttpResponse; +import akka.stream.Materializer; +import com.fasterxml.jackson.databind.JsonNode; +import dto.MessageDTO; +import dto.RequestDTO; +import utils.MessageConverter; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ThreadLocalRandom; + +public class Messenger extends AbstractActor { + private LoggingAdapter log = Logging.getLogger(getContext().getSystem(), this); + + private ActorRef out; + + public Messenger(ActorRef out) { + this.out = out; + } + + public static Props props(ActorRef out) { + return Props.create(Messenger.class, () -> new Messenger(out)); + } + + @Override + public void preStart() throws Exception { + log.info("Messenger actor started at {}", + OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); + } + + @Override + public void postStop() throws Exception { + log.info("Messenger actor stopped at {}", + OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); + } + + private void onSendMessage(JsonNode jsonNode) { + RequestDTO requestDTO = MessageConverter.jsonNodeToRequest(jsonNode); + String message = requestDTO.getMessage().toLowerCase(); + if("stop".equals(message)) { + MessageDTO messageDTO = createMessageDTO("1", "1", "Stop", "Stopping actor"); + out.tell(MessageConverter.messageToJsonNode(messageDTO), getSelf()); + self().tell(PoisonPill.getInstance(), getSelf()); + } else { + log.info("Actor received. {}", requestDTO); + processMessage(requestDTO); + } + } + + private MessageDTO createMessageDTO(String userId, String id, String title, String message) { + MessageDTO messageDTO = new MessageDTO(); + messageDTO.setUserId(UUID.randomUUID().toString()); + messageDTO.setId(UUID.randomUUID().toString()); + messageDTO.setTitle("Self Kill"); + messageDTO.setBody("Stopping actor"); + return messageDTO; + } + + private void processMessage(RequestDTO requestDTO) { + CompletionStage responseFuture = getRandomMessage(); + responseFuture.thenCompose(this::consumeHttpResponse) + .thenAccept(messageDTO -> + out.tell(MessageConverter.messageToJsonNode(messageDTO), getSelf())); + } + + private CompletionStage getRandomMessage() { + int postId = ThreadLocalRandom.current().nextInt(0, 100); + return Http.get(getContext().getSystem()).singleRequest( + HttpRequest.create("https://jsonplaceholder.typicode.com/posts/" + postId) + ); + } + + private void discardEntity(HttpResponse httpResponse, Materializer materializer) { + httpResponse.discardEntityBytes(materializer) + .completionStage() + .whenComplete((done, ex) -> log.info("Entity discarded completely!")); + } + + private CompletionStage consumeHttpResponse(HttpResponse httpResponse) { + Materializer materializer = Materializer.matFromSystem(getContext().getSystem()); + return Jackson.unmarshaller(MessageDTO.class) + .unmarshal(httpResponse.entity(), materializer) + .thenApply(messageDTO -> { + log.info("Received message: {}", messageDTO); + discardEntity(httpResponse, materializer); + return messageDTO; + }); + } + + @Override + public Receive createReceive() { + return receiveBuilder() + .match(JsonNode.class, this::onSendMessage) + .matchAny(o -> log.error("Received unknown message: {}", o.getClass())) + .build(); + } +} diff --git a/play-framework/websockets/app/controllers/HomeController.java b/play-framework/websockets/app/controllers/HomeController.java new file mode 100644 index 0000000000..39c670fe59 --- /dev/null +++ b/play-framework/websockets/app/controllers/HomeController.java @@ -0,0 +1,79 @@ +package controllers; + +import actors.Messenger; +import akka.actor.ActorSystem; +import akka.stream.Materializer; +import akka.stream.javadsl.Flow; +import akka.stream.javadsl.Sink; +import akka.stream.javadsl.Source; +import com.fasterxml.jackson.databind.JsonNode; +import dto.MessageDTO; +import lombok.extern.slf4j.Slf4j; +import play.libs.F; +import play.libs.streams.ActorFlow; +import play.mvc.*; +import utils.MessageConverter; + +import javax.inject.Inject; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +@Slf4j +public class HomeController extends Controller { + private ActorSystem actorSystem; + private Materializer materializer; + + @Inject + public HomeController(ActorSystem actorSystem, Materializer materializer) { + this.actorSystem = actorSystem; + this.materializer = materializer; + } + + public Result index(Http.Request request) { + String url = routes.HomeController.socket().webSocketURL(request); + //To test WebSockets with akka streams, uncomment the next line and comment out the previous + //String url = routes.HomeController.akkaStreamsSocket().webSocketURL(request); + return ok(views.html.index.render(url)); + } + + + public WebSocket socket() { + return WebSocket.Json.acceptOrResult(this::createActorFlow); + } + + private CompletionStage>> createActorFlow( + Http.RequestHeader request) { + return CompletableFuture.completedFuture(F.Either.Right(createFlowForActor())); + } + + private CompletionStage>> + createActorFlow2(Http.RequestHeader request) { + return CompletableFuture.completedFuture( + request.session() + .getOptional("username") + .map(username -> + F.Either.>Right( + createFlowForActor())) + .orElseGet(() -> F.Either.Left(forbidden()))); + } + + private Flow createFlowForActor() { + return ActorFlow.actorRef(out -> Messenger.props(out), actorSystem, materializer); + } + + public WebSocket akkaStreamsSocket() { + return WebSocket.Json.accept( + request -> { + Sink in = Sink.foreach(System.out::println); + MessageDTO messageDTO = new MessageDTO("1", "1", "Title", "Test Body"); + Source out = Source.tick( + Duration.ofSeconds(2), + Duration.ofSeconds(2), + MessageConverter.messageToJsonNode(messageDTO) + ); + return Flow.fromSinkAndSource(in, out); + }); + } +} diff --git a/play-framework/websockets/app/dto/MessageDTO.java b/play-framework/websockets/app/dto/MessageDTO.java new file mode 100644 index 0000000000..e6b2bac1af --- /dev/null +++ b/play-framework/websockets/app/dto/MessageDTO.java @@ -0,0 +1,60 @@ +package dto; + +public class MessageDTO { + private String userId; + private String id; + private String title; + private String body; + + public MessageDTO() { + } + + public MessageDTO(String userId, String id, String title, String body) { + this.userId = userId; + this.id = id; + this.title = title; + this.body = body; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + @Override + public String toString() { + return "MessageDTO{" + + "userId='" + userId + '\'' + + ", id='" + id + '\'' + + ", title='" + title + '\'' + + ", body='" + body + '\'' + + '}'; + } +} diff --git a/play-framework/websockets/app/dto/RequestDTO.java b/play-framework/websockets/app/dto/RequestDTO.java new file mode 100644 index 0000000000..a85d3770a4 --- /dev/null +++ b/play-framework/websockets/app/dto/RequestDTO.java @@ -0,0 +1,27 @@ +package dto; + +public class RequestDTO { + private String message; + + public RequestDTO() { + } + + public RequestDTO(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return "RequestDTO{" + + "message='" + message + '\'' + + '}'; + } +} diff --git a/play-framework/websockets/app/utils/MessageConverter.java b/play-framework/websockets/app/utils/MessageConverter.java new file mode 100644 index 0000000000..85729cd1da --- /dev/null +++ b/play-framework/websockets/app/utils/MessageConverter.java @@ -0,0 +1,24 @@ +package utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import dto.MessageDTO; +import dto.RequestDTO; + +public class MessageConverter { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + public static MessageDTO jsonNodeToMessage(JsonNode jsonNode) { + return OBJECT_MAPPER.convertValue(jsonNode, MessageDTO.class); + } + + public static JsonNode messageToJsonNode(MessageDTO messageDTO) { + return OBJECT_MAPPER.convertValue(messageDTO, JsonNode.class); + } + public static RequestDTO jsonNodeToRequest(JsonNode jsonNode) { + return OBJECT_MAPPER.convertValue(jsonNode, RequestDTO.class); + } + + public static JsonNode requestToJsonNode(RequestDTO requestDTO) { + return OBJECT_MAPPER.convertValue(requestDTO, JsonNode.class); + } +} diff --git a/play-framework/websockets/app/views/index.scala.html b/play-framework/websockets/app/views/index.scala.html new file mode 100644 index 0000000000..b837fc6f74 --- /dev/null +++ b/play-framework/websockets/app/views/index.scala.html @@ -0,0 +1,97 @@ +@(url: String) +@main("Welcome to Play") { +

Welcome to Play WebSockets!

+
+
+ + +
+ + + + +} diff --git a/play-framework/websockets/app/views/main.scala.html b/play-framework/websockets/app/views/main.scala.html new file mode 100644 index 0000000000..be5dd8f09d --- /dev/null +++ b/play-framework/websockets/app/views/main.scala.html @@ -0,0 +1,14 @@ +@(title: String)(content: Html) + + + + + @title + + + + + @content + + + diff --git a/play-framework/websockets/build.sbt b/play-framework/websockets/build.sbt new file mode 100644 index 0000000000..a076daa4f0 --- /dev/null +++ b/play-framework/websockets/build.sbt @@ -0,0 +1,22 @@ +name := """websockets""" +organization := "com.baeldung" + +version := "1.0-SNAPSHOT" + +lazy val root = (project in file(".")).enablePlugins(PlayJava) + +scalaVersion := "2.13.0" + +lazy val akkaVersion = "2.6.0-M8" +lazy val akkaHttpVersion = "10.1.10" + +libraryDependencies += guice +libraryDependencies += "com.typesafe.akka" %% "akka-actor" % akkaVersion +libraryDependencies += "com.typesafe.akka" %% "akka-testkit" % akkaVersion +libraryDependencies += "com.typesafe.akka" %% "akka-stream" % akkaVersion +libraryDependencies += "com.typesafe.akka" %% "akka-http-jackson" % akkaHttpVersion +libraryDependencies += "com.typesafe.akka" %% "akka-http" % akkaHttpVersion +libraryDependencies += "org.projectlombok" % "lombok" % "1.18.8" % "provided" +libraryDependencies += "junit" % "junit" % "4.12" + +PlayKeys.devSettings += "play.server.http.idleTimeout" -> "infinite" diff --git a/play-framework/websockets/conf/application.conf b/play-framework/websockets/conf/application.conf new file mode 100644 index 0000000000..87cb978051 --- /dev/null +++ b/play-framework/websockets/conf/application.conf @@ -0,0 +1,7 @@ +# This is the main configuration file for the application. +# https://www.playframework.com/documentation/latest/ConfigFile +######################################## +# akka-http-core Reference Config File # +######################################## + +play.server.http.idleTimeout = "infinite" \ No newline at end of file diff --git a/play-framework/websockets/conf/logback.xml b/play-framework/websockets/conf/logback.xml new file mode 100644 index 0000000000..8efb66cda3 --- /dev/null +++ b/play-framework/websockets/conf/logback.xml @@ -0,0 +1,37 @@ + + + + + + + ${application.home:-.}/logs/application.log + + %date [%level] from %logger in %thread - %message%n%xException + + + + + + %coloredLevel %logger{15} - %message%n%xException{10} + + + + + + + + + + + + + + + + + + + + + + diff --git a/play-framework/websockets/conf/routes b/play-framework/websockets/conf/routes new file mode 100644 index 0000000000..674aba00bd --- /dev/null +++ b/play-framework/websockets/conf/routes @@ -0,0 +1,11 @@ +# Routes +# This file defines all application routes (Higher priority routes first) +# ~~~~ + +# An example controller showing a sample home page +GET / controllers.HomeController.index(request: Request) +GET /chat controllers.HomeController.socket +GET /chat/with/streams controllers.HomeController.akkaStreamsSocket + +# Map static resources from the /public folder to the /assets URL path +GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) diff --git a/play-framework/websockets/project/build.properties b/play-framework/websockets/project/build.properties new file mode 100644 index 0000000000..c0bab04941 --- /dev/null +++ b/play-framework/websockets/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.2.8 diff --git a/play-framework/websockets/project/plugins.sbt b/play-framework/websockets/project/plugins.sbt new file mode 100644 index 0000000000..1c8c62a0d5 --- /dev/null +++ b/play-framework/websockets/project/plugins.sbt @@ -0,0 +1,7 @@ +// The Play plugin +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.7.3") + +// Defines scaffolding (found under .g8 folder) +// http://www.foundweekends.org/giter8/scaffolding.html +// sbt "g8Scaffold form" +addSbtPlugin("org.foundweekends.giter8" % "sbt-giter8-scaffold" % "0.11.0") diff --git a/play-framework/websockets/public/images/favicon.png b/play-framework/websockets/public/images/favicon.png new file mode 100644 index 0000000000..c7d92d2ae4 Binary files /dev/null and b/play-framework/websockets/public/images/favicon.png differ diff --git a/play-framework/websockets/public/javascripts/main.js b/play-framework/websockets/public/javascripts/main.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/play-framework/websockets/public/stylesheets/main.css b/play-framework/websockets/public/stylesheets/main.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/play-framework/websockets/test/controllers/HomeControllerTest.java b/play-framework/websockets/test/controllers/HomeControllerTest.java new file mode 100644 index 0000000000..b006feab8c --- /dev/null +++ b/play-framework/websockets/test/controllers/HomeControllerTest.java @@ -0,0 +1,32 @@ +package controllers; + +import org.junit.Test; +import play.Application; +import play.inject.guice.GuiceApplicationBuilder; +import play.mvc.Http; +import play.mvc.Result; +import play.test.WithApplication; + +import static org.junit.Assert.assertEquals; +import static play.mvc.Http.Status.OK; +import static play.test.Helpers.GET; +import static play.test.Helpers.route; + +public class HomeControllerTest extends WithApplication { + + @Override + protected Application provideApplication() { + return new GuiceApplicationBuilder().build(); + } + + @Test + public void giveRequest_whenRootPath_ThenStatusOkay() { + Http.RequestBuilder request = new Http.RequestBuilder() + .method(GET) + .uri("/"); + + Result result = route(app, request); + assertEquals(OK, result.status()); + } + +}