Skip to content

Instantly share code, notes, and snippets.

@leovegas
Created October 4, 2020 20:45
Show Gist options
  • Save leovegas/6230033810d4ca8f3d724556169b3527 to your computer and use it in GitHub Desktop.
Save leovegas/6230033810d4ca8f3d724556169b3527 to your computer and use it in GitHub Desktop.
Simple Web Chat ver 1
package actors
import actors.ChatActor.{SendMessage, Tick}
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import tasks.KeepAliveTask
class ChatActor(out: ActorRef, manager: ActorRef) extends Actor{
manager ! ChatManager.NewChatter(self)
override def receive: Receive = {
case s: String => manager ! ChatManager.Message(s)
case SendMessage(msg) => out ! msg
case Tick => println("tick")
}
}
object ChatActor {
def props(out: ActorRef,manager: ActorRef) = Props(new ChatActor(out, manager))
case class SendMessage(msg: String)
object Tick
}
package actors
import actors.ChatActor.Tick
import actors.ChatManager.{LogoutMessage, Message, NewChatter}
import akka.actor.{Actor, ActorRef}
class ChatManager extends Actor{
private var chatters = List.empty[ActorRef]
override def receive: Receive = {
case "tick009" => for (c <- chatters) c ! "tick009"
case NewChatter(chatter) => chatters ::= chatter
case Message(msg) => for (c <- chatters) c ! ChatActor.SendMessage(msg)
case LogoutMessage(username) => for (c <- chatters) c ! ChatActor.SendMessage("Logged out user: "+username)
}
}
object ChatManager {
case class LogoutMessage(username: String)
case class NewChatter(chatter: ActorRef)
case class Message(msg: String)
}
package actors
import actors.TickActor.{SendMessage, Tick}
import akka.actor.{Actor, ActorRef, Props}
class TickActor(out: ActorRef, manager: ActorRef) extends Actor{
manager ! ChatManager.NewChatter(self)
override def receive: Receive = {
case s: String => manager ! ChatManager.Message(s)
case SendMessage(msg) => out ! msg
case Tick => out ! "tick"
}
}
object TickActor {
def props(out: ActorRef,manager: ActorRef) = Props(new TickActor(out, manager))
case class SendMessage(msg: String)
object Tick
}
package controllers
import javax.inject._
import akka.actor.ActorSystem
import play.api.mvc._
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future, Promise}
/**
* This controller creates an `Action` that demonstrates how to write
* simple asynchronous code in a controller. It uses a timer to
* asynchronously delay sending a response for 1 second.
*
* @param cc standard controller components
* @param actorSystem We need the `ActorSystem`'s `Scheduler` to
* run code after a delay.
* @param exec We need an `ExecutionContext` to execute our
* asynchronous code. When rendering content, you should use Play's
* default execution context, which is dependency injected. If you are
* using blocking operations, such as database or network access, then you should
* use a different custom execution context that has a thread pool configured for
* a blocking API.
*/
@Singleton
class AsyncController @Inject()(cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends AbstractController(cc) {
/**
* Creates an Action that returns a plain text message after a delay
* of 1 second.
*
* The configuration in the `routes` file means that this method
* will be called when the application receives a `GET` request with
* a path of `/message`.
*/
def message = Action.async {
getFutureMessage(1.second).map { msg => Ok(msg) }
}
private def getFutureMessage(delayTime: FiniteDuration): Future[String] = {
val promise: Promise[String] = Promise[String]()
actorSystem.scheduler.scheduleOnce(delayTime) {
promise.success("Hi!")
}(actorSystem.dispatcher) // run scheduled tasks using the actor system's dispatcher
promise.future
}
}
package controllers
import javax.inject._
import play.api.mvc._
import services.Counter
/**
* This controller demonstrates how to use dependency injection to
* bind a component into a controller class. The class creates an
* `Action` that shows an incrementing count to users. The [[Counter]]
* object is injected by the Guice dependency injection system.
*/
@Singleton
class CountController @Inject() (cc: ControllerComponents,
counter: Counter) extends AbstractController(cc) {
/**
* Create an action that responds with the [[Counter]]'s current
* count. The result is plain text. This `Action` is mapped to
* `GET /count` requests by an entry in the `routes` config file.
*/
def count = Action { Ok(counter.nextCount().toString) }
}
package controllers
import javax.inject._
import play.api.mvc._
/**
* This controller creates an `Action` to handle HTTP requests to the
* application's home page.
*/
@Singleton
class HomeController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
/**
* Create an Action to render an HTML page with a welcome message.
* The configuration in the `routes` file means that this method
* will be called when the application receives a `GET` request with
* a path of `/`.
*/
def index = Action {
Ok(views.html.index("Your new application is ready."))
}
}
package controllers
import javax.inject.{Inject, Singleton}
import models.{TaskListInMemoryModel, UserData}
import play.api.libs.json.{JsError, JsSuccess, Json, Reads}
import play.api.mvc.{AbstractController, AnyContent, ControllerComponents, Request, Result}
@Singleton
class TaskList3 @Inject()(cc: ControllerComponents) extends AbstractController(cc) with play.api.i18n.I18nSupport {
implicit val UserDataReads: Reads[UserData] = Json.reads[UserData]
def load = Action { implicit request =>
Ok(views.html.version4Main())
}
def withJsonBody[A](f: A => Result)(implicit request: Request[AnyContent], reads: Reads[A]) = {
request.body.asJson.map { body =>
Json.fromJson[A](body) match {
case JsSuccess(a, path) => f(a)
case e @ JsError(_) => Redirect(routes.TaskList3.load())
}
}.getOrElse(Redirect(routes.TaskList3.load()))
}
def withSessionUsername(f: String => Result)(implicit request: Request[AnyContent]) = {
request.session.get("username").map(f).getOrElse(Ok(Json.toJson(Seq.empty[String])))
}
def validate = Action { implicit request =>
request.body.asJson.map { body=>
Json.fromJson[UserData](body) match {
case JsSuccess(ud, path) =>
val username = ud.username
val password = ud.password
if (TaskListInMemoryModel.validateUser(username, password)) {
Ok(Json.toJson(true))
.withSession("username" -> username, "csrfToken" -> play.filters.csrf.CSRF.getToken.get.value)
}else {
Ok(Json.toJson(false))
}
case e @ JsError(_) => Redirect(routes.TaskList3.load())
}
}.getOrElse(Redirect(routes.TaskList3.load()))
}
def createUser = Action { implicit request =>
request.body.asJson.map { body=>
Json.fromJson[UserData](body) match {
case JsSuccess(ud, path) =>
val username = ud.username
val password = ud.password
if (TaskListInMemoryModel.createUser(username, password)) {
Ok(Json.toJson(true))
.withSession("username" -> username, "csrfToken" -> play.filters.csrf.CSRF.getToken.get.value)
}else {
Ok(Json.toJson(false))
}
case e @ JsError(_) => Redirect(routes.TaskList3.load())
}
}.getOrElse(Redirect(routes.TaskList3.load()))
}
def taskList = Action {implicit request =>
val usernameOption = request.session.get("username")
usernameOption.map { username =>
Ok(Json.toJson(TaskListInMemoryModel.getTasks(username)))
}.getOrElse(Ok(Json.toJson(Seq.empty[String])))
}
def addTask = Action { implicit request =>
val usernameOption = request.session.get("username")
usernameOption.map { username =>
request.body.asJson.map { body =>
Json.fromJson[String](body) match {
case JsSuccess(task, path) => {
TaskListInMemoryModel.addTask(username, task);
Ok(Json.toJson(true))
}
case e @ JsError(_) => Redirect(routes.TaskList3.load())
}
}.getOrElse(Ok(Json.toJson(false)))
}.getOrElse(Ok(Json.toJson(false)))
}
def delete = Action { implicit request =>
val usernameOption = request.session.get("username")
usernameOption.map { username =>
request.body.asJson.map { body =>
Json.fromJson[Int](body) match {
case JsSuccess(taskNumber, path) => {
TaskListInMemoryModel.removeTask(username, taskNumber);
Ok(Json.toJson(true))
}
case e @ JsError(_) => Redirect(routes.TaskList3.load())
}
}.getOrElse(Ok(Json.toJson(false)))
}.getOrElse(Ok(Json.toJson(false)))
}
def logout = Action {
Redirect(routes.TaskList3.load()).withNewSession
}
}
package controllers
import javax.inject.{Inject, Singleton}
import models.{TaskListInMemoryModel, UserData}
import play.api.libs.json.{JsError, JsSuccess, Json, Reads}
import play.api.mvc.{AbstractController, ControllerComponents}
@Singleton
class TaskList4 @Inject()(cc: ControllerComponents) extends AbstractController(cc) with play.api.i18n.I18nSupport {
def load = Action { implicit request =>
Ok(views.html.version5Main())
}
}
package controllers
import actors.ChatManager
import actors.ChatManager.LogoutMessage
import akka.actor.{ActorSystem, Props}
import akka.stream.Materializer
import javax.inject.{Inject, Singleton}
import models.{TaskItem, TaskListDatabaseModel, UserData}
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
import play.api.libs.json.{JsError, JsSuccess, Json, Reads, Writes}
import play.api.mvc.{AbstractController, AnyContent, ControllerComponents, Request, Result}
import play.mvc.Action
import slick.jdbc.JdbcProfile
import slick.jdbc.PostgresProfile.api._
import scala.concurrent.{ExecutionContext, Future}
@Singleton
class TaskList5 @Inject()(protected val dbConfigProvider: DatabaseConfigProvider, cc: ControllerComponents)(implicit ec: ExecutionContext)
extends AbstractController(cc) with play.api.i18n.I18nSupport with HasDatabaseConfigProvider[JdbcProfile] {
private val model = new TaskListDatabaseModel(db)
def load = Action { implicit request =>
Ok(views.html.version6Main())
}
implicit val UserDataReads: Reads[UserData] = Json.reads[UserData]
implicit val itemDataWrites: Writes[TaskItem] = Json.writes[TaskItem]
def withJsonBody[A](f: A => Future[Result])(implicit request: Request[AnyContent], reads: Reads[A]): Future[Result] = {
request.body.asJson.map { body =>
Json.fromJson[A](body) match {
case JsSuccess(a, path) => f(a)
case e@JsError(_) => Future.successful(Redirect(routes.TaskList5.load()))
}
}.getOrElse(Future.successful(Redirect(routes.TaskList5.load())))
}
def withSessionUsername(f: String => Future[Result])(implicit request: Request[AnyContent]): Future[Result] = {
request.session.get("username").map(f).getOrElse(Future.successful(Ok(Json.toJson(Seq.empty[String]))))
}
def withSessionUserid(f: Int => Future[Result])(implicit request: Request[AnyContent]): Future[Result] = {
request.session.get("userid").map(userid => f(userid.toInt)).getOrElse(Future.successful(Ok(Json.toJson(Seq.empty[String]))))
}
def validate = Action.async { implicit request =>
withJsonBody[UserData] { ud =>
model.validateUser(ud.username, ud.password, true).map {
case Some(userid) =>
Ok(Json.toJson(true))
.withSession("username" -> ud.username, "userid" -> userid.toString, "csrfToken" -> play.filters.csrf.CSRF.getToken.map(_.value).getOrElse(""))
case None =>
Ok(Json.toJson(false))
}
}
}
def createUser = Action.async { implicit request =>
withJsonBody[UserData] { ud =>
model.createUser(ud.username, ud.password, false).map {
case Some(userid) =>
Ok(Json.toJson(true))
.withSession("username" -> ud.username, "userid" -> userid.toString, "csrfToken" -> play.filters.csrf.CSRF.getToken.map(_.value).getOrElse(""))
case None =>
Ok(Json.toJson(false))
}
}
}
def taskList = Action.async { implicit request =>
withSessionUsername { username =>
model.getTasks(username).map(tasks => Ok(Json.toJson(tasks)))
}
}
def addTask = Action.async { implicit request =>
withSessionUserid { userid =>
withJsonBody[String] { task =>
model.addTask(userid, task).map(count => Ok(Json.toJson(count > 0)))
}
}
}
def delete = Action.async { implicit request =>
withSessionUsername { username =>
withJsonBody[Int] { itemId =>
model.removeTask(itemId).map(removed => Ok(Json.toJson(removed)))
}
}
}
//
// def logout = Action { implicit request =>
// Ok(Json.toJson(true)).withSession(request.session - "username")
// }
def logout = Action.async { implicit request =>
withSessionUsername {username =>
model.logoutUpdateStatus(username, false).map {
case Some(_) =>
Redirect(routes.TaskList3.load()).withSession(request.session - "username")
case None =>
Ok("")
}
}
}
}
package controllers
import java.util.concurrent.atomic.AtomicInteger
import actors.ChatManager.LogoutMessage
import actors.{ChatActor, ChatManager, TickActor}
import akka.actor.{ActorSystem, Props}
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.ws.{Message, WebSocketRequest}
import akka.http.scaladsl.settings.ClientConnectionSettings
import akka.stream.Materializer
import akka.stream.scaladsl.{Flow, Sink, Source}
import akka.util.ByteString
import javax.inject.{Inject, Singleton}
import models.{TaskListInMemoryModel, UserData}
import play.api.libs.json.{JsError, JsSuccess, Json, Reads}
import play.api.libs.streams.ActorFlow
import play.api.mvc.{AbstractController, ControllerComponents, WebSocket}
import tasks.KeepAliveTask
import scala.concurrent.duration._
@Singleton
class WebSocketChat @Inject()(cc: ControllerComponents)(implicit system:ActorSystem, mat: Materializer) extends AbstractController(cc) with play.api.i18n.I18nSupport {
var currentUsername = "";
import system.dispatcher
val manager = system.actorOf(Props[ChatManager], "Manager")
val keepAlive = new KeepAliveTask(system, manager)
def index(username: String) = Action { implicit request =>
currentUsername = username
Ok(views.html.chatPage(username))
}
def logoutMessage() = Action { implicit request =>
manager ! LogoutMessage(currentUsername)
Redirect(routes.TaskList5.logout())
}
def socket() = WebSocket.accept[String,String] { implicit request =>
ActorFlow.actorRef { out =>
ChatActor.props(out, manager)
}
}
}
package filters
import akka.stream.Materializer
import javax.inject._
import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future}
/**
* This is a simple filter that adds a header to all requests. It's
* added to the application's list of filters by the
* [[Filters]] class.
*
* @param mat This object is needed to handle streaming of requests
* and responses.
* @param exec This class is needed to execute code asynchronously.
* It is used below by the `map` method.
*/
@Singleton
class ExampleFilter @Inject()(
implicit override val mat: Materializer,
exec: ExecutionContext) extends Filter {
override def apply(nextFilter: RequestHeader => Future[Result])
(requestHeader: RequestHeader): Future[Result] = {
// Run the next filter in the chain. This will call other filters
// and eventually call the action. Take the result and modify it
// by adding a new header.
nextFilter(requestHeader).map { result =>
result.withHeaders("X-ExampleFilter" -> "foo")
}
}
}
package models
object CodeGen extends App {
slick.codegen.SourceCodeGenerator.run(
"slick.jdbc.PostgresProfile",
"org.postgresql.Driver",
"jdbc:postgresql://",
"/home/leonid/Programs/SimpleWebChat/app",
"models", None, None, true, false
)
}
package models
// AUTO-GENERATED Slick data model
import slick.jdbc.{ MySQLProfile => profile }
/** Stand-alone Slick data model for immediate use */
object Tables extends Tables {
val profile = slick.jdbc.PostgresProfile
}
/** Slick data model trait for extension, choice of backend or usage in the cake pattern. (Make sure to initialize this late.) */
trait Tables {
val profile: slick.jdbc.JdbcProfile
import profile.api._
import slick.model.ForeignKeyAction
// NOTE: GetResult mappers for plain SQL are only generated for tables where Slick knows how to map the types of all columns.
import slick.jdbc.{GetResult => GR}
/** DDL for all tables. Call .create to execute. */
lazy val schema: profile.SchemaDescription = Items.schema ++ Users.schema
@deprecated("Use .schema instead of .ddl", "3.0")
def ddl = schema
/** Entity class storing rows of table Items
* @param itemId Database column item_id SqlType(serial), AutoInc, PrimaryKey
* @param userId Database column user_id SqlType(int4)
* @param text Database column text SqlType(varchar), Length(2000,true) */
case class ItemsRow(itemId: Int, userId: Int, text: String)
/** GetResult implicit for fetching ItemsRow objects using plain SQL queries */
implicit def GetResultItemsRow(implicit e0: GR[Int], e1: GR[String]): GR[ItemsRow] = GR{
prs => import prs._
ItemsRow.tupled((<<[Int], <<[Int], <<[String]))
}
/** Table description of table items. Objects of this class serve as prototypes for rows in queries. */
class Items(_tableTag: Tag) extends profile.api.Table[ItemsRow](_tableTag, "items") {
def * = (itemId, userId, text) <> (ItemsRow.tupled, ItemsRow.unapply)
/** Maps whole row to an option. Useful for outer joins. */
def ? = ((Rep.Some(itemId), Rep.Some(userId), Rep.Some(text))).shaped.<>({r=>import r._; _1.map(_=> ItemsRow.tupled((_1.get, _2.get, _3.get)))}, (_:Any) => throw new Exception("Inserting into ? projection not supported."))
/** Database column item_id SqlType(serial), AutoInc, PrimaryKey */
val itemId: Rep[Int] = column[Int]("item_id", O.AutoInc, O.PrimaryKey)
/** Database column user_id SqlType(int4) */
val userId: Rep[Int] = column[Int]("user_id")
/** Database column text SqlType(varchar), Length(2000,true) */
val text: Rep[String] = column[String]("text", O.Length(2000,varying=true))
/** Foreign key referencing Users (database name items_user_id_fkey) */
lazy val usersFk = foreignKey("items_user_id_fkey", userId, Users)(r => r.id, onUpdate=ForeignKeyAction.NoAction, onDelete=ForeignKeyAction.Cascade)
}
/** Collection-like TableQuery object for table Items */
lazy val Items = new TableQuery(tag => new Items(tag))
/** Entity class storing rows of table Users
* @param id Database column id SqlType(serial), AutoInc, PrimaryKey
* @param username Database column username SqlType(varchar), Length(20,true)
* @param password Database column password SqlType(varchar), Length(200,true)
* @param online Database column online SqlType(bool) */
case class UsersRow(id: Int, username: String, password: String, online: Boolean)
/** GetResult implicit for fetching UsersRow objects using plain SQL queries */
implicit def GetResultUsersRow(implicit e0: GR[Int], e1: GR[String], e2: GR[Boolean]): GR[UsersRow] = GR{
prs => import prs._
UsersRow.tupled((<<[Int], <<[String], <<[String], <<[Boolean]))
}
/** Table description of table users. Objects of this class serve as prototypes for rows in queries. */
class Users(_tableTag: Tag) extends profile.api.Table[UsersRow](_tableTag, "users") {
def * = (id, username, password, online) <> (UsersRow.tupled, UsersRow.unapply)
/** Maps whole row to an option. Useful for outer joins. */
def ? = ((Rep.Some(id), Rep.Some(username), Rep.Some(password), Rep.Some(online))).shaped.<>({r=>import r._; _1.map(_=> UsersRow.tupled((_1.get, _2.get, _3.get, _4.get)))}, (_:Any) => throw new Exception("Inserting into ? projection not supported."))
/** Database column id SqlType(serial), AutoInc, PrimaryKey */
val id: Rep[Int] = column[Int]("id", O.AutoInc, O.PrimaryKey)
/** Database column username SqlType(varchar), Length(20,true) */
val username: Rep[String] = column[String]("username", O.Length(20,varying=true))
/** Database column password SqlType(varchar), Length(200,true) */
val password: Rep[String] = column[String]("password", O.Length(200,varying=true))
/** Database column online SqlType(bool) */
val online: Rep[Boolean] = column[Boolean]("online")
}
/** Collection-like TableQuery object for table Users */
lazy val Users = new TableQuery(tag => new Users(tag))
}
package models
case class TaskItem(id:Int, text:String) {
}
package models
import scala.collection.mutable
import slick.jdbc.PostgresProfile.api._
import scala.concurrent.{ExecutionContext, Future}
import models.Tables._
import org.mindrot.jbcrypt.BCrypt
class TaskListDatabaseModel(db: Database)(implicit ec: ExecutionContext) {
def validateUser(username: String, password: String, status: Boolean): Future[Option[Int]] = {
db.run(
(for {
user <- Users if user.username === username
} yield {
user
}).map(_.online).update(status))
val matches = db.run(Users.filter(userRow => userRow.username === username).result)
matches.map(userRows => userRows.headOption.flatMap {
userRow => if (BCrypt.checkpw(password, userRow.password)) Some(userRow.id) else None
})
}
def createUser(username: String, password: String, status: Boolean): Future[Option[Int]] = {
val matches = db.run(Users.filter(userRow => userRow.username === username).result)
matches.flatMap { userRows =>
if (userRows.isEmpty) {
db.run(Users += UsersRow(-1, username, BCrypt.hashpw(password, BCrypt.gensalt()), status))
.flatMap { addCount =>
if (addCount > 0) db.run(Users.filter(userRow => userRow.username === username).result)
.map(_.headOption.map(_.id))
else Future.successful(None)
}
} else Future.successful(None)
}
}
def getTasks(username: String): Future[Seq[TaskItem]] = {
db.run(
(for {
user <- Users if user.username === username
item <- Items if item.userId === user.id
} yield {
// TaskItem(item.itemId, item.text)
item
}).result
).map(_.map(itemRow => TaskItem(itemRow.itemId, itemRow.text)))
}
def addTask(userid: Int, task: String): Future[Int] = {
db.run(Items += ItemsRow(-1, userid, task))
}
def removeTask(itemId: Int): Future[Boolean] = {
db.run(Items.filter(_.itemId === itemId).delete).map(count => count > 0)
}
def logoutUpdateStatus(username: String, status: Boolean): Future[Option[Int]] = {
db.run(
(for {
user <- Users if user.username === username
} yield {
user
}).map(_.online).update(status)
).flatMap { addCount =>
if (addCount > 0) db.run(Users.filter(userRow => userRow.username === username).result)
.map(_.headOption.map(_.id))
else Future.successful(None)
}
}
}
package models
import scala.collection.mutable
object TaskListInMemoryModel {
private val users = mutable.Map[String, String]("Mark" -> "pass")
private val tasks = mutable.Map[String, List[String]]("Mark" -> List("Make videos", "eat","sleep","code"))
def validateUser(username: String, password: String): Boolean = {
users.get(username).map(_ == password).getOrElse(false)
}
def createUser(username: String, password: String): Boolean = {
if (users.contains(username)) false else {
users(username) = password
true
}
}
def getTasks(username: String): Seq[String] = {
tasks.get(username).getOrElse(Nil)
}
def addTask(username: String, task: String): Unit = {
tasks(username) = task :: tasks.get(username).getOrElse(Nil)
}
def removeTask(username: String, index: Int): Boolean = {
if (index < 0 || tasks.get(username).isEmpty || index >= tasks(username).length) false
else {
tasks(username) = tasks(username).patch(index, Nil, 1)
true
}
}
}
package models
case class UserData(username:String, password:String, status: Boolean) {
}
import com.google.inject.AbstractModule
import java.time.Clock
import services.{ApplicationTimer, AtomicCounter, Counter}
/**
* This class is a Guice module that tells Guice how to bind several
* different types. This Guice module is created when the Play
* application starts.
* Play will automatically use any class called `Module` that is in
* the root package. You can create modules in other locations by
* adding `play.modules.enabled` settings to the `application.conf`
* configuration file.
*/
class Module extends AbstractModule {
override def configure() = {
// Use the system clock as the default implementation of Clock
bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)
// Ask Guice to create an instance of ApplicationTimer when the
// application starts.
bind(classOf[ApplicationTimer]).asEagerSingleton()
// Set AtomicCounter as the implementation for Counter.
bind(classOf[Counter]).to(classOf[AtomicCounter])
}
}
package services
import java.time.{Clock, Instant}
import javax.inject._
import play.api.Logger
import play.api.inject.ApplicationLifecycle
import scala.concurrent.Future
/**
* This class demonstrates how to run code when the
* application starts and stops. It starts a timer when the
* application starts. When the application stops it prints out how
* long the application was running for.
*
* This class is registered for Guice dependency injection in the
* [[Module]] class. We want the class to start when the application
* starts, so it is registered as an "eager singleton". See the code
* in the [[Module]] class to see how this happens.
*
* This class needs to run code when the server stops. It uses the
* application's [[ApplicationLifecycle]] to register a stop hook.
*/
@Singleton
class ApplicationTimer @Inject() (clock: Clock, appLifecycle: ApplicationLifecycle) {
private val logger: Logger = Logger(this.getClass)
// This code is called when the application starts.
private val start: Instant = clock.instant
logger.info(s"ApplicationTimer demo: Starting application at $start.")
// When the application starts, register a stop hook with the
// ApplicationLifecycle object. The code inside the stop hook will
// be run when the application stops.
appLifecycle.addStopHook { () =>
val stop: Instant = clock.instant
val runningTime: Long = stop.getEpochSecond - start.getEpochSecond
logger.info(s"ApplicationTimer demo: Stopping application at ${clock.instant} after ${runningTime}s.")
Future.successful(())
}
}
package services
import java.util.concurrent.atomic.AtomicInteger
import javax.inject._
/**
* This trait demonstrates how to create a component that is injected
* into a controller. The trait represents a counter that returns a
* incremented number each time it is called.
*/
trait Counter {
def nextCount(): Int
}
/**
* This class is a concrete implementation of the [[Counter]] trait.
* It is configured for Guice dependency injection in the [[Module]]
* class.
*
* This class has a `Singleton` annotation because we need to make
* sure we only use one counter per application. Without this
* annotation we would get a new instance every time a [[Counter]] is
* injected.
*/
@Singleton
class AtomicCounter extends Counter {
private val atomicCounter = new AtomicInteger()
override def nextCount(): Int = atomicCounter.getAndIncrement()
}
package tasks
import akka.actor.{ActorRef, ActorSystem}
import javax.inject.{Inject, Named}
import scala.concurrent.ExecutionContext
import scala.concurrent.duration._
class KeepAliveTask @Inject() (actorSystem: ActorSystem, @Named("some-actor") someActor: ActorRef)(
implicit executionContext: ExecutionContext
) {
actorSystem.scheduler.scheduleAtFixedRate(
initialDelay = 0.microseconds,
interval = 5.seconds,
receiver = someActor,
message = "tick009"
)
}
@(v:String)(implicit requestHeader: RequestHeader, flash: Flash)
@main("WebSocket Chat") {
<!DOCTYPE html>
<html lang="en">
<head>
<title>Login Activity Control API</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--===============================================================================================-->
<link rel="icon" type="@routes.Assets.versioned("login_v1/image/png")" href="@routes.Assets.versioned("login_v1/images/icons/favicon.ico")"/>
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/vendor/bootstrap/css/bootstrap.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/fonts/font-awesome-4.7.0/css/font-awesome.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/fonts/Linearicons-Free-v1.0.0/icon-font.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/vendor/animate/animate.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/vendor/css-hamburgers/hamburgers.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/vendor/select2/select2.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/css/util.css")">
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/css/main.css")">
<!--===============================================================================================-->
<!--===============================================================================================-->
</head>
<body>
<input type="hidden" id="csrfToken" value="@helper.CSRF.getToken.value">
<input type="hidden" id="validateRoute" value="@routes.TaskList3.validate()">
<input type="hidden" id="tasksRoute" value="@routes.TaskList3.taskList()">
<input type="hidden" id="createRoute" value="@routes.TaskList3.createUser()">
<input type="hidden" id="deleteRoute" value="@routes.TaskList3.delete()">
<input type="hidden" id="addRoute" value="@routes.TaskList3.addTask()">
<input type="hidden" id="ws-route" value="@routes.WebSocketChat.socket.absoluteURL()">
<input type="hidden" id="chat-route" value="@routes.WebSocketChat.index(v)">
<input type="hidden" id="username" value=@v>
<div class="limiter">
<div class="container-login100" style="background-image: url('images/img-01.jpg');">
<div class="wrap-login100 p-t-130 p-b-30">
@* <form class="login100-form validate-form">*@
<div class="login100-form-avatar">
<img src="@routes.Assets.versioned("login_v1/images/icons/fire.png")" alt="AVATAR">
</div>
<span class="login100-form-title p-t-20 p-b-45">
Simple Chat
</span>
<div class="wrap-input100 validate-input m-b-10">
<textarea id="chat-area" class="input101" rows="15" cols="200"></textarea>
<span class="focus-input100"></span>
<span class="symbol-input100">
</span>
</div>
<div class="wrap-input100 validate-input m-b-10" data-validate = "Username is required">
<input id="chat-input" class="input100" size="150" type="text" placeholder="Text">
<span class="focus-input100"></span>
<span class="symbol-input100">
</span>
</div>
<div class="container-login100-form-btn p-t-10">
<button id="sendButton" onclick="login()" class="login100-form-btn">
Send
</button>
</div>
<div class="text-center w-full p-t-25 p-b-230">
<div class="text-center w-full">
<a href="@routes.WebSocketChat.logoutMessage()">Logout</a>
</div>
</div>
</div>
</div>
</div>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("login_v1/vendor/jquery/jquery-3.2.1.min.js")"></script>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("login_v1/vendor/bootstrap/js/popper.js")"></script>
<script src="@routes.Assets.versioned("login_v1/vendor/bootstrap/js/bootstrap.min.js")"></script>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("login_v1/vendor/select2/select2.min.js")"></script>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("login_v1/js/main.js")"></script>
<script src="@routes.Assets.versioned("javascripts/version3.js")"></script>
<script src="@routes.Assets.versioned("javascripts/chat.js")"></script>
</body>
</html>
}
@*
* This template takes a single argument, a String containing a
* message to display.
*@
@(message: String)
@*
* Call the `main` template with two arguments. The first
* argument is a `String` with the title of the page, the second
* argument is an `Html` object containing the body of the page.
*@
@main("Welcome to Play") {
@*
* Get an `Html` object by calling the built-in Play welcome
* template and passing a `String` message.
*@
@welcome(message, style = "scala")
}
@*
* This template is called from the `index` template. This template
* handles the rendering of the page header and body tags. It takes
* two arguments, a `String` for the title of the page and an `Html`
* object to insert into the body of the page.
*@
@(title: String)(content: Html)
<!DOCTYPE html>
<html lang="en">
<head>
@* Here's where we render the page title `String`. *@
<title>@title</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
<script src="@routes.Assets.versioned("javascripts/hello.js")" type="text/javascript"></script>
</head>
<body>
@* And here's where we render the `Html` object containing
* the page content. *@
@content
</body>
</html>
@()(implicit request: RequestHeader, flash: Flash)
@main("Task List (Version 3)") {
<div id="contents"></div>
@* ------------------------------------*@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Login Activity Control API</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--===============================================================================================-->
<link rel="icon" type="@routes.Assets.versioned("login_v1/image/png")" href="@routes.Assets.versioned("login_v1/images/icons/favicon.ico")"/>
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/vendor/bootstrap/css/bootstrap.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/fonts/font-awesome-4.7.0/css/font-awesome.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/fonts/Linearicons-Free-v1.0.0/icon-font.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/vendor/animate/animate.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/vendor/css-hamburgers/hamburgers.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/vendor/select2/select2.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/css/util.css")">
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("login_v1/css/main.css")">
<!--===============================================================================================-->
</head>
<body>
<input type="hidden" id="csrfToken" value="@helper.CSRF.getToken.value">
<input type="hidden" id="validateRoute" value="@routes.TaskList5.validate()">
<input type="hidden" id="tasksRoute" value="@routes.TaskList5.taskList()">
<input type="hidden" id="createRoute" value="@routes.TaskList5.createUser()">
<input type="hidden" id="deleteRoute" value="@routes.TaskList5.delete()">
<input type="hidden" id="addRoute" value="@routes.TaskList5.addTask()">
<div id="register-section" hidden>
<div class="limiter">
<div class="container-login100" style="background-image: url('images/img-01.jpg');">
<div class="wrap-login100 p-t-130 p-b-30">
@* <form class="login100-form validate-form">*@
<div class="login100-form-avatar">
<img src="@routes.Assets.versioned("login_v1/images/icons/fire.png")" alt="AVATAR">
</div>
<span class="login100-form-title p-t-20 p-b-45">
Register on Simple Chat
</span>
<div class="wrap-input100 validate-input m-b-10" data-validate = "Email is required">
<input id="email" class="input100" type="text" placeholder="Email">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-envelope-o"></i>
</span>
</div>
<div class="wrap-input100 validate-input m-b-10" data-validate = "Username is required">
<input id="createName" class="input100" type="text" placeholder="Username">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-user"></i>
</span>
</div>
<div class="wrap-input100 validate-input m-b-10" data-validate = "Password is required">
<input id="createPass" class="input100" type="password" placeholder="Password">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-lock"></i>
</span>
</div>
<div class="container-login100-form-btn p-t-10">
<button onclick="createUser()" class="login100-form-btn">
Register
</button>
</div>
<div class="text-center w-full p-t-25 p-b-100">
<a href="#" class="txt1">
</a>
</div>
<br>
<div class="text-center w-full">
<a href="#" class="txt1" onclick="visibleLogin()">
Go to login page
</a>
</div>
@* </form>*@
</div>
</div>
</div>
</div>
<div id="login-section">
<div class="limiter">
<div class="container-login100" style="background-image: url('images/img-01.jpg');">
<div class="wrap-login100 p-t-190 p-b-30">
@* <form class="login100-form validate-form">*@
<div class="login100-form-avatar">
<img src="@routes.Assets.versioned("login_v1/images/icons/fire.png")" alt="AVATAR">
</div>
<span class="login100-form-title p-t-20 p-b-45">
Welcome to Simple Chat
</span>
<div class="wrap-input100 validate-input m-b-10" data-validate = "Username is required">
<input id="loginName" class="input100" type="text" placeholder="Username">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-user"></i>
</span>
</div>
<div class="wrap-input100 validate-input m-b-10" data-validate = "Password is required">
<input id="loginPass" class="input100" type="password" placeholder="Password">
<span class="focus-input100"></span>
<span class="symbol-input100">
<i class="fa fa-lock"></i>
</span>
</div>
<div class="container-login100-form-btn p-t-10">
<button onclick="login()" class="login100-form-btn">
Login
</button>
</div>
<div class="text-center w-full p-t-25 p-b-50">
<a href="#" class="txt1">
Forgot Username / Password?
</a>
</div>
<div class="text-center w-full">
<a href="#" class="txt1" onclick="visibleCreateUser()">
Create new account
</a>
</div>
@* </form>*@
</div>
</div>
</div>
</div>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("login_v1/vendor/jquery/jquery-3.2.1.min.js")"></script>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("login_v1/vendor/bootstrap/js/popper.js")"></script>
<script src="@routes.Assets.versioned("login_v1/vendor/bootstrap/js/bootstrap.min.js")"></script>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("login_v1/vendor/select2/select2.min.js")"></script>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("login_v1/js/main.js")"></script>
<script src="@routes.Assets.versioned("javascripts/version3.js")"></script>
</body>
</html>
@* ------------------------------------*@
</div>
}
@()(implicit request: RequestHeader, flash: Flash)
@main("Task List (Version 4)") {
<div id="contents"></div>
@* ------------------------------------*@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Login Activity Control API</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--===============================================================================================-->
<link rel="icon" type="@routes.Assets.versioned("version1/login_v1/image/png")" href="@routes.Assets.versioned("version1/login_v1/images/icons/favicon.ico")"/>
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/vendor/bootstrap/css/bootstrap.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/fonts/font-awesome-4.7.0/css/font-awesome.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/fonts/Linearicons-Free-v1.0.0/icon-font.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/vendor/animate/animate.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/vendor/css-hamburgers/hamburgers.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/vendor/select2/select2.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/css/util.css")">
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/css/main.css")">
<!--===============================================================================================-->
</head>
<body>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("version1/login_v1/vendor/jquery/jquery-3.2.1.min.js")"></script>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("version1/login_v1/vendor/bootstrap/js/popper.js")"></script>
<script src="@routes.Assets.versioned("version1/login_v1/vendor/bootstrap/js/bootstrap.min.js")"></script>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("version1/login_v1/vendor/select2/select2.min.js")"></script>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("version1/login_v1/js/main.js")"></script>
<script src="https://unpkg.com/react@@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@@16/umd/react-dom.development.js"></script>
</body>
</html>
@* ------------------------------------*@
<input type="hidden" id="csrfToken" value="@helper.CSRF.getToken.value">
<input type="hidden" id="validateRoute" value="@routes.TaskList3.validate()">
<input type="hidden" id="tasksRoute" value="@routes.TaskList3.taskList()">
<input type="hidden" id="createRoute" value="@routes.TaskList3.createUser()">
<input type="hidden" id="deleteRoute" value="@routes.TaskList3.delete()">
<input type="hidden" id="addRoute" value="@routes.TaskList3.addTask()">
<div id="react-root"></div>
<script src="@routes.Assets.versioned("javascripts/version4.js")"></script>
}
@()(implicit request: RequestHeader, flash: Flash)
@main("Task List (Version 4)") {
<div id="contents"></div>
@* ------------------------------------*@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Login Activity Control API</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--===============================================================================================-->
<link rel="icon" type="@routes.Assets.versioned("version1/login_v1/image/png")" href="@routes.Assets.versioned("version1/login_v1/images/icons/favicon.ico")"/>
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/vendor/bootstrap/css/bootstrap.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/fonts/font-awesome-4.7.0/css/font-awesome.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/fonts/Linearicons-Free-v1.0.0/icon-font.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/vendor/animate/animate.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/vendor/css-hamburgers/hamburgers.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/vendor/select2/select2.min.css")">
<!--===============================================================================================-->
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/css/util.css")">
<link rel="stylesheet" type="text/css" href="@routes.Assets.versioned("version1/login_v1/css/main.css")">
<!--===============================================================================================-->
</head>
<body>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("version1/login_v1/vendor/jquery/jquery-3.2.1.min.js")"></script>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("version1/login_v1/vendor/bootstrap/js/popper.js")"></script>
<script src="@routes.Assets.versioned("version1/login_v1/vendor/bootstrap/js/bootstrap.min.js")"></script>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("version1/login_v1/vendor/select2/select2.min.js")"></script>
<!--===============================================================================================-->
<script src="@routes.Assets.versioned("version1/login_v1/js/main.js")"></script>
<script src="https://unpkg.com/react@@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@@16/umd/react-dom.development.js"></script>
</body>
</html>
@* ------------------------------------*@
<input type="hidden" id="csrfToken" value="@helper.CSRF.getToken.value">
<input type="hidden" id="validateRoute" value="@routes.TaskList5.validate()">
<input type="hidden" id="tasksRoute" value="@routes.TaskList5.taskList()">
<input type="hidden" id="createRoute" value="@routes.TaskList5.createUser()">
<input type="hidden" id="deleteRoute" value="@routes.TaskList5.delete()">
<input type="hidden" id="addRoute" value="@routes.TaskList5.addTask()">
<input type="hidden" id="logoutRoute" value="@routes.TaskList5.logout()">
<div id="react-root"></div>
<script src="@routes.Assets.versioned("javascripts/version5.js")"></script>
}
@(message: String, style: String = "scala")
@defining(play.core.PlayVersion.current) { version =>
<section id="top">
<div class="wrapper">
<h1><a href="https://playframework.com/documentation/@version/Home">@message</a></h1>
</div>
</section>
<div id="content" class="wrapper doc">
<article>
<h1>Welcome to Play</h1>
<p>
Congratulations, you’ve just created a new Play application. This page will help you with the next few steps.
</p>
<blockquote>
<p>
You’re using Play @version
</p>
</blockquote>
<h2>Why do you see this page?</h2>
<p>
The <code>conf/routes</code> file defines a route that tells Play to invoke the <code>HomeController.index</code> action
whenever a browser requests the <code>/</code> URI using the GET method:
</p>
<pre><code># Home page
GET / controllers.HomeController.index</code></pre>
<p>
Play has invoked the <code>controllers.HomeController.index</code> method to obtain the <code>Action</code> to execute:
</p>
<pre><code>def index = Action { implicit request: Request[AnyContent] =>
Ok(views.html.index("Your new application is ready!"))
}</code></pre>
<p>
An action is a function that handles the incoming HTTP request, and returns the HTTP result to send back to the web client.
Here we send a <code>200 OK</code> response, using a template to fill its content.
</p>
<p>
The template is defined in the <code>app/views/index.scala.html</code> file and compiled as a Scala function.
</p>
<pre><code>@@(message: String)
@@main("Welcome to Play") {
@@welcome(message)
}</code></pre>
<p>
The first line of the template defines the function signature. Here it just takes a single <code>String</code> parameter.
This template then calls another function defined in <code>app/views/main.scala.html</code>, which displays the HTML
layout, and another function that displays this welcome message. You can freely add any HTML fragment mixed with Scala
code in this file.
</p>
<p>You can read more about <a href="https://www.playframework.com/documentation/@version/ScalaTemplates">Twirl</a>, the template language used by Play, and how Play handles <a href="https://www.playframework.com/documentation/@version/ScalaActions">actions</a>.</p>
<h2>Async Controller</h2>
Now that you've seen how Play renders a page, take a look at <code>AsyncController.scala</code>, which shows how to do asynchronous programming when handling a request. The code is almost exactly the same as <code>HomeController.scala</code>, but instead of returning <code>Result</code>, the action returns <code>Future[Result]</code> to Play. When the execution completes, Play can use a thread to render the result without blocking the thread in the mean time.
<p>
<a href="@routes.AsyncController.message">Click here for the AsyncController action!</a>
</p>
<p>
You can read more about <a href="https://www.playframework.com/documentation/@version/ScalaAsync">asynchronous actions</a> in the documentation.
</p>
<h2>Count Controller</h2>
<p>
Both the HomeController and AsyncController are very simple, and typically controllers present the results of the interaction of several services. As an example, see the <code>CountController</code>, which shows how to inject a component into a controller and use the component when handling requests. The count controller increments every time you click on it, so keep clicking to see the numbers go up.
</p>
<p>
<a href="@routes.CountController.count">Click here for the CountController action!</a>
</p>
<p>
You can read more about <a href="https://www.playframework.com/documentation/@version/ScalaDependencyInjection">dependency injection</a> in the documentation.
</p>
<h2>Need more info on the console?</h2>
<p>
For more information on the various commands you can run on Play, i.e. running tests and packaging applications for production, see <a href="https://playframework.com/documentation/@version/PlayConsole">Using the Play console</a>.
</p>
<h2>Need to set up an IDE?</h2>
<p>
You can start hacking your application right now using any text editor. Any changes will be automatically reloaded at each page refresh,
including modifications made to Scala source files.
</p>
<p>
If you want to set-up your application in <strong>IntelliJ IDEA</strong> or any other Java IDE, check the
<a href="https://www.playframework.com/documentation/@version/IDE">Setting up your preferred IDE</a> page.
</p>
<h2>Need more documentation?</h2>
<p>
Play documentation is available at <a href="https://www.playframework.com/documentation/@version">https://www.playframework.com/documentation</a>.
</p>
<p>
Play comes with lots of example templates showcasing various bits of Play functionality at <a href="https://www.playframework.com/download#examples">https://www.playframework.com/download#examples</a>.
</p>
<h2>Need more help?</h2>
<p>
Play questions are asked and answered on Stackoverflow using the "playframework" tag: <a href="https://stackoverflow.com/questions/tagged/playframework">https://stackoverflow.com/questions/tagged/playframework</a>
</p>
<p>
The <a href="https://groups.google.com/group/play-framework">Play Google Group</a> is where Play users come to seek help,
announce projects, and discuss issues and new features. If you don’t have a Google account, you can still join the mailing
list by sending an e-mail to
<strong>play-framework+subscribe@@googlegroups.com</strong>.
</p>
<p>
Gitter is a real time chat channel, like IRC. The <a href="https://gitter.im/playframework/playframework">playframework/playframework</a> channel is used by Play users to discuss the ins and outs of writing great Play applications.
</p>
</article>
<aside>
<h3>Browse</h3>
<ul>
<li><a href="https://playframework.com/documentation/@version">Documentation</a></li>
<li><a href="https://playframework.com/documentation/@version/api/@style/index.html">Browse the @{style.capitalize} API</a></li>
</ul>
<h3>Start here</h3>
<ul>
<li><a href="https://playframework.com/documentation/@version/PlayConsole">Using the Play console</a></li>
<li><a href="https://playframework.com/documentation/@version/IDE">Setting up your preferred IDE</a></li>
<li><a href="https://playframework.com/download#examples">Example Projects</a>
</ul>
<h3>Help here</h3>
<ul>
<li><a href="https://stackoverflow.com/questions/tagged/playframework">Stack Overflow</a></li>
<li><a href="https://groups.google.com/group/play-framework">Mailing List</a></li>
<li><a href="https://gitter.im/playframework/playframework">Gitter Channel</a></li>
</ul>
</aside>
</div>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment