├── .g8 └── form │ ├── app │ ├── controllers │ │ └── $model__Camel$Controller.scala │ └── views │ │ └── $model__camel$ │ │ └── form.scala.html │ ├── default.properties │ └── test │ └── controllers │ └── $model__Camel$ControllerSpec.scala ├── .gitignore ├── README.md ├── app ├── auth │ ├── AuthAction.scala │ └── AuthService.scala ├── controllers │ └── ApiController.scala ├── models │ ├── Comment.scala │ └── Post.scala └── repositories │ └── DataRepository.scala ├── build.gradle ├── build.sbt ├── conf ├── application.conf ├── logback.xml ├── messages └── routes ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── project ├── build.properties ├── plugins.sbt └── scaffold.sbt /.g8/form/app/controllers/$model__Camel$Controller.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject._ 4 | import play.api.mvc._ 5 | 6 | import play.api.data._ 7 | import play.api.data.Forms._ 8 | 9 | case class $model;format="Camel"$Data(name: String, age: Int) 10 | 11 | // NOTE: Add the following to conf/routes to enable compilation of this class: 12 | /* 13 | GET /$model;format="camel"$ controllers.$model;format="Camel"$Controller.$model;format="camel"$Get 14 | POST /$model;format="camel"$ controllers.$model;format="Camel"$Controller.$model;format="camel"$Post 15 | */ 16 | 17 | /** 18 | * $model;format="Camel"$ form controller for Play Scala 19 | */ 20 | class $model;format="Camel"$Controller @Inject()(mcc: MessagesControllerComponents) extends MessagesAbstractController(mcc) { 21 | 22 | val $model;format="camel"$Form = Form( 23 | mapping( 24 | "name" -> text, 25 | "age" -> number 26 | )($model;format="Camel"$Data.apply)($model;format="Camel"$Data.unapply) 27 | ) 28 | 29 | def $model;format="camel"$Get() = Action { implicit request: MessagesRequest[AnyContent] => 30 | Ok(views.html.$model;format="camel"$.form($model;format="camel"$Form)) 31 | } 32 | 33 | def $model;format="camel"$Post() = Action { implicit request: MessagesRequest[AnyContent] => 34 | $model;format="camel"$Form.bindFromRequest.fold( 35 | formWithErrors => { 36 | // binding failure, you retrieve the form containing errors: 37 | BadRequest(views.html.$model;format="camel"$.form(formWithErrors)) 38 | }, 39 | $model;format="camel"$Data => { 40 | /* binding success, you get the actual value. */ 41 | /* flashing uses a short lived cookie */ 42 | Redirect(routes.$model;format="Camel"$Controller.$model;format="camel"$Get()).flashing("success" -> ("Successful " + $model;format="camel"$Data.toString)) 43 | } 44 | ) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.g8/form/app/views/$model__camel$/form.scala.html: -------------------------------------------------------------------------------- 1 | @($model;format="camel"$Form: Form[$model;format="Camel"$Data])(implicit request: MessagesRequestHeader) 2 | 3 |

$model;format="camel"$ form

4 | 5 | @request.flash.get("success").getOrElse("") 6 | 7 | @helper.form(action = routes.$model;format="Camel"$Controller.$model;format="camel"$Post()) { 8 | @helper.CSRF.formField 9 | @helper.inputText($model;format="camel"$Form("name")) 10 | @helper.inputText($model;format="camel"$Form("age")) 11 | 12 | } 13 | -------------------------------------------------------------------------------- /.g8/form/default.properties: -------------------------------------------------------------------------------- 1 | description = Generates a Controller with form handling 2 | model = user 3 | -------------------------------------------------------------------------------- /.g8/form/test/controllers/$model__Camel$ControllerSpec.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject._ 4 | import play.api._ 5 | import play.api.mvc._ 6 | import play.api.i18n._ 7 | 8 | import play.api.data._ 9 | import play.api.data.Forms._ 10 | 11 | import org.scalatestplus.play._ 12 | import play.api.test._ 13 | import play.api.test.Helpers._ 14 | 15 | import play.filters.csrf.CSRF.Token 16 | import play.filters.csrf.{CSRFConfigProvider, CSRFFilter} 17 | 18 | /** 19 | * $model;format="Camel"$ form controller specs 20 | */ 21 | class $model;format="Camel"$ControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting { 22 | 23 | // Provide stubs for components based off Helpers.stubControllerComponents() 24 | class StubComponents(cc:ControllerComponents = stubControllerComponents()) extends MessagesControllerComponents { 25 | override val parsers: PlayBodyParsers = cc.parsers 26 | override val messagesApi: MessagesApi = cc.messagesApi 27 | override val langs: Langs = cc.langs 28 | override val fileMimeTypes: FileMimeTypes = cc.fileMimeTypes 29 | override val executionContext: ExecutionContext = cc.executionContext 30 | override val actionBuilder: ActionBuilder[Request, AnyContent] = cc.actionBuilder 31 | override val messagesActionBuilder: MessagesActionBuilder = new DefaultMessagesActionBuilderImpl(parsers.default, messagesApi)(executionContext) 32 | } 33 | 34 | "$model;format="Camel"$Controller GET" should { 35 | 36 | "render the index page from a new instance of controller" in { 37 | val controller = new $model;format="Camel"$Controller(new StubComponents()) 38 | val request = FakeRequest().withCSRFToken 39 | val home = controller.$model;format="camel"$Get().apply(request) 40 | 41 | status(home) mustBe OK 42 | contentType(home) mustBe Some("text/html") 43 | } 44 | 45 | "render the index page from the application" in { 46 | val controller = inject[$model;format="Camel"$Controller] 47 | val request = FakeRequest().withCSRFToken 48 | val home = controller.$model;format="camel"$Get().apply(request) 49 | 50 | status(home) mustBe OK 51 | contentType(home) mustBe Some("text/html") 52 | } 53 | 54 | "render the index page from the router" in { 55 | val request = CSRFTokenHelper.addCSRFToken(FakeRequest(GET, "/derp")) 56 | val home = route(app, request).get 57 | 58 | status(home) mustBe OK 59 | contentType(home) mustBe Some("text/html") 60 | } 61 | } 62 | 63 | "$model;format="Camel"$Controller POST" should { 64 | "process form" in { 65 | val request = { 66 | FakeRequest(POST, "/$model;format="camel"$") 67 | .withFormUrlEncodedBody("name" -> "play", "age" -> "4") 68 | } 69 | val home = route(app, request).get 70 | 71 | status(home) mustBe SEE_OTHER 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | target 3 | /.idea 4 | /.idea_modules 5 | /.classpath 6 | /.project 7 | /.settings 8 | /RUNNING_PID 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build and Secure a Scala Play Framework API 2 | 3 | This is the companion code project for the [Build and Secure a Scala Play Framework API blog post](https://auth0.com/blog/build-and-secure-a-scala-play-framework-api/) @ [auth0.com/blog](https://auth0.com/blog/). 4 | 5 | It is a REST API built using Scala and [Play Framework](https://playframework.com). It statically serves blog posts and comments from two endpoints. The endpoints are secured using access tokens issued by [auth0.com](https://auth0.com). In order to successfully call the endpoints and retrieve data, the request must supply a valid JWT bearer token in the `Authorization` header. Otherwise, a `401 Unauthorized` response is returned. 6 | 7 | Example request: 8 | 9 | ``` 10 | curl \ 11 | -H 'Authorization: Bearer ' \ 12 | localhost:9000/api/post/1 13 | ``` 14 | 15 | ## Running the project 16 | 17 | To start the API, use: 18 | 19 | ``` 20 | $ AUTH0_DOMAIN= AUTH0_AUDIENCE= sbt run 9000 21 | ``` 22 | 23 | Where `AUTH0_DOMAIN` is your Auth0 tenant domain, and `AUTH0_AUDIENCE` is your API identifier. 24 | 25 | ## Endpoints 26 | 27 | There are two endpoints available: 28 | 29 | ### /api/post/:postId 30 | 31 | Returns a single blog post with the specified ID. The static data provides two blog posts with IDs 1 and 2 respectively. 32 | 33 | Example response: 34 | 35 | ```json 36 | { 37 | "id": 1, 38 | "content": "This is a blog post" 39 | } 40 | ``` 41 | 42 | ### /api/post/:postId/comments 43 | 44 | Returns comments for the specified post. The static data provides comments for posts 1 and 2. 45 | 46 | Example response: 47 | 48 | ```json 49 | [ 50 | { 51 | "id":1, 52 | "postId":1, 53 | "text":"This is an awesome blog post", 54 | "authorName":"Fantastic Mr Fox" 55 | }, 56 | { 57 | "id":2, 58 | "postId":1, 59 | "text":"Thanks for the insights", 60 | "authorName":"Jane Doe" 61 | } 62 | ] 63 | ``` 64 | -------------------------------------------------------------------------------- /app/auth/AuthAction.scala: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import javax.inject.Inject 4 | import pdi.jwt._ 5 | import play.api.http.HeaderNames 6 | import play.api.mvc._ 7 | 8 | import scala.concurrent.{ExecutionContext, Future} 9 | import scala.util.{Failure, Success} 10 | 11 | // A custom request type to hold our JWT claims, we can pass these on to the 12 | // handling action 13 | case class UserRequest[A](jwt: JwtClaim, token: String, request: Request[A]) extends WrappedRequest[A](request) 14 | 15 | class AuthAction @Inject()(bodyParser: BodyParsers.Default, authService: AuthService)(implicit ec: ExecutionContext) 16 | extends ActionBuilder[UserRequest, AnyContent] { 17 | 18 | override def parser: BodyParser[AnyContent] = bodyParser 19 | override protected def executionContext: ExecutionContext = ec 20 | 21 | // A regex for parsing the Authorization header value 22 | private val headerTokenRegex = """Bearer (.+?)""".r 23 | 24 | // Called when a request is invoked. We should validate the bearer token here 25 | // and allow the request to proceed if it is valid. 26 | override def invokeBlock[A](request: Request[A], block: UserRequest[A] => Future[Result]): Future[Result] = 27 | extractBearerToken(request) map { token => 28 | authService.validateJwt(token) match { 29 | case Success(claim) => block(UserRequest(claim, token, request)) // token was valid - proceed! 30 | case Failure(t) => Future.successful(Results.Unauthorized(t.getMessage)) // token was invalid - return 401 31 | } 32 | } getOrElse Future.successful(Results.Unauthorized) // no token was sent - return 401 33 | 34 | // Helper for extracting the token value 35 | private def extractBearerToken[A](request: Request[A]): Option[String] = 36 | request.headers.get(HeaderNames.AUTHORIZATION) collect { 37 | case headerTokenRegex(token) => token 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/auth/AuthService.scala: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import com.auth0.jwk.UrlJwkProvider 4 | import javax.inject.Inject 5 | import pdi.jwt.{JwtAlgorithm, JwtBase64, JwtClaim, JwtJson} 6 | import play.api.Configuration 7 | 8 | import scala.util.{Failure, Success, Try} 9 | 10 | class AuthService @Inject()(config: Configuration) { 11 | 12 | // A regex that defines the JWT pattern and allows us to 13 | // extract the header, claims and signature 14 | private val jwtRegex = """(.+?)\.(.+?)\.(.+?)""".r 15 | 16 | // Your Auth0 domain, read from configuration 17 | private def domain = config.get[String]("auth0.domain") 18 | private def audience = config.get[String]("auth0.audience") 19 | private def issuer = s"https://$domain/" 20 | 21 | // Validates a JWT and potentially returns the claims if the token was successfully parsed 22 | def validateJwt(token: String): Try[JwtClaim] = for { 23 | jwk <- getJwk(token) // Get the secret key for this token 24 | claims <- JwtJson.decode(token, jwk.getPublicKey, Seq(JwtAlgorithm.RS256)) // Decode the token using the secret key 25 | _ <- validateClaims(claims) // validate the data stored inside the token 26 | } yield claims 27 | 28 | // Splits a JWT into it's 3 component parts 29 | private val splitToken = (jwt: String) => jwt match { 30 | case jwtRegex(header, body, sig) => Success((header, body, sig)) 31 | case _ => Failure(new Exception("Token does not match the correct pattern")) 32 | } 33 | 34 | // As the header and claims data are base64-encoded, this function 35 | // decodes those elements 36 | private val decodeElements = (data: Try[(String, String, String)]) => data map { 37 | case (header, body, sig) => (JwtBase64.decodeString(header), JwtBase64.decodeString(body), sig) 38 | } 39 | 40 | // Gets the JWK from the JWKS endpoint using the jwks-rsa library 41 | private val getJwk = (token: String) => 42 | (splitToken andThen decodeElements) (token) flatMap { 43 | case (header, _, _) => 44 | val jwtHeader = JwtJson.parseHeader(header) // extract the header 45 | val jwkProvider = new UrlJwkProvider(s"https://$domain") 46 | 47 | // Use jwkProvider to load the JWKS data and return the JWK 48 | jwtHeader.keyId.map { k => 49 | Try(jwkProvider.get(k)) 50 | } getOrElse Failure(new Exception("Unable to retrieve kid")) 51 | } 52 | 53 | // Validates the claims inside the token. isValid checks the issuedAt, expiresAt, 54 | // issuer and audience fields. 55 | private val validateClaims = (claims: JwtClaim) => 56 | if (claims.isValid(issuer, audience)) { 57 | Success(claims) 58 | } else { 59 | Failure(new Exception("The JWT did not pass validation")) 60 | } 61 | } -------------------------------------------------------------------------------- /app/controllers/ApiController.scala: -------------------------------------------------------------------------------- 1 | // Make sure it's in the 'controllers' package 2 | package controllers 3 | 4 | import auth.AuthAction 5 | import javax.inject.{Inject, Singleton} 6 | import play.api.libs.json.Json 7 | import play.api.mvc.{AbstractController, ControllerComponents} 8 | import repositories.DataRepository 9 | 10 | @Singleton 11 | class ApiController @Inject()(cc: ControllerComponents, 12 | dataRepository: DataRepository, 13 | authAction: AuthAction 14 | ) 15 | extends AbstractController(cc) { 16 | 17 | // Create a simple 'ping' endpoint for now, so that we 18 | // can get up and running with a basic implementation 19 | def ping = Action { implicit request => 20 | Ok("Hello, Scala!") 21 | } 22 | 23 | // Get a single post 24 | def getPost(postId: Int) = authAction { implicit request => 25 | dataRepository.getPost(postId) map { post => 26 | // If the post was found, return a 200 with the post data as JSON 27 | Ok(Json.toJson(post)) 28 | } getOrElse NotFound // otherwise, return Not Found 29 | } 30 | 31 | // Get comments for a post 32 | def getComments(postId: Int) = authAction { implicit request => 33 | // Simply return 200 OK with the comment data as JSON. 34 | Ok(Json.toJson(dataRepository.getComments(postId))) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/models/Comment.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import play.api.libs.json.Json 4 | 5 | // Represents a comment on a blog post 6 | case class Comment(id: Int, postId: Int, text: String, authorName: String) 7 | 8 | object Comment { 9 | // Use a default JSON formatter for the Comment type 10 | implicit val format = Json.format[Comment] 11 | } 12 | -------------------------------------------------------------------------------- /app/models/Post.scala: -------------------------------------------------------------------------------- 1 | // Make sure it goes in the models package 2 | package models 3 | 4 | import play.api.libs.json.Json 5 | 6 | // Create our Post type 7 | case class Post(id: Int, content: String) 8 | 9 | object Post { 10 | // We're going to be serving this type as JSON, so specify a 11 | // default Json formatter for our Post type here 12 | implicit val format = Json.format[Post] 13 | } 14 | -------------------------------------------------------------------------------- /app/repositories/DataRepository.scala: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import javax.inject.Singleton 4 | import models.{Comment, Post} 5 | 6 | @Singleton 7 | class DataRepository { 8 | 9 | // Specify a couple of posts for our API to serve up 10 | private val posts = Seq( 11 | Post(1, "This is a blog post"), 12 | Post(2, "Another blog post with awesome content") 13 | ) 14 | 15 | // Specify some comments for our API to serve up 16 | private val comments = Seq( 17 | Comment(1, 1, "This is an awesome blog post", "Fantastic Mr Fox"), 18 | Comment(2, 1, "Thanks for the insights", "Jane Doe"), 19 | Comment(3, 2, "Great, thanks for this post", "Joe Bloggs") 20 | ) 21 | 22 | /* 23 | * Returns a blog post that matches the specified id, or None if no 24 | * post was found (collectFirst returns None if the function is undefined for the 25 | * given post id) 26 | */ 27 | def getPost(postId: Int): Option[Post] = posts.collectFirst { 28 | case p if p.id == postId => p 29 | } 30 | 31 | /* 32 | * Returns the comments for a blog post 33 | * If no comments exist for the specified post id, an empty sequence is returned 34 | * by virtue of the fact that we're using 'collect' 35 | */ 36 | def getComments(postId: Int): Seq[Comment] = comments.collect { 37 | case c if c.postId == postId => c 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'play' 3 | id 'idea' 4 | } 5 | 6 | def playVersion = '2.6.19' 7 | def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") 8 | 9 | model { 10 | components { 11 | play { 12 | platform play: playVersion, scala: scalaVersion, java: '1.8' 13 | injectedRoutesGenerator = true 14 | 15 | sources { 16 | twirlTemplates { 17 | defaultImports = TwirlImports.SCALA 18 | } 19 | } 20 | } 21 | } 22 | } 23 | 24 | // EXAMPLE: How to add parameter to Scala compiler 25 | // Another option is documented here: 26 | // https://docs.gradle.org/current/userguide/play_plugin.html#sec:configuring_compiler_options 27 | // 28 | // See other configurations for ScalaCompile task here: 29 | // https://docs.gradle.org/current/dsl/org.gradle.api.tasks.scala.ScalaCompile.html 30 | // 31 | // tasks.withType(ScalaCompile) { 32 | // scalaCompileOptions.additionalParameters = [ 33 | // "-deprecation", 34 | // "-verbose" 35 | // ] 36 | // } 37 | 38 | // EXAMPLE: How to add JVM parameters when running `runPlayBinary`. 39 | // See other configurations for PlayRun here: 40 | // https://docs.gradle.org/current/dsl/org.gradle.play.tasks.PlayRun.html 41 | // 42 | // tasks.withType(PlayRun) { 43 | // forkOptions.jvmArgs = ['-Dplay.http.secret.key=yadayada'] 44 | // } 45 | 46 | // EXAMPLE: How to add JVM parameters to the script created by `stage`. 47 | // See more details here: 48 | // https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/application/CreateStartScripts.html 49 | // 50 | // tasks.withType(CreateStartScripts) { 51 | // defaultJvmOpts = ['-Dplay.http.secret.key=yadayada'] 52 | // } 53 | 54 | dependencies { 55 | play "com.typesafe.play:play-guice_$scalaVersion:$playVersion" 56 | play "com.typesafe.play:play-logback_$scalaVersion:$playVersion" 57 | play "com.typesafe.play:filters-helpers_$scalaVersion:$playVersion" 58 | 59 | playTest "org.scalatestplus.play:scalatestplus-play_$scalaVersion:3.1.2" 60 | } 61 | 62 | repositories { 63 | jcenter() 64 | maven { 65 | name "lightbend-maven-releases" 66 | url "https://repo.lightbend.com/lightbend/maven-release" 67 | } 68 | ivy { 69 | name "lightbend-ivy-release" 70 | url "https://repo.lightbend.com/lightbend/ivy-releases" 71 | layout "ivy" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := """scala-api-sample""" 2 | organization := "com.example" 3 | 4 | version := "1.0-SNAPSHOT" 5 | 6 | lazy val root = (project in file(".")).enablePlugins(PlayScala) 7 | 8 | scalaVersion := "2.12.6" 9 | 10 | libraryDependencies += guice 11 | libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test 12 | 13 | libraryDependencies ++= Seq( 14 | "com.pauldijou" %% "jwt-play" % "0.19.0", 15 | "com.pauldijou" %% "jwt-core" % "0.19.0", 16 | "com.auth0" % "jwks-rsa" % "0.6.1" 17 | ) 18 | 19 | // Adds additional packages into Twirl 20 | //TwirlKeys.templateImports += "com.example.controllers._" 21 | 22 | // Adds additional packages into conf/routes 23 | // play.sbt.routes.RoutesKeys.routesImport += "com.example.binders._" 24 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | auth0 { 2 | domain = ${?AUTH0_DOMAIN} 3 | audience = ${?AUTH0_AUDIENCE} 4 | } 5 | -------------------------------------------------------------------------------- /conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ${application.home:-.}/logs/application.log 8 | 9 | %date [%level] from %logger in %thread - %message%n%xException 10 | 11 | 12 | 13 | 14 | 15 | %coloredLevel %logger{15} - %message%n%xException{10} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /conf/messages: -------------------------------------------------------------------------------- 1 | # https://www.playframework.com/documentation/latest/ScalaI18N 2 | -------------------------------------------------------------------------------- /conf/routes: -------------------------------------------------------------------------------- 1 | GET /api/ping controllers.ApiController.ping 2 | 3 | # NEW 4 | GET /api/post/:postId controllers.ApiController.getPost(postId: Int) 5 | GET /api/post/:postId/comments controllers.ApiController.getComments(postId: Int) 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0-blog/scala-api-sample/d1f71552a92bb963b527d279cf5b0288cd95a028/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStorePath=wrapper/dists 5 | zipStoreBase=GRADLE_USER_HOME 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.18") 2 | -------------------------------------------------------------------------------- /project/scaffold.sbt: -------------------------------------------------------------------------------- 1 | // Defines scaffolding (found under .g8 folder) 2 | // http://www.foundweekends.org/giter8/scaffolding.html 3 | // sbt "g8Scaffold form" 4 | 5 | addSbtPlugin("org.foundweekends.giter8" % "sbt-giter8-scaffold" % "0.11.0") 6 | --------------------------------------------------------------------------------