diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..87e020b --- /dev/null +++ b/build.sbt @@ -0,0 +1,25 @@ +name := "vaadin-raffle" + +version := "0.1" + +scalaVersion := "2.12.4" + +resolvers ++= Seq( + "vaadin-addons" at "http://maven.vaadin.com/vaadin-addons" +) + +val vaadinVersion = "8.3.1" +val akkaVersion = "2.5.11" +libraryDependencies ++= Seq( + "org.vaadin.addons" % "vaactor" % "1.0.2", + "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided", + "com.vaadin" % "vaadin-server" % vaadinVersion, + "com.vaadin" % "vaadin-client-compiled" % vaadinVersion, + "com.vaadin" % "vaadin-themes" % vaadinVersion, + "com.vaadin" % "vaadin-push" % vaadinVersion, + "com.typesafe.akka" %% "akka-actor" % akkaVersion +) + +containerLibs in Jetty := Seq("org.eclipse.jetty" % "jetty-runner" % "9.3.21.v20170918" intransitive()) + +enablePlugins(JettyPlugin) diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..5517665 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version = 1.1.1 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..a99c689 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.0.2") diff --git a/src/main/scala/org/scala_vienna/raffle/RaffleComponent.scala b/src/main/scala/org/scala_vienna/raffle/RaffleComponent.scala new file mode 100644 index 0000000..7dd301a --- /dev/null +++ b/src/main/scala/org/scala_vienna/raffle/RaffleComponent.scala @@ -0,0 +1,102 @@ +package org.scala_vienna.raffle + +import org.scala_vienna.raffle.RaffleServer._ +import org.vaadin.addons.vaactor.Vaactor.VaactorComponent +import org.vaadin.addons.vaactor.VaactorUI +import com.vaadin.data.provider.{ DataProvider, ListDataProvider } +import com.vaadin.server.Sizeable +import com.vaadin.ui.themes.ValoTheme +import com.vaadin.ui._ +import scala.collection.JavaConverters._ + +class RaffleComponent(override val vaactorUI: VaactorUI, title: String) extends CustomComponent with VaactorComponent { + /** Contains list of chatroom menbers */ + val participantsList = new java.util.ArrayList[String]() + val participantsDataProvider: ListDataProvider[String] = DataProvider.ofCollection[String](participantsList) + val participantsPanel: ListSelect[String] = new ListSelect("Raffle Participants", participantsDataProvider) { + setWidth(100, Sizeable.Unit.PIXELS) + } + + /** Contains list of messages from chatroom */ + //val chatList = new java.util.ArrayList[ChatServer.Statement]() + //val chatDataProvider: ListDataProvider[ChatServer.Statement] = DataProvider.ofCollection[ChatServer.Statement](chatList) + //val chatPanel: Grid[ChatServer.Statement] = new Grid[ChatServer.Statement]("Chat", chatDataProvider) { + // addColumn(d => d.name) + // addColumn(d => d.msg) + // setWidth(400, Sizeable.Unit.PIXELS) + //} + + /** Contains username */ + val userName = new TextField() + + val loginPanel: HorizontalLayout = new HorizontalLayout { + setSpacing(true) + addComponents( + userName, + new Button("Enter Raffle", _ => { RaffleServer.raffleServer ! Subscribe(Client(userName.getValue, self)) }) + ) + } + + + /** Contains user interface for login/logout and sending of messages */ + val userPanel = new Panel( + new VerticalLayout { + setSpacing(true) + setMargin(true) + addComponents( + new HorizontalLayout { + setSpacing(true) + addComponents(loginPanel) + }) + } + ) + + val startButton = new Button("Start", _ => { RaffleServer.raffleServer ! StartRaffle }) + + val winnerLabel: Label = new Label { + setValue("Winner: ") + addStyleName(ValoTheme.LABEL_H2) + } + + startButton.setVisible(false) + + setCompositionRoot(new VerticalLayout { + + addComponents( + new Label { + setValue(title) + addStyleName(ValoTheme.LABEL_H1) + }, + new HorizontalLayout { + setSpacing(true) + addComponents( + new VerticalLayout { + setSpacing(true) + addComponents(userPanel) + }, + participantsPanel) + }, + winnerLabel, + startButton) + }) + + + /** Receive function, is called in context of VaadinUI (via ui.access) */ + override def receive: PartialFunction[Any, Unit] = { + // User entered chatroom, update member list + case Enter(participants) => + + participantsList.clear() + participantsList.addAll(participants.asJava) + participantsDataProvider.refreshAll() + + case SubscriptionFailure(error) => + Notification.show(error, Notification.Type.WARNING_MESSAGE) + + case YouAreCoordinator => startButton.setVisible(true) + + case Result(name) => { + winnerLabel.setValue(s"Winner: $name") + } + } +} diff --git a/src/main/scala/org/scala_vienna/raffle/RaffleServer.scala b/src/main/scala/org/scala_vienna/raffle/RaffleServer.scala new file mode 100644 index 0000000..1337025 --- /dev/null +++ b/src/main/scala/org/scala_vienna/raffle/RaffleServer.scala @@ -0,0 +1,88 @@ +package org.scala_vienna.raffle + +import org.vaadin.addons.vaactor.VaactorServlet + +import akka.actor.{ Actor, ActorRef, Props } + +import scala.util.Random + +object RaffleServer { + + /** Clients handled by chat room + * + * @param name name of user + * @param actor actorref for communication + */ + case class Client(name: String, actor: ActorRef) + + /** Subscribe client to chatroom, processed by chatroom + * + * @param client enters chatroom + */ + case class Subscribe(client: Client) + + case class SubscriptionSuccess(name: String) + + case class SubscriptionFailure(error: String) + + case class SubscriptionCancelled(name: String) + + case class Participants(names: Seq[String]) + + case class Enter(participants: List[String]) + + case object YouAreCoordinator + + case object StartRaffle + + case class Result(name: String) + + + case object RequestMembers + + /** ActoRef of chatroom actor */ + val raffleServer: ActorRef = VaactorServlet.system.actorOf(Props[ServerActor], "raffleServer") + + /** Actor handling chatroom */ + class ServerActor extends Actor { + + // List of participants + private var participants = Map.empty[String, Client] + + /** Process received messages */ + def receive: Receive = { + // Subscribe from client + case Subscribe(client) => + // no name, reply with failure + if (client.name.isEmpty) + sender ! SubscriptionFailure("Empty name not valid") + // duplicate name, reply with failure + else if (participants.contains(client.name)) + sender ! SubscriptionFailure(s"Name '${ client.name }' already subscribed") + // add client to chatroom, reply with success, brodcast Enter to clients + else { + if (client.name == "stevan") { + sender ! YouAreCoordinator + } + participants += client.name -> client + //sender ! SubscriptionSuccess(client.name) + broadcast(Enter(participants.keys.toList)) + } + case StartRaffle => { + val winner = Random.nextInt(participants.size) + broadcast(Result(participants.keys.toSeq(winner))) + } + // RequestMembers from client, send member list to sending client + //case RequestMembers => + // sender ! Participants(participants.keySet.toList) + } + + /** Send message to every client in chatroom + * + * @param msg message + */ + def broadcast(msg: Any): Unit = participants foreach { _._2.actor ! msg } + + } + +} diff --git a/src/main/scala/org/scala_vienna/raffle/RaffleServlet.scala b/src/main/scala/org/scala_vienna/raffle/RaffleServlet.scala new file mode 100644 index 0000000..f994203 --- /dev/null +++ b/src/main/scala/org/scala_vienna/raffle/RaffleServlet.scala @@ -0,0 +1,19 @@ +package org.scala_vienna.raffle + +import javax.servlet.annotation.WebServlet + +import org.vaadin.addons.vaactor.VaactorServlet +import com.vaadin.annotations.VaadinServletConfiguration + +/** Define servlet, url pattern and ui-class to start + * + */ +@WebServlet( + urlPatterns = Array("/*"), + asyncSupported = true +) +@VaadinServletConfiguration( + productionMode = false, + ui = classOf[RaffleUI] +) +class RaffleServlet extends VaactorServlet diff --git a/src/main/scala/org/scala_vienna/raffle/RaffleUI.scala b/src/main/scala/org/scala_vienna/raffle/RaffleUI.scala new file mode 100644 index 0000000..37c378f --- /dev/null +++ b/src/main/scala/org/scala_vienna/raffle/RaffleUI.scala @@ -0,0 +1,18 @@ +package org.scala_vienna.raffle + +import org.vaadin.addons.vaactor.VaactorUI +import com.vaadin.annotations.Push +import com.vaadin.server.VaadinRequest +import com.vaadin.shared.communication.PushMode +import com.vaadin.shared.ui.ui.Transport + +@Push( + value = PushMode.AUTOMATIC, + transport = Transport.WEBSOCKET +) +class RaffleUI extends VaactorUI { + + override def init(request: VaadinRequest): Unit = + setContent(new RaffleComponent(this, "Vaactor raffle")) + +}