From e9e49006704d3b273c5f4ffabe11003433eb6854 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Sun, 31 Jul 2016 21:33:03 -0700 Subject: [PATCH 01/73] Initial commit --- .gitignore | 8 + LICENSE | 8 + README.md | 0 app/ErrorHandler.scala | 77 +++++++++ app/Filters.scala | 16 ++ app/Module.scala | 18 +++ app/RequestHandler.scala | 39 +++++ app/controllers/HomeController.scala | 11 ++ .../StrictTransportSecurityFilter.scala | 88 ++++++++++ app/post/Post.scala | 45 ++++++ app/post/PostAction.scala | 42 +++++ app/post/PostModule.scala | 65 ++++++++ app/post/PostRouter.scala | 151 ++++++++++++++++++ app/views/index.scala.html | 13 ++ app/views/main.scala.html | 30 ++++ app/views/posts/create.scala.html | 1 + app/views/posts/index.scala.html | 23 +++ app/views/posts/main.scala.html | 3 + app/views/posts/row.scala.html | 24 +++ app/views/posts/show.scala.html | 35 ++++ build.sbt | 47 ++++++ conf/application.conf | 30 ++++ conf/generated.keystore | Bin 0 -> 3175 bytes conf/logback.xml | 38 +++++ conf/routes | 7 + disabledAlgorithms.properties | 4 + .../lightbend/blog/comment/CommentData.scala | 11 ++ .../blog/comment/CommentRepository.scala | 95 +++++++++++ .../com/lightbend/blog/post/PostData.scala | 14 ++ .../lightbend/blog/post/PostRepository.scala | 86 ++++++++++ project/Common.scala | 37 +++++ project/build.properties | 1 + project/plugins.sbt | 2 + public/stylesheets/.crunch | 21 +++ public/stylesheets/main.css | 87 ++++++++++ public/stylesheets/main.css.map | 1 + public/stylesheets/main.less | 107 +++++++++++++ test/ApplicationSpec.scala | 23 +++ 38 files changed, 1308 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/ErrorHandler.scala create mode 100644 app/Filters.scala create mode 100644 app/Module.scala create mode 100644 app/RequestHandler.scala create mode 100644 app/controllers/HomeController.scala create mode 100644 app/filters/StrictTransportSecurityFilter.scala create mode 100644 app/post/Post.scala create mode 100644 app/post/PostAction.scala create mode 100644 app/post/PostModule.scala create mode 100644 app/post/PostRouter.scala create mode 100644 app/views/index.scala.html create mode 100644 app/views/main.scala.html create mode 100644 app/views/posts/create.scala.html create mode 100644 app/views/posts/index.scala.html create mode 100644 app/views/posts/main.scala.html create mode 100644 app/views/posts/row.scala.html create mode 100644 app/views/posts/show.scala.html create mode 100644 build.sbt create mode 100644 conf/application.conf create mode 100644 conf/generated.keystore create mode 100644 conf/logback.xml create mode 100644 conf/routes create mode 100644 disabledAlgorithms.properties create mode 100644 modules/comment/src/main/scala/com/lightbend/blog/comment/CommentData.scala create mode 100644 modules/comment/src/main/scala/com/lightbend/blog/comment/CommentRepository.scala create mode 100644 modules/post/src/main/scala/com/lightbend/blog/post/PostData.scala create mode 100644 modules/post/src/main/scala/com/lightbend/blog/post/PostRepository.scala create mode 100644 project/Common.scala create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 public/stylesheets/.crunch create mode 100644 public/stylesheets/main.css create mode 100644 public/stylesheets/main.css.map create mode 100644 public/stylesheets/main.less create mode 100644 test/ApplicationSpec.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..eb372fc71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +logs +target +/.idea +/.idea_modules +/.classpath +/.project +/.settings +/RUNNING_PID diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..4baedcb95 --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +This software is licensed under the Apache 2 license, quoted below. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with +the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/app/ErrorHandler.scala b/app/ErrorHandler.scala new file mode 100644 index 000000000..39dc41fea --- /dev/null +++ b/app/ErrorHandler.scala @@ -0,0 +1,77 @@ +import javax.inject.{Inject, Provider, Singleton} + +import play.api._ +import play.api.http.Status._ +import play.api.http.{ContentTypes, DefaultHttpErrorHandler, HttpErrorHandlerExceptions} +import play.api.mvc.Results._ +import play.api.mvc._ +import play.api.routing.Router +import play.core.SourceMapper + +import scala.concurrent._ +import scala.util.control.NonFatal + +/** + * Provides a stripped down error handler that does not use HTML in error pages. + * + * https://www.playframework.com/documentation/2.5.x/ScalaErrorHandling + */ +@Singleton +class ErrorHandler(environment: Environment, + configuration: Configuration, + sourceMapper: Option[SourceMapper] = None, + optionRouter: => Option[Router] = None) + extends DefaultHttpErrorHandler(environment, configuration, sourceMapper, optionRouter) with AcceptExtractors with Rendering { + + private val logger = org.slf4j.LoggerFactory.getLogger("application.ErrorHandler") + + // This maps through Guice so that the above constructor... + @Inject + def this(environment: Environment, + configuration: Configuration, + sourceMapper: OptionalSourceMapper, + router: Provider[Router]) = { + this(environment, configuration, sourceMapper.sourceMapper, Some(router.get)) + } + + override def onClientError(request: RequestHeader, statusCode: Int, message: String): Future[Result] = { + logger.debug(s"onClientError: statusCode = $statusCode, uri = ${request.uri}, message = $message") + + Future.successful { + val result = statusCode match { + case BAD_REQUEST => + Results.BadRequest(message) + case FORBIDDEN => + Results.Forbidden(message) + case NOT_FOUND => + Results.NotFound(message) + case clientError if statusCode >= 400 && statusCode < 500 => + Results.Status(statusCode) + case nonClientError => + val msg = s"onClientError invoked with non client error status code $statusCode: $message" + throw new IllegalArgumentException(msg) + } + result + } + } + + override def onServerError(request: RequestHeader, exception: Throwable): Future[Result] = { + try { + val usefulException = HttpErrorHandlerExceptions.throwableToUsefulException(sourceMapper, + environment.mode == Mode.Prod, exception) + + logger.error( + s"! @${usefulException.id} - Internal server error, for (${request.method}) [${request.uri}] ->", + usefulException + ) + + Future.successful { + InternalServerError + } + } catch { + case NonFatal(e) => + logger.error("Error while handling error", e) + Future.successful(InternalServerError) + } + } +} diff --git a/app/Filters.scala b/app/Filters.scala new file mode 100644 index 000000000..908bfd52c --- /dev/null +++ b/app/Filters.scala @@ -0,0 +1,16 @@ +import javax.inject.Inject + +import filters.StrictTransportSecurityFilter +import play.api.http.DefaultHttpFilters +import play.filters.csrf.CSRFFilter +import play.filters.headers.SecurityHeadersFilter +import play.filters.hosts.AllowedHostsFilter + +/** + * https://www.playframework.com/documentation/latest/ScalaHttpFilters + */ +class Filters @Inject()(hstsFilter: StrictTransportSecurityFilter, + csrfFilter: CSRFFilter, + securityFilter: SecurityHeadersFilter, + allowedHostsFilter: AllowedHostsFilter) + extends DefaultHttpFilters(hstsFilter, csrfFilter, securityFilter, allowedHostsFilter) diff --git a/app/Module.scala b/app/Module.scala new file mode 100644 index 000000000..c64d76d6d --- /dev/null +++ b/app/Module.scala @@ -0,0 +1,18 @@ +import com.google.inject.AbstractModule +import filters._ + +/** + * Provides a base Guice module for setting up some more components from configuration + * that aren't provided by Play itself. + */ +class Module extends AbstractModule { + override def configure() = { + bind(classOf[StrictTransportSecurityConfig]).toProvider(classOf[StrictTransportSecurityConfigProvider]) + + install(new post.PostModule) + } +} + + + + diff --git a/app/RequestHandler.scala b/app/RequestHandler.scala new file mode 100644 index 000000000..c0ea69ce3 --- /dev/null +++ b/app/RequestHandler.scala @@ -0,0 +1,39 @@ +import javax.inject.Inject + +import play.api.http._ +import play.api.mvc._ +import play.api.routing.Router + +/** + * Handles all requests. + */ +class RequestHandler @Inject()(router: Router, + errorHandler: HttpErrorHandler, + configuration: HttpConfiguration, + filters: HttpFilters) + extends DefaultHttpRequestHandler(router, errorHandler, configuration, filters) { + + override def handlerForRequest(request: RequestHeader): (RequestHeader, Handler) = { + super.handlerForRequest { + // ensures that REST API does not need a trailing "/" + if (request.uri.startsWith("/posts")) { + addTrailingSlash(request) + } else { + request + } + } + } + + private def addTrailingSlash(origReq: RequestHeader): RequestHeader = { + if (! origReq.path.endsWith("/")) { + val path = origReq.path + "/" + if (origReq.rawQueryString.isEmpty) { + origReq.copy(path = path, uri = path) + } else { + origReq.copy(path = path, uri = path + s"?${origReq.rawQueryString}") + } + } else { + origReq + } + } +} diff --git a/app/controllers/HomeController.scala b/app/controllers/HomeController.scala new file mode 100644 index 000000000..2ff585a96 --- /dev/null +++ b/app/controllers/HomeController.scala @@ -0,0 +1,11 @@ +package controllers + +import play.api.mvc.{Action, Controller} + +class HomeController extends Controller { + + def index = Action { implicit request => + Ok(views.html.index()) + } + +} diff --git a/app/filters/StrictTransportSecurityFilter.scala b/app/filters/StrictTransportSecurityFilter.scala new file mode 100644 index 000000000..40b56446a --- /dev/null +++ b/app/filters/StrictTransportSecurityFilter.scala @@ -0,0 +1,88 @@ +package filters + +import javax.inject._ + +import akka.stream.Materializer +import com.google.inject.AbstractModule +import com.netaporter.uri.Uri +import com.typesafe.config.Config +import play.api.Configuration +import play.api.mvc.{Filter, RequestHeader, Result, Results} + +import scala.concurrent.{ExecutionContext, Future} + +/** + * Sends a Strict Transport Security Filter header to clients over HTTPS. + * + * https://tools.ietf.org/html/rfc6797 + */ +class StrictTransportSecurityFilter @Inject()(config: StrictTransportSecurityConfig) + (implicit val mat: Materializer, ec: ExecutionContext) + extends Filter { + + def apply(nextFilter: RequestHeader => Future[Result])(request: RequestHeader): Future[Result] = { + if (request.secure) { + nextFilter(request).map { result => + result.withHeaders(hstsHeader) + } + } else { + Future.successful { + val secureURL = generateSecureURL(request) + // Use a permanent redirect to keep people on HTTPS + Results.PermanentRedirect(secureURL) + } + } + } + + def hstsHeader: (String, String) = { + ("Strict-Transport-Security", s"""max-age=${config.maxAge.getSeconds}""") + } + + def generateSecureURL(request: RequestHeader) = { + import com.netaporter.uri.dsl._ + val uri: Uri = request.uri + val secureUri = uri + .withScheme("https") + .withHost(config.secureHost) + .withPort(config.securePort) + secureUri.toString() + } + +} + +/** + * Configuration DTO for the HSTS filter + * + * @param maxAge how long the HSTS header max-age should be + * @param secureHost the secure hostname + * @param securePort the secure port + */ +final case class StrictTransportSecurityConfig(maxAge: java.time.Duration, + secureHost: String, + securePort: Int) + +object StrictTransportSecurityConfig { + def fromConfiguration(config: Config): StrictTransportSecurityConfig = { + val hstsConfig = config.getConfig("restapi.filters.hsts") + + val maxAge = hstsConfig.getDuration("maxAge") + val secureHost = hstsConfig.getString("secureHost") + val securePort = hstsConfig.getInt("securePort") + + StrictTransportSecurityConfig(maxAge, secureHost, securePort) + } +} + +/** + * Pulls in setting for StrictTransportSecurityFilter via conf/application.conf + * + * @param configuration the application.conf configuration + */ +@Singleton +class StrictTransportSecurityConfigProvider @Inject()(configuration: Configuration) + extends Provider[StrictTransportSecurityConfig] { + + lazy val get: StrictTransportSecurityConfig = { + StrictTransportSecurityConfig.fromConfiguration(configuration.underlying) + } +} diff --git a/app/post/Post.scala b/app/post/Post.scala new file mode 100644 index 000000000..1e50e5d07 --- /dev/null +++ b/app/post/Post.scala @@ -0,0 +1,45 @@ +package post + +import com.lightbend.blog.post.{PostData, PostId} +import play.api.libs.json._ + +case class Comment(body: String) + +object Comment { + implicit val implicitWrites = new Writes[Comment] { + def writes(comment: Comment): JsValue = { + Json.obj( + "body" -> comment.body + ) + } + } +} + + +case class Post(id: String, link: String, title: String, body: String, comments: Seq[Comment]) + +object Post { + + implicit val implicitWrites = new Writes[Post] { + def writes(post: Post): JsValue = { + Json.obj( + "id" -> post.id, + "link" -> post.link, + "title" -> post.title, + "body" -> post.body, + "comments" -> Json.toJson(post.comments) + ) + } + } + + def apply(p: PostData, comments: Seq[Comment]): Post = { + Post(p.id.toString, link(p.id), p.title, p.body, comments) + } + + private def link(id: PostId): String = { + import com.netaporter.uri.dsl._ + val url = "/posts" / id.toString + url.toString() + } + +} diff --git a/app/post/PostAction.scala b/app/post/PostAction.scala new file mode 100644 index 000000000..26119a0d0 --- /dev/null +++ b/app/post/PostAction.scala @@ -0,0 +1,42 @@ +package post + +import javax.inject.Inject + +import com.lightbend.blog.post.PostId +import play.api.i18n.{Messages, MessagesApi} +import play.api.mvc._ + +import scala.concurrent.{ExecutionContext, Future} + +/** + * The default action for a post request. + * + * This is the place to put logging, metrics, and general custom headers. + */ +class PostAction @Inject()(val messagesApi: MessagesApi)(implicit ec: ExecutionContext) extends ActionBuilder[PostRequest] { + private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) + + type PostRequestBlock[A] = (PostRequest[A]) => Future[Result] + + override def invokeBlock[A](request: Request[A], block: PostRequestBlock[A]): Future[Result] = { + val messages = messagesApi.preferred(request) + val postRequest = new PostRequest(request, messages) + block(postRequest).map { result => + if (logger.isTraceEnabled) { + logger.trace(s"postAction: request = $request, result = ${result.header}") + } + result + } + } + +} + +/** + * A wrapped request that can contain Post information. + * + * This is commonly used to hold request-specific information like + * security credentials and localized Messages + */ +class PostRequest[A](request: Request[A], val messages: Messages) extends WrappedRequest(request) { + override def toString(): String = s"[${request.uri}]" +} diff --git a/app/post/PostModule.scala b/app/post/PostModule.scala new file mode 100644 index 000000000..c4e2aa79d --- /dev/null +++ b/app/post/PostModule.scala @@ -0,0 +1,65 @@ +package post + +import javax.inject.{Inject, Provider, Singleton} + +import akka.actor.ActorSystem +import com.google.inject.AbstractModule +import com.lightbend.blog.comment._ +import com.lightbend.blog.post._ +import play.api.inject.ApplicationLifecycle + +class PostModule extends AbstractModule { + override def configure(): Unit = { + bind(classOf[CommentRepository]).toProvider(classOf[CommentRepositoryProvider]) + bind(classOf[PostRepository]).toProvider(classOf[PostRepositoryProvider]) + } +} + +/** + * Provides a post repository for Play. + */ +@Singleton +class PostRepositoryProvider @Inject()(applicationLifecycle: ApplicationLifecycle, + actorSystem: ActorSystem) + extends Provider[PostRepository] { + + lazy val get: PostRepository = { + val repo = new PostRepositoryImpl(executionContext) + // Hooks the repository lifecycle to Play's lifecycle, so any resources are shutdown + applicationLifecycle.addStopHook { () => + repo.stop() + } + repo + } + + private def executionContext: PostExecutionContext = { + //val ec = actorSystem.dispatchers.lookup("post.dispatcher") + val ec = actorSystem.dispatchers.defaultGlobalDispatcher + new PostExecutionContext(ec) + } +} + +/** + * Provides a comment repository for Play. + */ +@Singleton +class CommentRepositoryProvider @Inject()(applicationLifecycle: ApplicationLifecycle, + actorSystem: ActorSystem) + extends Provider[CommentRepository] { + + lazy val get: CommentRepository = { + val repo = new CommentRepositoryImpl(executionContext) + // Hooks the repository lifecycle to Play's lifecycle, so any resources are shutdown + applicationLifecycle.addStopHook { () => + repo.stop() + } + repo + } + + private def executionContext: CommentExecutionContext = { + //val ec = actorSystem.dispatchers.lookup("post.dispatcher") + val ec = actorSystem.dispatchers.defaultGlobalDispatcher + new CommentExecutionContext(ec) + } + +} diff --git a/app/post/PostRouter.scala b/app/post/PostRouter.scala new file mode 100644 index 000000000..570d96add --- /dev/null +++ b/app/post/PostRouter.scala @@ -0,0 +1,151 @@ +package post + +import javax.inject.Inject + +import com.lightbend.blog.comment._ +import com.lightbend.blog.post._ +import play.api.cache.Cached +import play.api.http.Status +import play.api.libs.json.Json +import play.api.mvc._ +import play.api.routing.Router.Routes +import play.api.routing.SimpleRouter +import play.api.routing.sird._ + +import scala.concurrent.Future + +/** + * Takes HTTP requests and produces JSON or HTML responses + * from a repository providing data. + */ +class PostRouter @Inject()(cached: Cached, + action: PostAction, + postRepository: PostRepository, + commentRepository: CommentRepository) + extends SimpleRouter with AcceptExtractors with Rendering { + + // A trampoline ties an execution context to the currently running thread. + // It should only be used for short running bits of non-blocking code. + import play.api.libs.iteratee.Execution.Implicits.trampoline + + private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) + + private val cacheDuration = 500 + + override def routes: Routes = { + + case GET(p"/") => + cached.status(rh => s"${rh.method} ${rh.uri}", Status.OK, cacheDuration) { + action.async { implicit request => + renderPosts() + } + } + + case HEAD(p"/") => + action.async { implicit request => + renderPosts() + } + + case GET(p"/$id") => + cached.status(rh => s"${rh.method} ${rh.uri}", Status.OK, cacheDuration) { + action.async { implicit request => + renderPost(PostId(id)) + } + } + + case HEAD(p"/$id") => + action.async { implicit request => + renderPost(PostId(id)) + } + } + + // https://www.playframework.com/documentation/2.5.x/ScalaContentNegotiation + + private def renderPost[A](id: PostId)(implicit request: PostRequest[A]): Future[Result] = { + logger.trace("renderPost: ") + // Find a single item from the repository + render.async { + + case Accepts.Json() if request.method == "GET" => + // Query the repository for post with this id + postRepository.get(id).flatMap { + case Some(p) => + findComments(p.id).map { comments => + val post = Post(p, comments) + val json = Json.toJson(post) + Results.Ok(json) + } + case None => + Future.successful(Results.NotFound) + } + + case Accepts.Html() if request.method == "GET" => + // Query the repository for post with this id + postRepository.get(id).flatMap { + case Some(p) => + findComments(p.id).map { comments => + val post = Post(p, comments) + Results.Ok(views.html.posts.show(post)) + } + case None => + Future.successful(Results.NotFound) + } + + case Accepts.Json() & Accepts.Html() if request.method == "HEAD" => + postRepository.get(id).flatMap { + case Some(p) => + Future.successful(Results.Ok) + case None => + Future.successful(Results.NotFound) + } + + } + } + + private def renderPosts[A]()(implicit request: PostRequest[A]): Future[Result] = { + render.async { + + case Accepts.Json() if request.method == "GET" => + // Query the repository for available posts + postRepository.list().flatMap { postDataList => + findPosts(postDataList).map { posts => + val json = Json.toJson(posts) + Results.Ok(json) + } + } + + case Accepts.Html() if request.method == "GET" => + // Query the repository for available posts + postRepository.list().flatMap { postDataList => + findPosts(postDataList).map { posts => + Results.Ok(views.html.posts.index(posts)) + } + } + + case Accepts.Json() & Accepts.Html() if request.method == "HEAD" => + // HEAD has no body, so just say hi + Future.successful(Results.Ok) + + } + } + + private def findPosts(postDataList: Iterable[PostData]): Future[Iterable[Post]] = { + // Get an Iterable[Future[Post]] containing comments + val listOfFutures = postDataList.map { p => + findComments(p.id).map { comments => + Post(p, comments) + } + } + + // Flip it into a single Future[Iterable[Post]] + Future.sequence(listOfFutures) + } + + private def findComments(postId: PostId): Future[Seq[Comment]] = { + // Find all the comments for this post + commentRepository.findByPost(postId.toString).map { comments => + comments.map(c => Comment(c.body)).toSeq + } + } + +} diff --git a/app/views/index.scala.html b/app/views/index.scala.html new file mode 100644 index 000000000..2f0be67d4 --- /dev/null +++ b/app/views/index.scala.html @@ -0,0 +1,13 @@ +@()(implicit request: Request[_]) + +@main("REST API Example") { +

+ Example text. +

+ + +} diff --git a/app/views/main.scala.html b/app/views/main.scala.html new file mode 100644 index 000000000..3a79c8c91 --- /dev/null +++ b/app/views/main.scala.html @@ -0,0 +1,30 @@ +@(title: String)(content: Html)(implicit r: Request[_]) + + + + + + @title + + + + + + +
+ @content +
+ + + + + + diff --git a/app/views/posts/create.scala.html b/app/views/posts/create.scala.html new file mode 100644 index 000000000..7433bf44e --- /dev/null +++ b/app/views/posts/create.scala.html @@ -0,0 +1 @@ +@()(implicit request: post.PostRequest[_]) diff --git a/app/views/posts/index.scala.html b/app/views/posts/index.scala.html new file mode 100644 index 000000000..317cf9956 --- /dev/null +++ b/app/views/posts/index.scala.html @@ -0,0 +1,23 @@ +@(posts: Iterable[post.Post])(implicit request: post.PostRequest[_]) + +@main("Posts") { + +
+

Posts

+
+ + + + + + + + + + @for(post <- posts) { + @row(post) + } + +
LinkTitleBodyComments
+ +} diff --git a/app/views/posts/main.scala.html b/app/views/posts/main.scala.html new file mode 100644 index 000000000..5093f68b4 --- /dev/null +++ b/app/views/posts/main.scala.html @@ -0,0 +1,3 @@ +@(title: String)(content: Html)(implicit request: post.PostRequest[_]) + +@views.html.main(title)(content) diff --git a/app/views/posts/row.scala.html b/app/views/posts/row.scala.html new file mode 100644 index 000000000..af2c2539e --- /dev/null +++ b/app/views/posts/row.scala.html @@ -0,0 +1,24 @@ +@(p: post.Post)(implicit request: post.PostRequest[_]) + + + + + @{p.link} + + + + @{p.title} + + + + @{p.body} + + + + @for(c <- p.comments) { + @{c.body} +
+ } + + + diff --git a/app/views/posts/show.scala.html b/app/views/posts/show.scala.html new file mode 100644 index 000000000..80df738f4 --- /dev/null +++ b/app/views/posts/show.scala.html @@ -0,0 +1,35 @@ +@(p: post.Post)(implicit request: post.PostRequest[_]) + +@main("Posts") { + +
+

Post @{p.id}

+
+ +
+
ID
+
@{p.id}
+
+
+
Link
+
@{p.link}
+
+
+
Title
+
@{p.title}
+
+
+
Body
+
@{p.body}
+
+
+
Comment
+
+ @for(c <- p.comments) { + @{c.body} +
+ } +
+
+ +} diff --git a/build.sbt b/build.sbt new file mode 100644 index 000000000..12adcc699 --- /dev/null +++ b/build.sbt @@ -0,0 +1,47 @@ +import sbt.Keys._ + +// Set up two data repositories that will serve us raw data which we have to +// present through a REST API. These don't know about Play, and can be run +// on their own without including any Play classes. They each contain a +// minimal Guice binding which Play calls through "play.modules.enabled". + +// Provides post repository to the system. +lazy val postModule = (project in file("modules/post")).enablePlugins(Common) + +// Provides comment repository to the system. +lazy val commentModule = (project in file("modules/comment")).enablePlugins(Common) + +// The Play project itself, which aggregates everything. +lazy val root = (project in file(".")).enablePlugins(Common, PlayScala) + .settings( + name := """rest-api""", + libraryDependencies ++= Seq( + // Use cached module to store cached data automatically + cache, + + // Use Play filters to set up security headers and HSTS. + filters, + + "com.netaporter" %% "scala-uri" % "0.4.14", + + // Pull in bootstrap and jquery + "org.webjars" %% "webjars-play" % "2.5.0", + "org.webjars" % "bootstrap" % "3.3.6", + "org.webjars" % "jquery" % "2.2.3" + ) + ).aggregate(postModule, commentModule) + .dependsOn(postModule, commentModule) + +// Required to set "javaOptions" where system properties are set on another JVM +fork in ThisBuild := true + +// Set up the JVM to run on an additional HTTPS port on 9443 +javaOptions in ThisBuild ++= Seq("-Dhttps.port=9443") ++ strongHttpsSettings + +// These are not required, but are useful defaults to secure HTTPS in Java +lazy val strongHttpsSettings = Seq( + "-Djava.security.properties=disabledAlgorithms.properties", // should only use TLS 1.2 + "-Djdk.tls.ephemeralDHKeySize=2048", // decent DH key size + //"-Djavax.net.debug=ssl,handshake", // debugging + "-Djdk.tls.rejectClientInitiatedRenegotiation=true" // no client downgrade attacks +) diff --git a/conf/application.conf b/conf/application.conf new file mode 100644 index 000000000..473eae632 --- /dev/null +++ b/conf/application.conf @@ -0,0 +1,30 @@ + +play { + crypto.secret = "changeme" + + editor="http://localhost:63342/api/file/?file=%s&line=%s" + + http { + cookies.strict = true + session.secure = true + flash.secure = true + + forwarded.trustedProxies = ["::1", "127.0.0.1"] + } + + filters { + csrf { + cookie.secure = true + } + + hosts { + allowed = ["localhost:9443", "localhost:9000"] + } + } +} + +restapi.filters.hsts { + maxAge = 5 seconds + secureHost = "localhost" + securePort = 9443 +} diff --git a/conf/generated.keystore b/conf/generated.keystore new file mode 100644 index 0000000000000000000000000000000000000000..c250ef6eec2204a355dc27d579979a5a2b13a7bc GIT binary patch literal 3175 zcmeH}c{CL49>-@h7-Si5ys}Q$l6A(GwL*zx&tA4M#u%9yhU_u+B{7IXBCb8kzC_tB zrbvkFjCBfGLM6Sr=e_s5_uqT&IrpFbc+PX4&w2iM&iDMj&-om!AFTrb0MNe|FVV{- z5R37~__&ZUZe$-n-{TDc09tDq24VqBw(=@SbpQbDhopc>ND9dA5f}snfmi`GJxvYf zmjiT=98gy27=xI74j_$jY07u zd457>IGZtn;4P}_;)@YQp?Hv7KPe2(YV6{V@xh_-zNp`j@Sltg&Ti$6Ba2?c_>ytn z*neFG!&$uuXcsRJf-e~<%Ey6HMV?hcqLfuoNF{rIPDP~ZF+i#P3I2aiqW}?qjS0vE z08@aR03-zn15toLKxJil2%{}i6SJRBhglI~brA~z%p|Vv*gZ7+>QUd};tU_h@G539 zvvK5{MVz60i`>f-k-Pj<51hctMx6+9%`UIz2m}0@p>_`1O5PHQLgpF?+s-PmU)Z86 ztV`l4V_Wa*q!nL zQ4+^DI5Y|Z0|CIlCUiVV#|Z%etjnb~x{EEBX&HU=GlWA#RPE@E&o)<%!fKkOyi@@( zS^2V07%q1Gkmd767l!xLA-bj6i~AGEKoLC{qIySSPUGWrS*r=>!V0pLydUMvJ^%7* zDV)yHO;vAIoaBA^LXpoKzlWO9JtV;*!M~jtZ zYUVjTK)rKnLW(e-*7v5`?Qf1VT|3`&>@K$4N*rERAeXtfRhQCbido*SrFg4?olhU} zpe(sxy#{iPRD21tp*)l;R_bW<@Y0~9)1ENrjrHjJmtPmMU&s<9%KOtANpEC>4EvIa zw|g>|4_GY|Y2prEs5#8D4+`!S2 z1_l)^ZCC6liF{3(X0u71rB-Xz^PCekCos!iz4s3Lpm&G#TZl?eTB(f#5!-N(FyNN% zL3HJc9ionainp?VoChM_N$&@EP|(X@7wq)JmFLmeHj=jA?fy&U z2Cko7rs^u5Y`l&pzyt)s5Ds%V!iR^#-$;P6PMY4FzfFRt;OU~J(*b`A zoUZ7qa&(KAz1UI<*|dNJccDi)84hRGCdi2zUJa2Y)zt9;=|`NVlXP)1t?U9yJuZCwL^AyL#2l-kdy>Q1N#g4+~ z^oJ#q1+Z4l%>#W&B{U%a{LnIWj&9j-dP6wEH{xfdT7U2tw9 zsFbZz!%>uSSZZi||J%~zC<(JCiV+ET2ZR?ZiP{#un7@6kEAGOn(zBCUuv(xLx+-YECNGY~-&RsC!#%HM%dV$-gIF;hul-_ZUD zw6cB*na}3fra#50m@Y*2(DHKbOGaeza(c3awky1O?OTT*;w217fkh-+@<~+@0+`(E zR%@;!-H0zZY-ik4s!zZwI>=4CGni#b>2uwLyY*cM&5Wl_R^Bv8>h6sw4TY|2Q{is? zspFf6sdKU-$y?8NwVOJ1GS#G#Gxr?(zig{+aCzFU0xcfoDK9lA8(UBNNO>6a;l~9*wB#ktB1Ps+v3dFY9Y8&g@k`Abs%7yk51QhrIwe + + + + + + ${application.home:-.}/logs/application.log + + %date [%level] from %logger in %thread - %message%n%xException + + + + + + %coloredLevel %logger{15} - %message%n%xException{10} + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/routes b/conf/routes new file mode 100644 index 000000000..1f8ca5b71 --- /dev/null +++ b/conf/routes @@ -0,0 +1,7 @@ + +GET / controllers.HomeController.index() + +# Map static resources from the /public folder to the /assets URL path +GET /assets/*file controllers.Assets.at(path="/public", file) + +-> /posts post.PostRouter diff --git a/disabledAlgorithms.properties b/disabledAlgorithms.properties new file mode 100644 index 000000000..461b24085 --- /dev/null +++ b/disabledAlgorithms.properties @@ -0,0 +1,4 @@ +# disabledAlgorithms.properties +# http://sim.ivi.co/2013/11/harness-ssl-and-jsse-key-size-control.html +jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, EC keySize < 160, RSA keySize < 2048, DSA keySize < 2048 +jdk.certpath.disabledAlgorithms=MD2, MD4, MD5, EC keySize < 160, RSA keySize < 2048, DSA keySize < 2048 diff --git a/modules/comment/src/main/scala/com/lightbend/blog/comment/CommentData.scala b/modules/comment/src/main/scala/com/lightbend/blog/comment/CommentData.scala new file mode 100644 index 000000000..4703c30dd --- /dev/null +++ b/modules/comment/src/main/scala/com/lightbend/blog/comment/CommentData.scala @@ -0,0 +1,11 @@ +package com.lightbend.blog.comment + +final case class CommentData(id: CommentId, postId: String, body: String) + +class CommentId private(val underlying: String) extends AnyVal { + override def toString: String = underlying.toString +} + +object CommentId { + def apply(raw: String): CommentId = new CommentId(raw) +} diff --git a/modules/comment/src/main/scala/com/lightbend/blog/comment/CommentRepository.scala b/modules/comment/src/main/scala/com/lightbend/blog/comment/CommentRepository.scala new file mode 100644 index 000000000..94ab26fbf --- /dev/null +++ b/modules/comment/src/main/scala/com/lightbend/blog/comment/CommentRepository.scala @@ -0,0 +1,95 @@ +package com.lightbend.blog.comment + +import javax.inject.{Inject, Singleton} + +import scala.concurrent.{ExecutionContext, Future} + +/** + * A pure non-blocking interface for the Comment Repository. + */ +trait CommentRepository { + + def findByPost(postId: String): Future[Iterable[CommentData]] + + def list(): Future[Iterable[CommentData]] + + def get(id: CommentId): Future[Option[CommentData]] + +} + +/** + * Controls any resources owned by the comment repository. + */ +trait CommentRepositoryLifecycle { + + def stop(): Future[Unit] + +} + +/** + * A typed execution context for the Comment Repository. + * + * An execution context provides access to an Executor, but it's important + * that the thread pool is sized appropriately to the underlying implementation. + * For example, if you are using JDBC or a similar blocking model, then you will + * need a ThreadPoolExecutor with a fixed size equal to the maximum number of JDBC + * connections in the JDBC connection pool (i.e. HirakiCP). + * + * Because ExecutionContext is often passed round implicitly and it's not widely + * known, it's much better to ensure that anything Repository based has a custom + * strongly typed execution context so that an inappropriate ExecutionContext can't + * be used by accident. + */ +class CommentExecutionContext(val underlying: ExecutionContext) extends AnyVal + +/** + * A trivial implementation for the Comment Repository. + * + * The execution context is injected here and used with live Future (rather than + * Future.successful) to show how you would use this in a blocking or I/O bound + * implementation. + */ +@Singleton +class CommentRepositoryImpl @Inject()(cec: CommentExecutionContext) + extends CommentRepository with CommentRepositoryLifecycle { + private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) + + private implicit val ec: ExecutionContext = cec.underlying + + private val commentList = List( + CommentData(CommentId("1"), postId = "1", "comment 1"), + CommentData(CommentId("2"), postId = "1", "comment 2"), + CommentData(CommentId("3"), postId = "3", "comment 3"), + CommentData(CommentId("4"), postId = "3", "comment 4"), + CommentData(CommentId("5"), postId = "5", "comment 5") + ) + + override def list(): Future[Iterable[CommentData]] = { + Future { + logger.trace("list: ") + commentList + } + } + + override def findByPost(postId: String): Future[Iterable[CommentData]] = { + Future { + logger.trace(s"findByPost: postId = $postId") + commentList.filter(comment => comment.postId == postId) + } + } + + override def get(id: CommentId): Future[Option[CommentData]] = { + Future { + logger.trace(s"get: id = $id") + commentList.find(comment => comment.id == id) + } + } + + override def stop(): Future[Unit] = { + Future { + () + } + } +} + + diff --git a/modules/post/src/main/scala/com/lightbend/blog/post/PostData.scala b/modules/post/src/main/scala/com/lightbend/blog/post/PostData.scala new file mode 100644 index 000000000..b978c3083 --- /dev/null +++ b/modules/post/src/main/scala/com/lightbend/blog/post/PostData.scala @@ -0,0 +1,14 @@ +package com.lightbend.blog.post + +final case class PostData(id: PostId, title: String, body: String) + +class PostId private(val underlying: Int) extends AnyVal { + override def toString: String = underlying.toString +} + +object PostId { + def apply(raw: String): PostId = { + require(raw != null) + new PostId(Integer.parseInt(raw)) + } +} diff --git a/modules/post/src/main/scala/com/lightbend/blog/post/PostRepository.scala b/modules/post/src/main/scala/com/lightbend/blog/post/PostRepository.scala new file mode 100644 index 000000000..50849414e --- /dev/null +++ b/modules/post/src/main/scala/com/lightbend/blog/post/PostRepository.scala @@ -0,0 +1,86 @@ +package com.lightbend.blog.post + +import javax.inject.{Inject, Singleton} + +import scala.concurrent.{ExecutionContext, Future} + +/** + * A pure non-blocking interface for the PostRepository. + */ +trait PostRepository { + + def list(): Future[Iterable[PostData]] + + def get(id: PostId): Future[Option[PostData]] + + def stop(): Future[Unit] +} + +/** + * Controls any resources owned by the post repository. + */ +trait PostRepositoryLifecycle { + + def stop(): Future[Unit] + +} + +/** + * A typed execution context for the PostRepository. + * + * An execution context provides access to an Executor, but it's important + * that the thread pool is sized appropriately to the underlying implementation. + * For example, if you are using JDBC or a similar blocking model, then you will + * need a ThreadPoolExecutor with a fixed size equal to the maximum number of JDBC + * connections in the JDBC connection pool (i.e. HirakiCP). + * + * Because ExecutionContext is often passed round implicitly and it's not widely + * known, it's much better to ensure that anything Repository based has a custom + * strongly typed execution context so that an inappropriate ExecutionContext can't + * be used by accident. + */ +class PostExecutionContext(val underlying: ExecutionContext) extends AnyVal + +/** + * A trivial implementation for the Post Repository. + * + * The execution context is injected here and used with live Future (rather than + * Future.successful) to show how you would use this in a blocking or I/O bound + * implementation. + */ +@Singleton +class PostRepositoryImpl @Inject()(pec: PostExecutionContext) extends PostRepository with PostRepositoryLifecycle { + + private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) + + private implicit val ec: ExecutionContext = pec.underlying + + private val postList = List( + PostData(PostId("1"), "title 1", "blog post 1"), + PostData(PostId("2"), "title 2", "blog post 2"), + PostData(PostId("3"), "title 3", "blog post 3"), + PostData(PostId("4"), "title 4", "blog post 4"), + PostData(PostId("5"), "title 5", "blog post 5") + ) + + override def list(): Future[Iterable[PostData]] = { + Future { + logger.trace("list: ") + postList + } + } + + override def get(id: PostId): Future[Option[PostData]] = { + Future { + logger.trace(s"get: id = $id") + postList.find(post => post.id == id) + } + } + + override def stop(): Future[Unit] = { + Future { + () + } + } +} + diff --git a/project/Common.scala b/project/Common.scala new file mode 100644 index 000000000..0d4922fbb --- /dev/null +++ b/project/Common.scala @@ -0,0 +1,37 @@ +import sbt.Keys._ +import sbt._ +import sbt.plugins.JvmPlugin + +/** + * Settings that are comment to all the SBT projects + */ +object Common extends AutoPlugin { + override def trigger = allRequirements + override def requires: sbt.Plugins = JvmPlugin + + override def projectSettings = Seq( + organization := "com.lightbend.catapi", + version := "1.0-SNAPSHOT", + resolvers += Resolver.typesafeRepo("releases"), + javacOptions ++= Seq("-source", "1.8", "-target", "1.8"), + scalacOptions ++= Seq( + "-encoding", "UTF-8", // yes, this is 2 args + "-target:jvm-1.8", + "-deprecation", + "-feature", + "-unchecked", + "-Xlint", + "-Yno-adapted-args", + "-Ywarn-numeric-widen" + //"-Xfatal-warnings" + ), + scalaVersion := "2.11.8", + scalacOptions in Test ++= Seq("-Yrangepos"), + autoAPIMappings := true, + libraryDependencies ++= Seq( + "javax.inject" % "javax.inject" % "1", + "org.slf4j" % "slf4j-api" % "1.7.21", + "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test + ) + ) +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 000000000..43b8278c6 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.11 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 000000000..4fda36aca --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +// The Play plugin +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.4") diff --git a/public/stylesheets/.crunch b/public/stylesheets/.crunch new file mode 100644 index 000000000..8a06adb30 --- /dev/null +++ b/public/stylesheets/.crunch @@ -0,0 +1,21 @@ +{ + "files": { + "main.less": { + "engines": [ + { + "compiler": "less", + "output": "main.css", + "options": { + "compress": false, + "ieCompat": false, + "strictMath": false, + "strictUnits": false, + "javascriptEnabled": false, + "sourceMap": true + } + } + ], + "sources": [] + } + } +} \ No newline at end of file diff --git a/public/stylesheets/main.css b/public/stylesheets/main.css new file mode 100644 index 000000000..6d3d1a60d --- /dev/null +++ b/public/stylesheets/main.css @@ -0,0 +1,87 @@ +body { + padding-top: 60px; + background-color: #fbfcfe; +} +.navbar-default { + background-color: #797979; +} +.navbar-default .navbar-brand { + color: #eeeeee; +} +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + background-color: #868686; + color: white; +} +.navbar-default .navbar-brand.active { + background-color: #9f9f9f; + color: white; +} +.navbar-default .navbar-nav > li > a { + color: #eeeeee; +} +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + background-color: #868686; + color: white; +} +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + background-color: #9f9f9f; + color: white; +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + background-color: #9f9f9f; + color: white; +} +.navbar-default .dropdown-menu { + background-color: #9f9f9f; +} +.navbar-default .dropdown-menu > li > a { + color: white; +} +.navbar-default .dropdown-menu > li > a:hover { + background-color: #868686; + color: white; +} +.container { + max-width: 960px; +} +.footer { + background-color: #ececec; + margin-top: 20px; + padding: 20px 0; + line-height: 15px; +} +h3, +h4 { + margin-top: 35px; +} +h1 + h2, +h2 + h3, +h3 + h4 { + margin-top: 15px; +} +.highlight { + background-color: #FFFFF8; +} +pre code { + white-space: pre; +} +span.def { + color: #888; + font-size: 0.75em; + font-style: italic; + margin-left: 20px; +} +.error dd { + color: red; +} +.info { + color: blue; +} + +/*# sourceMappingURL=main.css.map */ \ No newline at end of file diff --git a/public/stylesheets/main.css.map b/public/stylesheets/main.css.map new file mode 100644 index 000000000..117d90ec6 --- /dev/null +++ b/public/stylesheets/main.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["main.less"],"names":[],"mappings":"AAWA;EACE,iBAAA;EACA,yBAAA;;AAaF;EACE,yBAAA;;AADF,eAEE;EACE,cAAA;;AACA,eAFF,cAEG;AAAQ,eAFX,cAEY;EARZ,yBAAA;EACA,YAAA;;AAUE,eALF,cAKG;EAhBH,yBAAA;EACA,YAAA;;AAoBE,eADF,YACI,KAAK;EACL,cAAA;;AACA,eAHJ,YACI,KAAK,IAEJ;AAAQ,eAHb,YACI,KAAK,IAEK;EAlBd,yBAAA;EACA,YAAA;;AAsBI,eARJ,YAOI,UAAU;AACP,eARP,YAOI,UAAU,IACN;AAAQ,eARhB,YAOI,UAAU,IACG;EA5BjB,yBAAA;EACA,YAAA;;AAgCI,eAbJ,YAYI,QAAQ;AACL,eAbP,YAYI,QAAQ,IACJ;AAAQ,eAbhB,YAYI,QAAQ,IACK;EAjCjB,yBAAA;EACA,YAAA;;AAQF,eA6BE;EACE,yBAAA;;AACA,eAFF,eAEI,KAAK;EACL,YAAA;;AACA,eAJJ,eAEI,KAAK,IAEJ;EArCL,yBAAA;EACA,YAAA;;AA2CF;EACE,gBAAA;;AAGF;EACE,yBAAA;EACA,gBAAA;EACA,eAAA;EACA,iBAAA;;AAGF;AAAI;EACF,gBAAA;;AAGF,EAAG;AAAM,EAAG;AAAM,EAAG;EACnB,gBAAA;;AAGF;EACE,yBAAA;;AAGF,GAAI;EACF,gBAAA;;AAGF,IAAI;EACF,WAAA;EACA,iBAAA;EACA,kBAAA;EACA,iBAAA;;AAGF,MAAO;EACL,UAAA;;AAGF;EACE,WAAA","sourcesContent":["@nav-bg-color: #797979;\n@nav-bg-color-hover: lighten(@nav-bg-color, 5%);\n@nav-bg-color-active: lighten(@nav-bg-color, 15%);\n@nav-color: #eeeeee;\n@nav-color-hover: white;\n@nav-color-active: white;\n@bg-color: #fbfcfe;\n@footer-bg-color: lighten(@nav-bg-color, 45%);\n\n@code-bg-color: #FFFFF8;\n\nbody {\n padding-top: 60px;\n background-color: @bg-color;\n}\n\n.navbar-mixin-active() {\n background-color: @nav-bg-color-active;\n color: @nav-color-active;\n}\n\n.navbar-mixin-hover() {\n background-color: @nav-bg-color-hover;\n color: white;\n}\n\n.navbar-default {\n background-color: @nav-bg-color;\n .navbar-brand {\n color: @nav-color;\n &:hover, &:focus {\n .navbar-mixin-hover();\n }\n &.active {\n .navbar-mixin-active();\n }\n }\n .navbar-nav {\n & > li > a {\n color: @nav-color;\n &:hover, &:focus {\n .navbar-mixin-hover();\n }\n }\n & > .active > a {\n &, &:hover, &:focus {\n .navbar-mixin-active();\n }\n }\n & > .open > a {\n &, &:hover, &:focus {\n .navbar-mixin-active();\n }\n }\n }\n .dropdown-menu {\n background-color: @nav-bg-color-active;\n & > li > a {\n color: @nav-color-active;\n &:hover {\n .navbar-mixin-hover();\n }\n }\n }\n}\n\n.container {\n max-width: 960px;\n}\n\n.footer {\n background-color: @footer-bg-color;\n margin-top: 20px;\n padding: 20px 0;\n line-height: 15px;\n}\n\nh3, h4 {\n margin-top: 35px;\n}\n\nh1 + h2, h2 + h3, h3 + h4 {\n margin-top: 15px;\n}\n\n.highlight {\n background-color: @code-bg-color;\n}\n\npre code {\n white-space: pre;\n}\n\nspan.def {\n color: #888;\n font-size: 0.75em;\n font-style: italic;\n margin-left: 20px;\n}\n\n.error dd {\n color: red;\n}\n\n.info {\n color: blue;\n}\n"]} \ No newline at end of file diff --git a/public/stylesheets/main.less b/public/stylesheets/main.less new file mode 100644 index 000000000..476a52089 --- /dev/null +++ b/public/stylesheets/main.less @@ -0,0 +1,107 @@ +@nav-bg-color: #797979; +@nav-bg-color-hover: lighten(@nav-bg-color, 5%); +@nav-bg-color-active: lighten(@nav-bg-color, 15%); +@nav-color: #eeeeee; +@nav-color-hover: white; +@nav-color-active: white; +@bg-color: #fbfcfe; +@footer-bg-color: lighten(@nav-bg-color, 45%); + +@code-bg-color: #FFFFF8; + +body { + padding-top: 60px; + background-color: @bg-color; +} + +.navbar-mixin-active() { + background-color: @nav-bg-color-active; + color: @nav-color-active; +} + +.navbar-mixin-hover() { + background-color: @nav-bg-color-hover; + color: white; +} + +.navbar-default { + background-color: @nav-bg-color; + .navbar-brand { + color: @nav-color; + &:hover, &:focus { + .navbar-mixin-hover(); + } + &.active { + .navbar-mixin-active(); + } + } + .navbar-nav { + & > li > a { + color: @nav-color; + &:hover, &:focus { + .navbar-mixin-hover(); + } + } + & > .active > a { + &, &:hover, &:focus { + .navbar-mixin-active(); + } + } + & > .open > a { + &, &:hover, &:focus { + .navbar-mixin-active(); + } + } + } + .dropdown-menu { + background-color: @nav-bg-color-active; + & > li > a { + color: @nav-color-active; + &:hover { + .navbar-mixin-hover(); + } + } + } +} + +.container { + max-width: 960px; +} + +.footer { + background-color: @footer-bg-color; + margin-top: 20px; + padding: 20px 0; + line-height: 15px; +} + +h3, h4 { + margin-top: 35px; +} + +h1 + h2, h2 + h3, h3 + h4 { + margin-top: 15px; +} + +.highlight { + background-color: @code-bg-color; +} + +pre code { + white-space: pre; +} + +span.def { + color: #888; + font-size: 0.75em; + font-style: italic; + margin-left: 20px; +} + +.error dd { + color: red; +} + +.info { + color: blue; +} diff --git a/test/ApplicationSpec.scala b/test/ApplicationSpec.scala new file mode 100644 index 000000000..4492801d5 --- /dev/null +++ b/test/ApplicationSpec.scala @@ -0,0 +1,23 @@ +import org.scalatestplus.play._ +import play.api.test._ +import play.api.test.Helpers._ + +class ApplicationSpec extends PlaySpec with OneAppPerTest { + + "Routes" should { + "send 404 on a bad request" in { + route(app, FakeRequest(GET, "/boum")).map(status(_)) mustBe Some(NOT_FOUND) + } + } + + "HomeController" should { + "render the index page" in { + val home = route(app, FakeRequest(GET, "/")).get + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + contentAsString(home) must include ("Your new application is ready.") + } + } + +} From 70b288cf4e8d8ccb40a499ea8fcf37dec4eefb2f Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Mon, 1 Aug 2016 17:45:12 -0700 Subject: [PATCH 02/73] Initial release --- .gitignore | 2 + README.md | 71 +++ app/ErrorHandler.scala | 30 +- app/Filters.scala | 16 - app/Module.scala | 25 +- app/RequestHandler.scala | 12 +- app/controllers/HomeController.scala | 4 +- .../StrictTransportSecurityFilter.scala | 88 ---- app/post/Post.scala | 45 -- app/post/PostAction.scala | 42 -- app/post/PostModule.scala | 65 --- app/post/PostRouter.scala | 151 ------- app/v1/post/PostAction.scala | 53 +++ app/v1/post/PostController.scala | 70 +++ app/v1/post/PostRepository.scala | 70 +++ app/v1/post/PostResourceHandler.scala | 70 +++ app/v1/post/PostRouter.scala | 33 ++ app/v1/post/package.scala | 17 + app/views/index.scala.html | 34 +- app/views/main.scala.html | 30 -- app/views/posts/create.scala.html | 1 - app/views/posts/index.scala.html | 23 - app/views/posts/main.scala.html | 3 - app/views/posts/row.scala.html | 24 - app/views/posts/show.scala.html | 35 -- build.sbt | 64 +-- conf/application.conf | 30 +- conf/logback.xml | 11 +- conf/routes | 4 +- conf/secure.conf | 38 ++ disabledAlgorithms.properties | 4 - docs/build.sbt | 8 + docs/src/main/paradox/appendix.md | 84 ++++ docs/src/main/paradox/index.md | 52 +++ docs/src/main/paradox/part-1/index.md | 414 ++++++++++++++++++ gatling/simulation/GatlingSpec.scala | 33 ++ .../lightbend/blog/comment/CommentData.scala | 11 - .../blog/comment/CommentRepository.scala | 95 ---- .../com/lightbend/blog/post/PostData.scala | 14 - .../lightbend/blog/post/PostRepository.scala | 86 ---- project/Common.scala | 9 +- project/build.properties | 2 +- project/plugins.sbt | 9 +- test/ApplicationSpec.scala | 23 - 44 files changed, 1120 insertions(+), 885 deletions(-) delete mode 100644 app/Filters.scala delete mode 100644 app/filters/StrictTransportSecurityFilter.scala delete mode 100644 app/post/Post.scala delete mode 100644 app/post/PostAction.scala delete mode 100644 app/post/PostModule.scala delete mode 100644 app/post/PostRouter.scala create mode 100644 app/v1/post/PostAction.scala create mode 100644 app/v1/post/PostController.scala create mode 100644 app/v1/post/PostRepository.scala create mode 100644 app/v1/post/PostResourceHandler.scala create mode 100644 app/v1/post/PostRouter.scala create mode 100644 app/v1/post/package.scala delete mode 100644 app/views/main.scala.html delete mode 100644 app/views/posts/create.scala.html delete mode 100644 app/views/posts/index.scala.html delete mode 100644 app/views/posts/main.scala.html delete mode 100644 app/views/posts/row.scala.html delete mode 100644 app/views/posts/show.scala.html create mode 100644 conf/secure.conf delete mode 100644 disabledAlgorithms.properties create mode 100644 docs/build.sbt create mode 100644 docs/src/main/paradox/appendix.md create mode 100644 docs/src/main/paradox/index.md create mode 100644 docs/src/main/paradox/part-1/index.md create mode 100644 gatling/simulation/GatlingSpec.scala delete mode 100644 modules/comment/src/main/scala/com/lightbend/blog/comment/CommentData.scala delete mode 100644 modules/comment/src/main/scala/com/lightbend/blog/comment/CommentRepository.scala delete mode 100644 modules/post/src/main/scala/com/lightbend/blog/post/PostData.scala delete mode 100644 modules/post/src/main/scala/com/lightbend/blog/post/PostRepository.scala delete mode 100644 test/ApplicationSpec.scala diff --git a/.gitignore b/.gitignore index eb372fc71..8fddb041c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ target /.project /.settings /RUNNING_PID + +/.vscode diff --git a/README.md b/README.md index e69de29bb..3c4e868fd 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,71 @@ +# Play REST API + +This is the project that goes along with the article. + +## Appendix + +### Running + +You need to download and install sbt for this application to run. + +Once you have sbt installed, the following at the command prompt will start up Play in development mode: + +``` +sbt run +``` + +Play will start up on the HTTP port at http://localhost:9000/. You don't need to reploy or reload anything -- changing any source code while the server is running will automatically recompile and hot-reload the application on the next HTTP request. + +### Usage + +If you call the same URL from the command line, you’ll see JSON. Using httpie, we can execute the command: + +``` +http --verbose http://localhost:9000/v1/posts +``` + +and get back: + +``` +GET /v1/posts HTTP/1.1 +``` + +Likewise, you can also send a POST directly as JSON: + +``` +http --verbose POST http://localhost:9000/v1/posts title="hello" body="world" +``` + +and get: + +``` +POST /v1/posts HTTP/1.1 +``` + +### Load Testing + +The best way to see what Play can do is to run a load test. We've included Gatling in this test project for integrated load testing. + +Start Play in production mode, by [staging the application](https://www.playframework.com/documentation/2.5.x/Deploying) and running the play script:s + +``` +sbt stage +cd target/universal/stage +bin/play-rest-api -Dplay.crypto.secret=testing +``` + +Then you'll start the Gatling load test up (it's already integrated into the project): + +``` +sbt gatling:test +``` + +For best results, start the gatling load test up on another machine so you do not have contending resources. You can edit the [Gatling simulation](http://gatling.io/docs/2.2.2/general/simulation_structure.html#simulation-structure), and change the numbers as appropriate. + +Once the test completes, you'll see an HTML file containing the load test chart: + +``` + ./rest-api/target/gatling/gatlingspec-1472579540405/index.html +``` + +That will contain your load test results. diff --git a/app/ErrorHandler.scala b/app/ErrorHandler.scala index 39dc41fea..1b6f2ea10 100644 --- a/app/ErrorHandler.scala +++ b/app/ErrorHandler.scala @@ -3,6 +3,7 @@ import javax.inject.{Inject, Provider, Singleton} import play.api._ import play.api.http.Status._ import play.api.http.{ContentTypes, DefaultHttpErrorHandler, HttpErrorHandlerExceptions} +import play.api.libs.json.Json import play.api.mvc.Results._ import play.api.mvc._ import play.api.routing.Router @@ -12,7 +13,8 @@ import scala.concurrent._ import scala.util.control.NonFatal /** - * Provides a stripped down error handler that does not use HTML in error pages. + * Provides a stripped down error handler that does not use HTML in error pages, and + * prints out debugging output. * * https://www.playframework.com/documentation/2.5.x/ScalaErrorHandling */ @@ -21,11 +23,11 @@ class ErrorHandler(environment: Environment, configuration: Configuration, sourceMapper: Option[SourceMapper] = None, optionRouter: => Option[Router] = None) - extends DefaultHttpErrorHandler(environment, configuration, sourceMapper, optionRouter) with AcceptExtractors with Rendering { + extends DefaultHttpErrorHandler(environment, configuration, sourceMapper, optionRouter) with RequestExtractors with Rendering { private val logger = org.slf4j.LoggerFactory.getLogger("application.ErrorHandler") - // This maps through Guice so that the above constructor... + // This maps through Guice so that the above constructor can call methods. @Inject def this(environment: Environment, configuration: Configuration, @@ -55,23 +57,11 @@ class ErrorHandler(environment: Environment, } } - override def onServerError(request: RequestHeader, exception: Throwable): Future[Result] = { - try { - val usefulException = HttpErrorHandlerExceptions.throwableToUsefulException(sourceMapper, - environment.mode == Mode.Prod, exception) - - logger.error( - s"! @${usefulException.id} - Internal server error, for (${request.method}) [${request.uri}] ->", - usefulException - ) + override protected def onDevServerError(request: RequestHeader, exception: UsefulException): Future[Result] = { + Future.successful(InternalServerError(Json.obj("exception" -> exception.toString))) + } - Future.successful { - InternalServerError - } - } catch { - case NonFatal(e) => - logger.error("Error while handling error", e) - Future.successful(InternalServerError) - } + override protected def onProdServerError(request: RequestHeader, exception: UsefulException): Future[Result] = { + Future.successful(InternalServerError) } } diff --git a/app/Filters.scala b/app/Filters.scala deleted file mode 100644 index 908bfd52c..000000000 --- a/app/Filters.scala +++ /dev/null @@ -1,16 +0,0 @@ -import javax.inject.Inject - -import filters.StrictTransportSecurityFilter -import play.api.http.DefaultHttpFilters -import play.filters.csrf.CSRFFilter -import play.filters.headers.SecurityHeadersFilter -import play.filters.hosts.AllowedHostsFilter - -/** - * https://www.playframework.com/documentation/latest/ScalaHttpFilters - */ -class Filters @Inject()(hstsFilter: StrictTransportSecurityFilter, - csrfFilter: CSRFFilter, - securityFilter: SecurityHeadersFilter, - allowedHostsFilter: AllowedHostsFilter) - extends DefaultHttpFilters(hstsFilter, csrfFilter, securityFilter, allowedHostsFilter) diff --git a/app/Module.scala b/app/Module.scala index c64d76d6d..caa9f2890 100644 --- a/app/Module.scala +++ b/app/Module.scala @@ -1,18 +1,21 @@ +import javax.inject._ + import com.google.inject.AbstractModule -import filters._ +import net.codingwell.scalaguice.ScalaModule +import play.api.{Configuration, Environment} +import v1.post._ + +import scala.concurrent.Future /** - * Provides a base Guice module for setting up some more components from configuration - * that aren't provided by Play itself. + * Sets up custom components for Play. + * + * https://www.playframework.com/documentation/2.5.x/ScalaDependencyInjection */ -class Module extends AbstractModule { - override def configure() = { - bind(classOf[StrictTransportSecurityConfig]).toProvider(classOf[StrictTransportSecurityConfigProvider]) +class Module(environment: Environment, + configuration: Configuration) extends AbstractModule with ScalaModule { - install(new post.PostModule) + override def configure() = { + bind[PostRepository].to[PostRepositoryImpl].in[Singleton] } } - - - - diff --git a/app/RequestHandler.scala b/app/RequestHandler.scala index c0ea69ce3..b2987672b 100644 --- a/app/RequestHandler.scala +++ b/app/RequestHandler.scala @@ -6,8 +6,11 @@ import play.api.routing.Router /** * Handles all requests. + * + * https://www.playframework.com/documentation/2.5.x/ScalaHttpRequestHandlers#extending-the-default-request-handler */ class RequestHandler @Inject()(router: Router, + postRouter: v1.post.PostRouter, errorHandler: HttpErrorHandler, configuration: HttpConfiguration, filters: HttpFilters) @@ -16,7 +19,7 @@ class RequestHandler @Inject()(router: Router, override def handlerForRequest(request: RequestHeader): (RequestHeader, Handler) = { super.handlerForRequest { // ensures that REST API does not need a trailing "/" - if (request.uri.startsWith("/posts")) { + if (isREST(request)) { addTrailingSlash(request) } else { request @@ -24,6 +27,13 @@ class RequestHandler @Inject()(router: Router, } } + private def isREST(request: RequestHeader) = { + request.uri match { + case uri: String if uri.contains("post") => true + case _ => false + } + } + private def addTrailingSlash(origReq: RequestHeader): RequestHeader = { if (! origReq.path.endsWith("/")) { val path = origReq.path + "/" diff --git a/app/controllers/HomeController.scala b/app/controllers/HomeController.scala index 2ff585a96..d269560ca 100644 --- a/app/controllers/HomeController.scala +++ b/app/controllers/HomeController.scala @@ -2,10 +2,12 @@ package controllers import play.api.mvc.{Action, Controller} +/** + * A very small controller that renders a home page. + */ class HomeController extends Controller { def index = Action { implicit request => Ok(views.html.index()) } - } diff --git a/app/filters/StrictTransportSecurityFilter.scala b/app/filters/StrictTransportSecurityFilter.scala deleted file mode 100644 index 40b56446a..000000000 --- a/app/filters/StrictTransportSecurityFilter.scala +++ /dev/null @@ -1,88 +0,0 @@ -package filters - -import javax.inject._ - -import akka.stream.Materializer -import com.google.inject.AbstractModule -import com.netaporter.uri.Uri -import com.typesafe.config.Config -import play.api.Configuration -import play.api.mvc.{Filter, RequestHeader, Result, Results} - -import scala.concurrent.{ExecutionContext, Future} - -/** - * Sends a Strict Transport Security Filter header to clients over HTTPS. - * - * https://tools.ietf.org/html/rfc6797 - */ -class StrictTransportSecurityFilter @Inject()(config: StrictTransportSecurityConfig) - (implicit val mat: Materializer, ec: ExecutionContext) - extends Filter { - - def apply(nextFilter: RequestHeader => Future[Result])(request: RequestHeader): Future[Result] = { - if (request.secure) { - nextFilter(request).map { result => - result.withHeaders(hstsHeader) - } - } else { - Future.successful { - val secureURL = generateSecureURL(request) - // Use a permanent redirect to keep people on HTTPS - Results.PermanentRedirect(secureURL) - } - } - } - - def hstsHeader: (String, String) = { - ("Strict-Transport-Security", s"""max-age=${config.maxAge.getSeconds}""") - } - - def generateSecureURL(request: RequestHeader) = { - import com.netaporter.uri.dsl._ - val uri: Uri = request.uri - val secureUri = uri - .withScheme("https") - .withHost(config.secureHost) - .withPort(config.securePort) - secureUri.toString() - } - -} - -/** - * Configuration DTO for the HSTS filter - * - * @param maxAge how long the HSTS header max-age should be - * @param secureHost the secure hostname - * @param securePort the secure port - */ -final case class StrictTransportSecurityConfig(maxAge: java.time.Duration, - secureHost: String, - securePort: Int) - -object StrictTransportSecurityConfig { - def fromConfiguration(config: Config): StrictTransportSecurityConfig = { - val hstsConfig = config.getConfig("restapi.filters.hsts") - - val maxAge = hstsConfig.getDuration("maxAge") - val secureHost = hstsConfig.getString("secureHost") - val securePort = hstsConfig.getInt("securePort") - - StrictTransportSecurityConfig(maxAge, secureHost, securePort) - } -} - -/** - * Pulls in setting for StrictTransportSecurityFilter via conf/application.conf - * - * @param configuration the application.conf configuration - */ -@Singleton -class StrictTransportSecurityConfigProvider @Inject()(configuration: Configuration) - extends Provider[StrictTransportSecurityConfig] { - - lazy val get: StrictTransportSecurityConfig = { - StrictTransportSecurityConfig.fromConfiguration(configuration.underlying) - } -} diff --git a/app/post/Post.scala b/app/post/Post.scala deleted file mode 100644 index 1e50e5d07..000000000 --- a/app/post/Post.scala +++ /dev/null @@ -1,45 +0,0 @@ -package post - -import com.lightbend.blog.post.{PostData, PostId} -import play.api.libs.json._ - -case class Comment(body: String) - -object Comment { - implicit val implicitWrites = new Writes[Comment] { - def writes(comment: Comment): JsValue = { - Json.obj( - "body" -> comment.body - ) - } - } -} - - -case class Post(id: String, link: String, title: String, body: String, comments: Seq[Comment]) - -object Post { - - implicit val implicitWrites = new Writes[Post] { - def writes(post: Post): JsValue = { - Json.obj( - "id" -> post.id, - "link" -> post.link, - "title" -> post.title, - "body" -> post.body, - "comments" -> Json.toJson(post.comments) - ) - } - } - - def apply(p: PostData, comments: Seq[Comment]): Post = { - Post(p.id.toString, link(p.id), p.title, p.body, comments) - } - - private def link(id: PostId): String = { - import com.netaporter.uri.dsl._ - val url = "/posts" / id.toString - url.toString() - } - -} diff --git a/app/post/PostAction.scala b/app/post/PostAction.scala deleted file mode 100644 index 26119a0d0..000000000 --- a/app/post/PostAction.scala +++ /dev/null @@ -1,42 +0,0 @@ -package post - -import javax.inject.Inject - -import com.lightbend.blog.post.PostId -import play.api.i18n.{Messages, MessagesApi} -import play.api.mvc._ - -import scala.concurrent.{ExecutionContext, Future} - -/** - * The default action for a post request. - * - * This is the place to put logging, metrics, and general custom headers. - */ -class PostAction @Inject()(val messagesApi: MessagesApi)(implicit ec: ExecutionContext) extends ActionBuilder[PostRequest] { - private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) - - type PostRequestBlock[A] = (PostRequest[A]) => Future[Result] - - override def invokeBlock[A](request: Request[A], block: PostRequestBlock[A]): Future[Result] = { - val messages = messagesApi.preferred(request) - val postRequest = new PostRequest(request, messages) - block(postRequest).map { result => - if (logger.isTraceEnabled) { - logger.trace(s"postAction: request = $request, result = ${result.header}") - } - result - } - } - -} - -/** - * A wrapped request that can contain Post information. - * - * This is commonly used to hold request-specific information like - * security credentials and localized Messages - */ -class PostRequest[A](request: Request[A], val messages: Messages) extends WrappedRequest(request) { - override def toString(): String = s"[${request.uri}]" -} diff --git a/app/post/PostModule.scala b/app/post/PostModule.scala deleted file mode 100644 index c4e2aa79d..000000000 --- a/app/post/PostModule.scala +++ /dev/null @@ -1,65 +0,0 @@ -package post - -import javax.inject.{Inject, Provider, Singleton} - -import akka.actor.ActorSystem -import com.google.inject.AbstractModule -import com.lightbend.blog.comment._ -import com.lightbend.blog.post._ -import play.api.inject.ApplicationLifecycle - -class PostModule extends AbstractModule { - override def configure(): Unit = { - bind(classOf[CommentRepository]).toProvider(classOf[CommentRepositoryProvider]) - bind(classOf[PostRepository]).toProvider(classOf[PostRepositoryProvider]) - } -} - -/** - * Provides a post repository for Play. - */ -@Singleton -class PostRepositoryProvider @Inject()(applicationLifecycle: ApplicationLifecycle, - actorSystem: ActorSystem) - extends Provider[PostRepository] { - - lazy val get: PostRepository = { - val repo = new PostRepositoryImpl(executionContext) - // Hooks the repository lifecycle to Play's lifecycle, so any resources are shutdown - applicationLifecycle.addStopHook { () => - repo.stop() - } - repo - } - - private def executionContext: PostExecutionContext = { - //val ec = actorSystem.dispatchers.lookup("post.dispatcher") - val ec = actorSystem.dispatchers.defaultGlobalDispatcher - new PostExecutionContext(ec) - } -} - -/** - * Provides a comment repository for Play. - */ -@Singleton -class CommentRepositoryProvider @Inject()(applicationLifecycle: ApplicationLifecycle, - actorSystem: ActorSystem) - extends Provider[CommentRepository] { - - lazy val get: CommentRepository = { - val repo = new CommentRepositoryImpl(executionContext) - // Hooks the repository lifecycle to Play's lifecycle, so any resources are shutdown - applicationLifecycle.addStopHook { () => - repo.stop() - } - repo - } - - private def executionContext: CommentExecutionContext = { - //val ec = actorSystem.dispatchers.lookup("post.dispatcher") - val ec = actorSystem.dispatchers.defaultGlobalDispatcher - new CommentExecutionContext(ec) - } - -} diff --git a/app/post/PostRouter.scala b/app/post/PostRouter.scala deleted file mode 100644 index 570d96add..000000000 --- a/app/post/PostRouter.scala +++ /dev/null @@ -1,151 +0,0 @@ -package post - -import javax.inject.Inject - -import com.lightbend.blog.comment._ -import com.lightbend.blog.post._ -import play.api.cache.Cached -import play.api.http.Status -import play.api.libs.json.Json -import play.api.mvc._ -import play.api.routing.Router.Routes -import play.api.routing.SimpleRouter -import play.api.routing.sird._ - -import scala.concurrent.Future - -/** - * Takes HTTP requests and produces JSON or HTML responses - * from a repository providing data. - */ -class PostRouter @Inject()(cached: Cached, - action: PostAction, - postRepository: PostRepository, - commentRepository: CommentRepository) - extends SimpleRouter with AcceptExtractors with Rendering { - - // A trampoline ties an execution context to the currently running thread. - // It should only be used for short running bits of non-blocking code. - import play.api.libs.iteratee.Execution.Implicits.trampoline - - private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) - - private val cacheDuration = 500 - - override def routes: Routes = { - - case GET(p"/") => - cached.status(rh => s"${rh.method} ${rh.uri}", Status.OK, cacheDuration) { - action.async { implicit request => - renderPosts() - } - } - - case HEAD(p"/") => - action.async { implicit request => - renderPosts() - } - - case GET(p"/$id") => - cached.status(rh => s"${rh.method} ${rh.uri}", Status.OK, cacheDuration) { - action.async { implicit request => - renderPost(PostId(id)) - } - } - - case HEAD(p"/$id") => - action.async { implicit request => - renderPost(PostId(id)) - } - } - - // https://www.playframework.com/documentation/2.5.x/ScalaContentNegotiation - - private def renderPost[A](id: PostId)(implicit request: PostRequest[A]): Future[Result] = { - logger.trace("renderPost: ") - // Find a single item from the repository - render.async { - - case Accepts.Json() if request.method == "GET" => - // Query the repository for post with this id - postRepository.get(id).flatMap { - case Some(p) => - findComments(p.id).map { comments => - val post = Post(p, comments) - val json = Json.toJson(post) - Results.Ok(json) - } - case None => - Future.successful(Results.NotFound) - } - - case Accepts.Html() if request.method == "GET" => - // Query the repository for post with this id - postRepository.get(id).flatMap { - case Some(p) => - findComments(p.id).map { comments => - val post = Post(p, comments) - Results.Ok(views.html.posts.show(post)) - } - case None => - Future.successful(Results.NotFound) - } - - case Accepts.Json() & Accepts.Html() if request.method == "HEAD" => - postRepository.get(id).flatMap { - case Some(p) => - Future.successful(Results.Ok) - case None => - Future.successful(Results.NotFound) - } - - } - } - - private def renderPosts[A]()(implicit request: PostRequest[A]): Future[Result] = { - render.async { - - case Accepts.Json() if request.method == "GET" => - // Query the repository for available posts - postRepository.list().flatMap { postDataList => - findPosts(postDataList).map { posts => - val json = Json.toJson(posts) - Results.Ok(json) - } - } - - case Accepts.Html() if request.method == "GET" => - // Query the repository for available posts - postRepository.list().flatMap { postDataList => - findPosts(postDataList).map { posts => - Results.Ok(views.html.posts.index(posts)) - } - } - - case Accepts.Json() & Accepts.Html() if request.method == "HEAD" => - // HEAD has no body, so just say hi - Future.successful(Results.Ok) - - } - } - - private def findPosts(postDataList: Iterable[PostData]): Future[Iterable[Post]] = { - // Get an Iterable[Future[Post]] containing comments - val listOfFutures = postDataList.map { p => - findComments(p.id).map { comments => - Post(p, comments) - } - } - - // Flip it into a single Future[Iterable[Post]] - Future.sequence(listOfFutures) - } - - private def findComments(postId: PostId): Future[Seq[Comment]] = { - // Find all the comments for this post - commentRepository.findByPost(postId.toString).map { comments => - comments.map(c => Comment(c.body)).toSeq - } - } - -} diff --git a/app/v1/post/PostAction.scala b/app/v1/post/PostAction.scala new file mode 100644 index 000000000..29541f387 --- /dev/null +++ b/app/v1/post/PostAction.scala @@ -0,0 +1,53 @@ +package v1.post + +import javax.inject.Inject + +import play.api.http.HttpVerbs +import play.api.i18n.{Messages, MessagesApi} +import play.api.mvc._ + +import scala.concurrent.{ExecutionContext, Future} + +/** + * A wrapped request for post resources. + * + * This is commonly used to hold request-specific information like + * security credentials, and useful shortcut methods. + */ +class PostRequest[A](request: Request[A], + val messages: Messages) + extends WrappedRequest(request) + +/** + * The default action for the Post resource. + * + * This is the place to put logging, metrics, to augment + * the request with contextual data, and manipulate the + * result. + */ +class PostAction @Inject()(messagesApi: MessagesApi) + (implicit ec: ExecutionContext) + extends ActionBuilder[PostRequest] with HttpVerbs { + + type PostRequestBlock[A] = PostRequest[A] => Future[Result] + + private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) + + override def invokeBlock[A](request: Request[A], block: PostRequestBlock[A]): Future[Result] = { + if (logger.isTraceEnabled()) { + logger.trace(s"invokeBlock: request = $request") + } + + val messages = messagesApi.preferred(request) + val future = block(new PostRequest(request, messages)) + + future.map { result => + request.method match { + case GET | HEAD => + result.withHeaders("Cache-Control" -> s"max-age: 100") + case other => + result + } + } + } +} diff --git a/app/v1/post/PostController.scala b/app/v1/post/PostController.scala new file mode 100644 index 000000000..9eeef7dbd --- /dev/null +++ b/app/v1/post/PostController.scala @@ -0,0 +1,70 @@ +package v1.post + +import javax.inject.Inject + +import play.api.data.Form +import play.api.libs.json.Json +import play.api.mvc._ + +import scala.concurrent.{ExecutionContext, Future} + +case class PostFormInput(title: String, body: String) + +/** + * Takes HTTP requests and produces JSON. + */ +class PostController @Inject()(action: PostAction, + handler: PostResourceHandler) + (implicit ec: ExecutionContext) + extends Controller { + + private val form: Form[PostFormInput] = { + import play.api.data.Forms._ + + Form( + mapping( + "title" -> nonEmptyText, + "body" -> text + )(PostFormInput.apply)(PostFormInput.unapply) + ) + } + + def index: Action[AnyContent] = { + action.async { implicit request => + handler.find.map { posts => + Ok(Json.toJson(posts)) + } + } + } + + def process: Action[AnyContent] = { + action.async { implicit request => + processJsonPost() + } + } + + def show(id: String): Action[AnyContent] = { + action.async { implicit request => + handler.lookup(id).map { post => + Ok(Json.toJson(post)) + } + } + } + + private def processJsonPost[A]()(implicit request: PostRequest[A]): Future[Result] = { + def failure(badForm: Form[PostFormInput]) = { + Future.successful(BadRequest(badForm.errorsAsJson)) + } + + def success(input: PostFormInput) = { + handler.create(input).map { post => + Created(Json.toJson(post)) + .withHeaders(LOCATION -> post.link) + } + } + + form.bindFromRequest().fold(failure, success) + } +} + + diff --git a/app/v1/post/PostRepository.scala b/app/v1/post/PostRepository.scala new file mode 100644 index 000000000..33167b6c4 --- /dev/null +++ b/app/v1/post/PostRepository.scala @@ -0,0 +1,70 @@ +package v1.post + +import javax.inject.{Inject, Singleton} + +import scala.concurrent.Future + +final case class PostData(id: PostId, title: String, body: String) + +class PostId private(val underlying: Int) extends AnyVal { + override def toString: String = underlying.toString +} + +object PostId { + def apply(raw: String): PostId = { + require(raw != null) + new PostId(Integer.parseInt(raw)) + } +} + +/** + * A pure non-blocking interface for the PostRepository. + */ +trait PostRepository { + def create(data: PostData): Future[PostId] + + def list(): Future[Iterable[PostData]] + + def get(id: PostId): Future[Option[PostData]] +} + + +/** + * A trivial implementation for the Post Repository. + */ +@Singleton +class PostRepositoryImpl @Inject() extends PostRepository { + + private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) + + private val postList = List( + PostData(PostId("1"), "title 1", "blog post 1"), + PostData(PostId("2"), "title 2", "blog post 2"), + PostData(PostId("3"), "title 3", "blog post 3"), + PostData(PostId("4"), "title 4", "blog post 4"), + PostData(PostId("5"), "title 5", "blog post 5") + ) + + override def list(): Future[Iterable[PostData]] = { + Future.successful { + logger.trace(s"list: ") + postList + } + } + + override def get(id: PostId): Future[Option[PostData]] = { + Future.successful { + logger.trace(s"get: id = $id") + postList.find(post => post.id == id) + } + } + + def create(data: PostData): Future[PostId] = { + Future.successful { + logger.trace(s"create: data = $data") + data.id + } + } + +} + diff --git a/app/v1/post/PostResourceHandler.scala b/app/v1/post/PostResourceHandler.scala new file mode 100644 index 000000000..2c042c571 --- /dev/null +++ b/app/v1/post/PostResourceHandler.scala @@ -0,0 +1,70 @@ +package v1.post + +import javax.inject.{Inject, Provider} + +import scala.concurrent.{ExecutionContext, Future} + +import play.api.libs.json._ + + +/** + * DTO for displaying post information. + */ +case class PostResource(id: String, + link: String, + title: String, + body: String) + +object PostResource { + + /** + * Mapping to write a PostResource out as a JSON value. + */ + implicit val implicitWrites = new Writes[PostResource] { + def writes(post: PostResource): JsValue = { + Json.obj( + "id" -> post.id, + "link" -> post.link, + "title" -> post.title, + "body" -> post.body + ) + } + } +} + +/** + * Controls access to the backend data, returning [[PostResource]] + */ +class PostResourceHandler @Inject()(routerProvider: Provider[PostRouter], + postRepository: PostRepository) + (implicit ec: ExecutionContext) +{ + + def create(postInput: PostFormInput): Future[PostResource] = { + val data = PostData(PostId("999"), postInput.title, postInput.body) + // We don't actually create the post, so return what we have + postRepository.create(data).map { id => + createPostResource(data) + } + } + + def lookup(id: String): Future[Option[PostResource]] = { + val postFuture = postRepository.get(PostId(id)) + postFuture.map { maybePostData => + maybePostData.map { postData => + createPostResource(postData) + } + } + } + + def find: Future[Iterable[PostResource]] = { + postRepository.list().map { postDataList => + postDataList.map(postData => createPostResource(postData)) + } + } + + private def createPostResource(p: PostData): PostResource = { + PostResource(p.id.toString, routerProvider.get.link(p.id), p.title, p.body) + } + +} diff --git a/app/v1/post/PostRouter.scala b/app/v1/post/PostRouter.scala new file mode 100644 index 000000000..f9172c93a --- /dev/null +++ b/app/v1/post/PostRouter.scala @@ -0,0 +1,33 @@ +package v1.post + +import javax.inject.Inject + +import play.api.routing.Router.Routes +import play.api.routing.SimpleRouter +import play.api.routing.sird._ + +/** + * Routes and URLs to the PostResource controller. + */ +class PostRouter @Inject()(controller: PostController) + extends SimpleRouter { + val prefix = "/v1/posts" + + def link(id: PostId): String = { + import com.netaporter.uri.dsl._ + val url = prefix / id.toString + url.toString() + } + + override def routes: Routes = { + case GET(p"/") => + controller.index + + case POST(p"/") => + controller.process + + case GET(p"/$id") => + controller.show(id) + } + +} diff --git a/app/v1/post/package.scala b/app/v1/post/package.scala new file mode 100644 index 000000000..d37eac281 --- /dev/null +++ b/app/v1/post/package.scala @@ -0,0 +1,17 @@ +package v1 + +import play.api.i18n.Messages +import play.api.mvc.Result + +/** + * Package object for post. This is a good place to put implicit conversions. + */ +package object post { + + /** + * Converts between PostRequest and Messages automatically. + */ + implicit def requestToMessages[A](implicit r: PostRequest[A]): Messages = { + r.messages + } +} diff --git a/app/views/index.scala.html b/app/views/index.scala.html index 2f0be67d4..e0709781e 100644 --- a/app/views/index.scala.html +++ b/app/views/index.scala.html @@ -1,13 +1,21 @@ -@()(implicit request: Request[_]) - -@main("REST API Example") { -

- Example text. -

- - -} +@() + + + + + + Play REST API + + + +

Play REST API

+ +

+ This is a placeholder page to show you the REST API. +

+ + + + diff --git a/app/views/main.scala.html b/app/views/main.scala.html deleted file mode 100644 index 3a79c8c91..000000000 --- a/app/views/main.scala.html +++ /dev/null @@ -1,30 +0,0 @@ -@(title: String)(content: Html)(implicit r: Request[_]) - - - - - - @title - - - - - - -
- @content -
- - - - - - diff --git a/app/views/posts/create.scala.html b/app/views/posts/create.scala.html deleted file mode 100644 index 7433bf44e..000000000 --- a/app/views/posts/create.scala.html +++ /dev/null @@ -1 +0,0 @@ -@()(implicit request: post.PostRequest[_]) diff --git a/app/views/posts/index.scala.html b/app/views/posts/index.scala.html deleted file mode 100644 index 317cf9956..000000000 --- a/app/views/posts/index.scala.html +++ /dev/null @@ -1,23 +0,0 @@ -@(posts: Iterable[post.Post])(implicit request: post.PostRequest[_]) - -@main("Posts") { - -
-

Posts

-
- - - - - - - - - - @for(post <- posts) { - @row(post) - } - -
LinkTitleBodyComments
- -} diff --git a/app/views/posts/main.scala.html b/app/views/posts/main.scala.html deleted file mode 100644 index 5093f68b4..000000000 --- a/app/views/posts/main.scala.html +++ /dev/null @@ -1,3 +0,0 @@ -@(title: String)(content: Html)(implicit request: post.PostRequest[_]) - -@views.html.main(title)(content) diff --git a/app/views/posts/row.scala.html b/app/views/posts/row.scala.html deleted file mode 100644 index af2c2539e..000000000 --- a/app/views/posts/row.scala.html +++ /dev/null @@ -1,24 +0,0 @@ -@(p: post.Post)(implicit request: post.PostRequest[_]) - - - - - @{p.link} - - - - @{p.title} - - - - @{p.body} - - - - @for(c <- p.comments) { - @{c.body} -
- } - - - diff --git a/app/views/posts/show.scala.html b/app/views/posts/show.scala.html deleted file mode 100644 index 80df738f4..000000000 --- a/app/views/posts/show.scala.html +++ /dev/null @@ -1,35 +0,0 @@ -@(p: post.Post)(implicit request: post.PostRequest[_]) - -@main("Posts") { - -
-

Post @{p.id}

-
- -
-
ID
-
@{p.id}
-
-
-
Link
-
@{p.link}
-
-
-
Title
-
@{p.title}
-
-
-
Body
-
@{p.body}
-
-
-
Comment
-
- @for(c <- p.comments) { - @{c.body} -
- } -
-
- -} diff --git a/build.sbt b/build.sbt index 12adcc699..2d074ec1c 100644 --- a/build.sbt +++ b/build.sbt @@ -1,47 +1,31 @@ import sbt.Keys._ -// Set up two data repositories that will serve us raw data which we have to -// present through a REST API. These don't know about Play, and can be run -// on their own without including any Play classes. They each contain a -// minimal Guice binding which Play calls through "play.modules.enabled". +lazy val GatlingTest = config("gatling") extend Test -// Provides post repository to the system. -lazy val postModule = (project in file("modules/post")).enablePlugins(Common) - -// Provides comment repository to the system. -lazy val commentModule = (project in file("modules/comment")).enablePlugins(Common) - -// The Play project itself, which aggregates everything. -lazy val root = (project in file(".")).enablePlugins(Common, PlayScala) +// The Play project itself +lazy val root = (project in file(".")).enablePlugins(Common, PlayScala, GatlingPlugin) + .configs(GatlingTest) + .settings(inConfig(GatlingTest)(Defaults.testSettings): _*) .settings( - name := """rest-api""", + name := """play-rest-api""", libraryDependencies ++= Seq( - // Use cached module to store cached data automatically - cache, - - // Use Play filters to set up security headers and HSTS. - filters, - + // A useful URL construction library "com.netaporter" %% "scala-uri" % "0.4.14", - // Pull in bootstrap and jquery - "org.webjars" %% "webjars-play" % "2.5.0", - "org.webjars" % "bootstrap" % "3.3.6", - "org.webjars" % "jquery" % "2.2.3" - ) - ).aggregate(postModule, commentModule) - .dependsOn(postModule, commentModule) - -// Required to set "javaOptions" where system properties are set on another JVM -fork in ThisBuild := true - -// Set up the JVM to run on an additional HTTPS port on 9443 -javaOptions in ThisBuild ++= Seq("-Dhttps.port=9443") ++ strongHttpsSettings - -// These are not required, but are useful defaults to secure HTTPS in Java -lazy val strongHttpsSettings = Seq( - "-Djava.security.properties=disabledAlgorithms.properties", // should only use TLS 1.2 - "-Djdk.tls.ephemeralDHKeySize=2048", // decent DH key size - //"-Djavax.net.debug=ssl,handshake", // debugging - "-Djdk.tls.rejectClientInitiatedRenegotiation=true" // no client downgrade attacks -) + // Use scala-guice + "net.codingwell" %% "scala-guice" % "4.1.0", + + // Add scalatest in for test framework + "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test, + + // Add Gatling in for laod testing + "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % Test, + "io.gatling" % "gatling-test-framework" % "2.2.2" % Test + ), + scalaSource in GatlingTest := baseDirectory.value / "/gatling/simulation" + ) + +// Documentation for this project: +// sbt "project docs" "~ paradox" +// open docs/target/paradox/site/index.html +lazy val docs = (project in file("docs")).enablePlugins(ParadoxPlugin) \ No newline at end of file diff --git a/conf/application.conf b/conf/application.conf index 473eae632..e1212f531 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -1,30 +1,2 @@ +include "secure" -play { - crypto.secret = "changeme" - - editor="http://localhost:63342/api/file/?file=%s&line=%s" - - http { - cookies.strict = true - session.secure = true - flash.secure = true - - forwarded.trustedProxies = ["::1", "127.0.0.1"] - } - - filters { - csrf { - cookie.secure = true - } - - hosts { - allowed = ["localhost:9443", "localhost:9000"] - } - } -} - -restapi.filters.hsts { - maxAge = 5 seconds - secureHost = "localhost" - securePort = 9443 -} diff --git a/conf/logback.xml b/conf/logback.xml index e017b5ea7..fa73fe6fa 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -10,6 +10,14 @@ + + + ${application.home:-.}/logs/metrics.log + + %date [%level] from %logger in %thread - %message%n%xException + + + %coloredLevel %logger{15} - %message%n%xException{10} @@ -27,9 +35,6 @@ - - - diff --git a/conf/routes b/conf/routes index 1f8ca5b71..91006f391 100644 --- a/conf/routes +++ b/conf/routes @@ -1,7 +1,7 @@ +GET / controllers.HomeController.index -GET / controllers.HomeController.index() +-> /v1/posts v1.post.PostRouter # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.at(path="/public", file) --> /posts post.PostRouter diff --git a/conf/secure.conf b/conf/secure.conf new file mode 100644 index 000000000..ff9d0720a --- /dev/null +++ b/conf/secure.conf @@ -0,0 +1,38 @@ +# Set up Play for HTTPS and locked down allowed hosts. +# Nothing in here is required for REST, but it's a good default. +play { + crypto.secret = "changeme" + + http { + cookies.strict = true + + session.secure = true + session.httpOnly = true + + flash.secure = true + flash.httpOnly = true + + forwarded.trustedProxies = ["::1", "127.0.0.1"] + } + + i18n { + langCookieSecure = true + langCookieHttpOnly = true + } + + filters { + csrf { + cookie.secure = true + } + + hosts { + allowed = ["localhost:9443", "localhost:9000"] + } + + hsts { + maxAge = 1 minute # don't interfere with other projects + secureHost = "localhost" + securePort = 9443 + } + } +} diff --git a/disabledAlgorithms.properties b/disabledAlgorithms.properties deleted file mode 100644 index 461b24085..000000000 --- a/disabledAlgorithms.properties +++ /dev/null @@ -1,4 +0,0 @@ -# disabledAlgorithms.properties -# http://sim.ivi.co/2013/11/harness-ssl-and-jsse-key-size-control.html -jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, EC keySize < 160, RSA keySize < 2048, DSA keySize < 2048 -jdk.certpath.disabledAlgorithms=MD2, MD4, MD5, EC keySize < 160, RSA keySize < 2048, DSA keySize < 2048 diff --git a/docs/build.sbt b/docs/build.sbt new file mode 100644 index 000000000..7caa997ac --- /dev/null +++ b/docs/build.sbt @@ -0,0 +1,8 @@ +// You will need private bintray credentials to publish this with Lightbend theme +// credentials += Credentials("Bintray", "dl.bintray.com", "", "") +//resolvers += "bintray-typesafe-internal-maven-releases" at "https://dl.bintray.com/typesafe/internal-maven-releases/" +//libraryDependencies += "com.lightbend.paradox" % "paradox-theme-lightbend" % "0.2.1-TH2" +//paradoxTheme := Some("com.lightbend.paradox" % "paradox-theme-lightbend" % "0.2.1-TH2") + +// Uses the out of the box generic theme. +paradoxTheme := Some(builtinParadoxTheme("generic")) \ No newline at end of file diff --git a/docs/src/main/paradox/appendix.md b/docs/src/main/paradox/appendix.md new file mode 100644 index 000000000..a81ef378d --- /dev/null +++ b/docs/src/main/paradox/appendix.md @@ -0,0 +1,84 @@ + +# Appendix + +This appendix covers how to download, run, use and load test Play. + +## Requirements + +You will need a JDK 1.8 that is more recent than b20. You can download the JDK from [here](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html). + +You will need to have git installed. + +## Downloading + +You can download the example project from Github: + +``` +git clone https://github.com/playframework/play-rest-api.git +``` + +## Running + +You need to download and install sbt for this application to run. You can do that by going to the [sbt download page](http://www.scala-sbt.org/download.html) and following the instructions for your platform. + +Once you have sbt installed, the following at the command prompt will download any required library dependencies, and start up Play in development mode: + +``` +sbt run +``` + +Play will start up on the HTTP port at http://localhost:9000/. You don't need to reploy or reload anything -- changing any source code while the server is running will automatically recompile and hot-reload the application on the next HTTP request. You can read more about using Play [here](https://www.playframework.com/documentation/2.5.x/PlayConsole). + +## Usage + +If you call the same URL from the command line, you’ll see JSON. Using [httpie](https://httpie.org/), we can execute the command: + +``` +http --verbose http://localhost:9000/v1/posts +``` + +and get back: + +``` +GET /v1/posts HTTP/1.1 +``` + +Likewise, you can also send a POST directly as JSON: + +``` +http --verbose POST http://localhost:9000/v1/posts title="hello" body="world" +``` + +and get: + +``` +POST /v1/posts HTTP/1.1 +``` + +## Load Testing + +The best way to see what Play can do is to run a load test. We've included Gatling in this test project for integrated load testing. + +Start Play in production mode, by [staging the application](https://www.playframework.com/documentation/2.5.x/Deploying) and running the play scripts: + +``` +sbt stage +cd target/universal/stage +bin/play-rest-api -Dplay.crypto.secret=testing +``` + +Then you'll start the Gatling load test up (it's already integrated into the project): + +``` +sbt gatling:test +``` + +For best results, start the gatling load test up on another machine so you do not have contending resources. You can edit the [Gatling simulation](http://gatling.io/docs/2.2.2/general/simulation_structure.html#simulation-structure), and change the numbers as appropriate. + +Once the test completes, you'll see an HTML file containing the load test chart: + +``` + ./rest-api/target/gatling/gatlingspec-1472579540405/index.html +``` + +That will contain your load test results. diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md new file mode 100644 index 000000000..1b6ea5b49 --- /dev/null +++ b/docs/src/main/paradox/index.md @@ -0,0 +1,52 @@ +# Making a REST API with Play + +This is a multi-part guide to walk you through how to make a RESTful API with JSON using [Play 2.5](https://playframework.com). + +We’ll demonstrate with a “best practices” REST API that you can clone from [Github](http://github.com/playframework/play-rest-api): + +``` +git clone https://github.com/playframework/play-rest-api.git +``` + +This example is in Scala, but Play also has a [Java API](https://www.playframework.com/documentation/2.5.x/JavaHome) which looks and acts just like the [Scala API](https://www.playframework.com/documentation/2.5.x/ScalaHome). For instructions on running and using the project, please see the [appendix](appendix.md). This project also comes with an integrated [Gatling](http://gatling.io/) load test -- again, instructions are in the [appendix](appendix.md). + +Note that there’s more involved in a REST API -- monitoring, representation, and managing access to back end resources -- that we'll cover in subsequent posts. But first, let's address why Play is so effective as a REST API. + +## When to use Play + +Play makes a good REST API implementation because Play does the right thing out of the box. Play makes simple things easy, makes hard things possible, and encourages code that scales because it works in sympathy with the JVM and the underlying hardware. But "safe and does the right thing" is the boring answer. + +The fun answer is that Play is **fast**. + +In fact, Play is so fast that you have to turn off machines so that the rest of your architecture can keep up. The Hootsuite team was able to **reduce the number of servers by 80%** by [switching to Play](https://www.lightbend.com/resources/case-studies-and-stories/how-hootsuite-modernized-its-url-shortener). if you deploy Play with the same infrastructure that you were using for other web frameworks, you are effectively staging a denial of service attack against your own database. + +Play is fast because Play is **built on reactive bedrock**. Play starts from a reactive core, and builds on reactive principles all the way from the ground. Play uses a small thread pool, and breaks network packets into a stream of small chunks of data keeps those threads fed with HTTP requests, which means it's fast. and feeds those through Akka Streams, the Reactive Streams implementation designed by the people who invented [Reactive Streams](http://www.reactive-streams.org/) and wrote the [Reactive Manifesto](http://www.reactivemanifesto.org/). + +Linkedin uses Play throughout its infrastructure. It wins on all [four quadrants of scalability](http://www.slideshare.net/brikis98/the-play-framework-at-linkedin/128-Outline1_Getting_started_with_Play2) ([video](https://youtu.be/8z3h4Uv9YbE)). Play's average "request per second" comes in around [tens of k on a basic quad core w/o any intentional tuning](https://twitter.com/kevinbowling1/status/764188720140398592) -- and it only gets better. + +Play provides an easy to use MVC paradigm, including hot-reloading without any JVM bytecode magic or container overhead. Startup time for a developer on Play was **reduced by roughly 7 times** for [Walmart Canada](https://www.lightbend.com/resources/case-studies-and-stories/walmart-boosts-conversions-by-20-with-lightbend-reactive-platform), and using Play **reduced development times by 2x to 3x**. + +Play combines this with a **reactive programming API** that lets you write async, non-blocking code in a straightforward fashion without worrying about complex and confusing "callback hell." In either Java or Scala, Play works on the same principle: leverage the asynchronous computation API that the language provides to you. In Play, you work with `java.util.concurrent.CompletionStage` or `scala.concurrent.Future` API directly, and Play passes that asynchronous computation back through the framework. + +Finally, Play is modular and extensible. Play works with multiple runtime and compile time dependency injection frameworks like [Guice](https://www.playframework.com/documentation/2.5.x/ScalaDependencyInjection), [Macwire](https://di-in-scala.github.io/), [Dagger](https://github.com/esfand-r/play-java-dagger-dependency-injection#master), and leverages DI principles to integrate authentication and authorization frameworks built on top of Play. + +## Community + +To learn more about Play, check out the [Play tutorials](https://playframework.com/documentation/2.5.x/Tutorials) and see more examples and blog posts about Play, including streaming [Server Side Events](https://github.com/playframework/play-streaming-scala) and first class [WebSocket support](https://github.com/playframework/play-websocket-scala). + +To get more involved and if you have questions, join the [mailing list](https://groups.google.com/forum/#!forum/play-framework) at and follow [PlayFramework on Twitter](https://twitter.com/playframework). + +## Microservices vs REST APIs + +One thing to note here is that although this guide covers how to make a REST API in Play, it only covers Play itself and deploying Play. Building a REST API in Play does not automatically make it a "microservice" because it does not cover larger scale concerns about microservices such as ensuring resiliency, persistence, distributing work over multiple machines, or monitoring. + +For full scale microservices, you want [Lagom](http://www.lagomframework.com/), which builds on top of Play -- a microservices framework for dealing with the ["data on the outside"](https://blog.acolyer.org/2016/09/13/data-on-the-outside-versus-data-on-the-inside/) problem, set up with persistence and service APIs that ensure that the service always stays up and responsive even in the face of chaos monkeys and network partitions. + +With that caveat, let's start working with Play! + +@@@index + +* [Basics](part-1/index.md) +* [Appendix](appendix.md) + +@@@ diff --git a/docs/src/main/paradox/part-1/index.md b/docs/src/main/paradox/part-1/index.md new file mode 100644 index 000000000..aa0d9cfc9 --- /dev/null +++ b/docs/src/main/paradox/part-1/index.md @@ -0,0 +1,414 @@ +# Basics + +This guide will walk you through how to make a RESTful API with JSON using [Play 2.5](https://playframework.com). + +To see the associated Github project, please go to [http://github.com/playframework/play-rest-api](http://github.com/playframework/play-rest-api). We're going to be showing an already working Play project with most of the code available under the "app/v1" directory. There will be several different versions of the same project as this series expands, so you can compare different versions of the controller against each other. + +To run Play on your own local computer, please see the instructions in the [appendix](../appendix.md). + +## Modelling a Post Resource + +We'll start off with a REST API that displays information for blog posts. This is a resource that will contain all the data to start with -- it will have a unique id, a URL hyperlink that indicates the canonical location of the resource, the title of the blog post, and the body of the blog post. + +This resource is represented as a single case class in the Play application [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostResourceHandler.scala#L13): + +```scala +case class PostResource(id: String, link: String, + title: String, body: String) +``` + +This resource is mapped to and from JSON on the front end using Play, and is mapped to and from a persistent datastore on the backend using a handler. + +Play handles HTTP routing and representation for the REST API and makes it easy to write a non-blocking, asynchronous API that is an order of magnitude more efficient than other web application frameworks. + +## Routing Post Requests + +Play has two complimentary routing mechanisms. In the conf directory, there's a file called "routes" which contains entries for the HTTP method and a relative URL path, and points it at an action in a controller. + +``` +GET / controllers.HomeController.index() +``` + +This is useful for situations where a front end service is rendering HTML. However, Play also contains a more powerful routing DSL that we will use for the REST API. + +For every HTTP request starting with `/v1/posts`, Play routes it to a dedicated PostRouter class to handle the Posts resource, through the [`conf/routes`](https://github.com/playframework/play-rest-api/blob/master/conf/routes) file: + +``` +-> /v1/posts v1.post.PostRouter +``` + +The PostRouter examines the URL and extracts data to pass along to the controller [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostRouter.scala): + +```scala +package v1.post +import javax.inject.Inject + +import play.api.mvc._ +import play.api.routing.Router.Routes +import play.api.routing.SimpleRouter +import play.api.routing.sird._ + +class PostRouter @Inject()(controller: PostController) + extends SimpleRouter +{ + override def routes: Routes = { + case GET(p"/") => + controller.index + + case POST(p"/") => + controller.process + + case GET(p"/$id") => + controller.show(id) + } +} +``` + +Play’s [routing DSL](https://www.playframework.com/documentation/2.5.x/ScalaSirdRouter) (aka SIRD) shows how data can be extracted from the URL concisely and cleanly. SIRD is based around HTTP methods and a string interpolated extractor object – this means that when we type the string “/$id” and prefix it with “p”, then the path parameter id can be extracted and used in the block. Naturally, there are also operators to extract queries, regular expressions, and even add custom extractors. If you have a URL as follows: + +``` +/posts/?sort=ascending&count=5 +``` + +then you can extract the "sort" and "count" parameters in a single line: + +```scala +GET("/" ? q_?"sort=$sort" & q_?”count=${ int(count) }") +``` + +SIRD is especially useful in a REST API where there can be many possible query parameters. Cake Solutions covers SIRD in more depth in a [fantastic blog post](http://www.cakesolutions.net/teamblogs/all-you-need-to-know-about-plays-routing-dsl). + +## Using a Controller + +The PostRouter has a PostController injected into it through standard [JSR-330 dependency injection](https://github.com/google/guice/wiki/JSR330) [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostRouter.scala#L12): + +```scala +class PostRouter @Inject()(controller: PostController) + extends SimpleRouter +``` + +Before heading into the PostController, let's discuss how controllers work in Play. + +A controller [handles the work of processing](https://www.playframework.com/documentation/2.5.x/ScalaActions) the HTTP request into an HTTP response in the context of an Action: it's where page rendering and HTML form processing happen. A controller extends [`play.api.mvc.Controller`](https://playframework.com/documentation/2.5.x/api/scala/index.html#play.api.mvc.Controller), which contains a number of utility methods and constants for working with HTTP. In particular, a Controller contains Result objects such as Ok and Redirect, and HeaderNames like ACCEPT. + +The methods in a controller consist of a method returning an [Action](https://playframework.com/documentation/2.5.x/api/scala/index.html#play.api.mvc.Action). The Action provides the "engine" to Play. + +Using the action, the controller passes in a block of code that takes a [`Request`](https://playframework.com/documentation/2.5.x/api/scala/index.html#play.api.mvc.Request) passed in as implicit – this means that any in-scope method that takes an implicit request as a parameter will use this request automatically. Then, the block must return either a [`Result`](https://playframework.com/documentation/2.5.x/api/scala/index.html#play.api.mvc.Result), or a [`Future[Result]`](http://www.scala-lang.org/api/current/index.html#scala.concurrent.Future), depending on whether or not the action was called as `action { ... }` or [`action.async { ... }`](https://www.playframework.com/documentation/2.5.x/ScalaAsync#How-to-create-a-Future[Result]). + +### Handling GET Requests + + +Here's a simple example of a Controller: + +```scala +import javax.inject.Inject +import play.api.mvc._ + +import scala.concurrent._ + +class MyController extends Controller { + + def index1: Action[AnyContent] = { + Action { implicit request => + val r: Result = Ok("hello world") + r + } + } + + def asyncIndex: Action[AnyContent] = { + Action.async { implicit request => + val r: Future[Result] = Future.successful(Ok("hello world")) + r + } + } +} +``` + +In this example, `index1` and `asyncIndex` have exactly the same behavior. Internally, it makes no difference whether we call `Result` or `Future[Result]` -- Play is non-blocking all the way through. + +However, if you're already working with `Future`, async makes it easier to pass that `Future` around. You can read more about this in the [handling asynchronous results](https://www.playframework.com/documentation/2.5.x/ScalaAsync) section of the Play documentation. + +The PostController methods dealing with GET requests is [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostController.scala): + +```scala +class PostController @Inject()(action: PostAction, + handler: PostResourceHandler) + (implicit ec: ExecutionContext) + extends Controller { + + def index: Action[AnyContent] = { + action.async { implicit request => + handler.find.map { posts => + Ok(Json.toJson(posts)) + } + } + } + + def show(id: String): Action[AnyContent] = { + action.async { implicit request => + handler.lookup(id).map { post => + Ok(Json.toJson(post)) + } + } + } + +} +``` + +Let's take `show` as an example. Here, the action defines a workflow for a request that maps to a single resource, i.e. `GET /v1/posts/123`. + +```scala +def show(id: String): Action[AnyContent] = { + action.async { implicit request => + handler.lookup(id).map { post => + Ok(Json.toJson(post)) + } + } +} +``` + +The id is passed in as a String, and the handler looks up and returns a `PostResource`. The `Ok()` sends back a `Result` with a status code of "200 OK", containing a response body consisting of the `PostResource` serialized as JSON. + +### Processing Form Input + +Handling a POST request is also easy and is done through the `process` method: + +```scala +class PostController @Inject()(action: PostAction, + handler: PostResourceHandler) + (implicit ec: ExecutionContext) + extends Controller { + + private val form: Form[PostFormInput] = { + import play.api.data.Forms._ + + Form( + mapping( + "title" -> nonEmptyText, + "body" -> text + )(PostFormInput.apply)(PostFormInput.unapply) + ) + } + + def process: Action[AnyContent] = { + action.async { implicit request => + processJsonPost() + } + } + + private def processJsonPost[A]()(implicit request: PostRequest[A]): Future[Result] = { + def failure(badForm: Form[PostFormInput]) = { + Future.successful(BadRequest(badForm.errorsAsJson)) + } + + def success(input: PostFormInput) = { + handler.create(input).map { post => + Created(Json.toJson(post)) + .withHeaders(LOCATION -> post.link) + } + } + + form.bindFromRequest().fold(failure, success) + } +} +``` + +Here, the `process` action is an action wrapper, and `processJsonPost` does most of the work. In `processJsonPost`, we get to the [form processing](https://www.playframework.com/documentation/2.5.x/ScalaForms) part of the code. + +Here, `form.bindFromRequest()` will map input from the HTTP request to a [`play.api.data.Form`](https://www.playframework.com/documentation/2.5.x/api/scala/index.html#play.api.data.Form), and handles form validation and error reporting. + +If the `PostFormInput` passes validation, it's passed to the resource handler, using the `success` method. If the form processing fails, then the `failure` method is called and the `FormError` is returned in JSON format. + +```scala +private val form: Form[PostFormInput] = { + import play.api.data.Forms._ + + Form( + mapping( + "title" -> nonEmptyText, + "body" -> text + )(PostFormInput.apply)(PostFormInput.unapply) + ) +} +``` + +The form binds to the HTTP request using the names in the mapping -- "title" and "body" to the `PostFormInput` case class [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostController.scala#L11). + +```scala +case class PostFormInput(title: String, body: String) +``` + +That's all you need to do to handle a basic web application! As with most things, there are more details that need to be handled. That's where creating custom Actions comes in. + +## Using Actions + +We saw in the `PostController` that each method is connected to an Action through the "action.async" method [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostController.scala#L32): + +```scala + def index: Action[AnyContent] = { + action.async { implicit request => + handler.find.map { posts => + Ok(Json.toJson(posts)) + } + } + } +``` + +The action.async takes a function, and comes from the class parameter "action", which we can see is of type `PostAction` [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostController.scala#L16): + +```scala +class PostController @Inject()(action: PostAction [...]) +``` + +`PostAction` is an ActionBuilder. It is involved in each action in the controller -- it mediates the paperwork involved with processing a request into a response, adding context to the request and enriching the response with headers and cookies. ActionBuilders are essential for handling authentication, authorization and monitoring functionality. + +ActionBuilders work through a process called [action composition](https://www.playframework.com/documentation/2.5.x/ScalaActionsComposition). The ActionBuilder class has a method called `invokeBlock` that takes in a `Request` and a function (also known as a block, lambda or closure) that accepts a `Request` of a given type, and produces a `Future[Result]`. + +So, if you want to work with an `Action` that has a "FooRequest" that has a Foo attached, it's easy: + +```scala +class FooRequest[A](request: Request[A], val foo: Foo) extends WrappedRequest(request) + +class FooAction extends ActionBuilder[FooRequest] { + type FooRequestBlock[A] = FooRequest[A] => Future[Result] + + override def invokeBlock[A](request: Request[A], block: FooRequestBlock[A]) = { + block(new FooRequest[A](request, new Foo)) + } +} +``` + +You create an `ActionBuilder[FooRequest]`, override `invokeBlock`, and then call the function with an instance of `FooRequest`. + +Then, when you call `fooAction`, the request type is `FooRequest`: + +```scala +fooAction { request: FooRequest => + Ok(request.foo.toString) +} +``` + +And `request.foo` will be added automatically. + +You can keep composing action builders inside each other, so you don't have to layer all the functionality in one single ActionBuilder, or you can create a custom `ActionBuilder` for each package you work with, according to your taste. For the purposes of this blog post, we'll keep everything together in a single class. + +You can see PostAction [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostAction.scala): + +```scala +class PostRequest[A](request: Request[A], + val messages: Messages) + extends WrappedRequest(request) + +class PostAction @Inject()(messagesApi: MessagesApi) + (implicit ec: ExecutionContext) + extends ActionBuilder[PostRequest] with HttpVerbs { + + type PostRequestBlock[A] = PostRequest[A] => Future[Result] + + private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) + + override def invokeBlock[A](request: Request[A], + block: PostRequestBlock[A]) = { + if (logger.isTraceEnabled()) { + logger.trace(s"invokeBlock: request = $request") + } + + val messages = messagesApi.preferred(request) + val future = block(new PostRequest(request, messages)) + + future.map { result => + request.method match { + case GET | HEAD => + result.withHeaders("Cache-Control" -> s"max-age: 100") + case other => + result + } + } + } +} +``` + +`PostAction` does a couple of different things here. The first thing it does is to log the request as it comes in. Next, it pulls out the localized `Messages` for the request, and adds that to a `PostRequest` , and runs the function, returning a `Future[Result]`. + +When the future completes, we map the result so we can replace it with a slightly different result. We compare the result's method against `HttpVerbs`, and if it's a GET or HEAD, we append a Cache-Control header with a max-age directive. We need an `ExecutionContext` for `future.map` operations, so we pass in the default execution context implicitly at the top of the class. + +Now that we have a `PostRequest`, we can call "request.messages" explicitly from any action in the controller, for free, and we can append information to the result after the user action has been completed. + +## Converting resources with PostResourceHandler + +The `PostResourceHandler` is responsible for converting backend data from a repository into a `PostResource`. We won't go into detail on the `PostRepository` details for now, only that it returns data in an backend-centric state. + +A REST resource has information that a backend repository does not -- it knows about the operations available on the resource, and contains URI information that a single backend may not have. As such, we want to be able to change the representation that we use internally without changing the resource that we expose publicly. + +You can see the `PostResourceHandler` [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostResourceHandler.scala): + +```scala +class PostResourceHandler @Inject()(routerProvider: Provider[PostRouter], + postRepository: PostRepository) + (implicit ec: ExecutionContext) +{ + + def create(postInput: PostFormInput): Future[PostResource] = { + val data = PostData(PostId("999"), postInput.title, postInput.body) + postRepository.create(data).map { id => + createPostResource(data) + } + } + + def lookup(id: String): Future[Option[PostResource]] = { + val postFuture = postRepository.get(PostId(id)) + postFuture.map { maybePostData => + maybePostData.map { postData => + createPostResource(postData) + } + } + } + + def find: Future[Iterable[PostResource]] = { + postRepository.list().map { postDataList => + postDataList.map(postData => createPostResource(postData)) + } + } + + private def createPostResource(p: PostData): PostResource = { + PostResource(p.id.toString, routerProvider.get.link(p.id), p.title, p.body) + } + +} +``` + +Here, it's a straight conversion in `createPostResource`, with the only hook being that the router provides the resource's URL, since it's something that `PostData` doesn't have itself. + +## Rendering Content as JSON + +Play handles the work of converting a `PostResource` through [Play JSON](https://www.playframework.com/documentation/2.5.x/ScalaJson). Play JSON provides a DSL that looks up the conversion for the `PostResource` singleton object, so you don't need to declare it at the use point. + +You can see the `PostResource` object [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostResourceHandler.scala#L18): + +```scala +object PostResource { + implicit val implicitWrites = new Writes[PostResource] { + def writes(post: PostResource): JsValue = { + Json.obj( + "id" -> post.id, + "link" -> post.link, + "title" -> post.title, + "body" -> post.body) + } + } +} +``` + +Once the implicit is defined in the companion object, then it will be looked up automatically when handed an instance of the class. This means that when the controller converts to JSON, the conversion will just work, without any additional imports or setup. + +```scala +val json: JsValue = Json.toJson(post) +``` + +Play JSON also has options to incrementally parse and generate JSON for continuously streaming JSON responses. + +## Summary + +We've shown how to easy it is to put together a basic REST API in Play. Using this code, we can put together backend data, convert it to JSON and transfer it over HTTP with a minimum of fuss. + +In the next guide, we'll discuss content representation and provide an HTML interface that exists alongside the JSON API. diff --git a/gatling/simulation/GatlingSpec.scala b/gatling/simulation/GatlingSpec.scala new file mode 100644 index 000000000..599d0a01c --- /dev/null +++ b/gatling/simulation/GatlingSpec.scala @@ -0,0 +1,33 @@ +package simulation + +import io.gatling.core.Predef._ +import io.gatling.http.Predef._ +import scala.concurrent.duration._ +import scala.language.postfixOps + +// run with "sbt gatling:test" on another machine so you don't have resources contending. +// http://gatling.io/docs/2.2.2/general/simulation_structure.html#simulation-structure +class GatlingSpec extends Simulation { + + // change this to another machine, make sure you have Play running in producion mode + // i.e. sbt stage / sbt dist and running the script + val httpConf = http.baseURL("http://localhost:9000/v1/posts") + + val readClients = scenario("Clients").exec(Index.refreshManyTimes) + + setUp( + // For reference, this hits 25% CPU on a 5820K with 32 GB, running both server and load test. + // In general, you want to ramp up load slowly, and measure with a JVM that has been "warmed up": + // https://groups.google.com/forum/#!topic/gatling/mD15aj-fyo4 + readClients.inject(rampUsers(10000) over (100 seconds)).protocols(httpConf) + ) +} + +object Index { + + def refreshAfterOneSecond = exec(http("Index").get("/").check(status.is(200))).pause(1) + + val refreshManyTimes = repeat(10000) { + refreshAfterOneSecond + } +} diff --git a/modules/comment/src/main/scala/com/lightbend/blog/comment/CommentData.scala b/modules/comment/src/main/scala/com/lightbend/blog/comment/CommentData.scala deleted file mode 100644 index 4703c30dd..000000000 --- a/modules/comment/src/main/scala/com/lightbend/blog/comment/CommentData.scala +++ /dev/null @@ -1,11 +0,0 @@ -package com.lightbend.blog.comment - -final case class CommentData(id: CommentId, postId: String, body: String) - -class CommentId private(val underlying: String) extends AnyVal { - override def toString: String = underlying.toString -} - -object CommentId { - def apply(raw: String): CommentId = new CommentId(raw) -} diff --git a/modules/comment/src/main/scala/com/lightbend/blog/comment/CommentRepository.scala b/modules/comment/src/main/scala/com/lightbend/blog/comment/CommentRepository.scala deleted file mode 100644 index 94ab26fbf..000000000 --- a/modules/comment/src/main/scala/com/lightbend/blog/comment/CommentRepository.scala +++ /dev/null @@ -1,95 +0,0 @@ -package com.lightbend.blog.comment - -import javax.inject.{Inject, Singleton} - -import scala.concurrent.{ExecutionContext, Future} - -/** - * A pure non-blocking interface for the Comment Repository. - */ -trait CommentRepository { - - def findByPost(postId: String): Future[Iterable[CommentData]] - - def list(): Future[Iterable[CommentData]] - - def get(id: CommentId): Future[Option[CommentData]] - -} - -/** - * Controls any resources owned by the comment repository. - */ -trait CommentRepositoryLifecycle { - - def stop(): Future[Unit] - -} - -/** - * A typed execution context for the Comment Repository. - * - * An execution context provides access to an Executor, but it's important - * that the thread pool is sized appropriately to the underlying implementation. - * For example, if you are using JDBC or a similar blocking model, then you will - * need a ThreadPoolExecutor with a fixed size equal to the maximum number of JDBC - * connections in the JDBC connection pool (i.e. HirakiCP). - * - * Because ExecutionContext is often passed round implicitly and it's not widely - * known, it's much better to ensure that anything Repository based has a custom - * strongly typed execution context so that an inappropriate ExecutionContext can't - * be used by accident. - */ -class CommentExecutionContext(val underlying: ExecutionContext) extends AnyVal - -/** - * A trivial implementation for the Comment Repository. - * - * The execution context is injected here and used with live Future (rather than - * Future.successful) to show how you would use this in a blocking or I/O bound - * implementation. - */ -@Singleton -class CommentRepositoryImpl @Inject()(cec: CommentExecutionContext) - extends CommentRepository with CommentRepositoryLifecycle { - private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) - - private implicit val ec: ExecutionContext = cec.underlying - - private val commentList = List( - CommentData(CommentId("1"), postId = "1", "comment 1"), - CommentData(CommentId("2"), postId = "1", "comment 2"), - CommentData(CommentId("3"), postId = "3", "comment 3"), - CommentData(CommentId("4"), postId = "3", "comment 4"), - CommentData(CommentId("5"), postId = "5", "comment 5") - ) - - override def list(): Future[Iterable[CommentData]] = { - Future { - logger.trace("list: ") - commentList - } - } - - override def findByPost(postId: String): Future[Iterable[CommentData]] = { - Future { - logger.trace(s"findByPost: postId = $postId") - commentList.filter(comment => comment.postId == postId) - } - } - - override def get(id: CommentId): Future[Option[CommentData]] = { - Future { - logger.trace(s"get: id = $id") - commentList.find(comment => comment.id == id) - } - } - - override def stop(): Future[Unit] = { - Future { - () - } - } -} - - diff --git a/modules/post/src/main/scala/com/lightbend/blog/post/PostData.scala b/modules/post/src/main/scala/com/lightbend/blog/post/PostData.scala deleted file mode 100644 index b978c3083..000000000 --- a/modules/post/src/main/scala/com/lightbend/blog/post/PostData.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.lightbend.blog.post - -final case class PostData(id: PostId, title: String, body: String) - -class PostId private(val underlying: Int) extends AnyVal { - override def toString: String = underlying.toString -} - -object PostId { - def apply(raw: String): PostId = { - require(raw != null) - new PostId(Integer.parseInt(raw)) - } -} diff --git a/modules/post/src/main/scala/com/lightbend/blog/post/PostRepository.scala b/modules/post/src/main/scala/com/lightbend/blog/post/PostRepository.scala deleted file mode 100644 index 50849414e..000000000 --- a/modules/post/src/main/scala/com/lightbend/blog/post/PostRepository.scala +++ /dev/null @@ -1,86 +0,0 @@ -package com.lightbend.blog.post - -import javax.inject.{Inject, Singleton} - -import scala.concurrent.{ExecutionContext, Future} - -/** - * A pure non-blocking interface for the PostRepository. - */ -trait PostRepository { - - def list(): Future[Iterable[PostData]] - - def get(id: PostId): Future[Option[PostData]] - - def stop(): Future[Unit] -} - -/** - * Controls any resources owned by the post repository. - */ -trait PostRepositoryLifecycle { - - def stop(): Future[Unit] - -} - -/** - * A typed execution context for the PostRepository. - * - * An execution context provides access to an Executor, but it's important - * that the thread pool is sized appropriately to the underlying implementation. - * For example, if you are using JDBC or a similar blocking model, then you will - * need a ThreadPoolExecutor with a fixed size equal to the maximum number of JDBC - * connections in the JDBC connection pool (i.e. HirakiCP). - * - * Because ExecutionContext is often passed round implicitly and it's not widely - * known, it's much better to ensure that anything Repository based has a custom - * strongly typed execution context so that an inappropriate ExecutionContext can't - * be used by accident. - */ -class PostExecutionContext(val underlying: ExecutionContext) extends AnyVal - -/** - * A trivial implementation for the Post Repository. - * - * The execution context is injected here and used with live Future (rather than - * Future.successful) to show how you would use this in a blocking or I/O bound - * implementation. - */ -@Singleton -class PostRepositoryImpl @Inject()(pec: PostExecutionContext) extends PostRepository with PostRepositoryLifecycle { - - private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) - - private implicit val ec: ExecutionContext = pec.underlying - - private val postList = List( - PostData(PostId("1"), "title 1", "blog post 1"), - PostData(PostId("2"), "title 2", "blog post 2"), - PostData(PostId("3"), "title 3", "blog post 3"), - PostData(PostId("4"), "title 4", "blog post 4"), - PostData(PostId("5"), "title 5", "blog post 5") - ) - - override def list(): Future[Iterable[PostData]] = { - Future { - logger.trace("list: ") - postList - } - } - - override def get(id: PostId): Future[Option[PostData]] = { - Future { - logger.trace(s"get: id = $id") - postList.find(post => post.id == id) - } - } - - override def stop(): Future[Unit] = { - Future { - () - } - } -} - diff --git a/project/Common.scala b/project/Common.scala index 0d4922fbb..eb63fabe7 100644 --- a/project/Common.scala +++ b/project/Common.scala @@ -10,7 +10,7 @@ object Common extends AutoPlugin { override def requires: sbt.Plugins = JvmPlugin override def projectSettings = Seq( - organization := "com.lightbend.catapi", + organization := "com.lightbend.restapi", version := "1.0-SNAPSHOT", resolvers += Resolver.typesafeRepo("releases"), javacOptions ++= Seq("-source", "1.8", "-target", "1.8"), @@ -27,11 +27,6 @@ object Common extends AutoPlugin { ), scalaVersion := "2.11.8", scalacOptions in Test ++= Seq("-Yrangepos"), - autoAPIMappings := true, - libraryDependencies ++= Seq( - "javax.inject" % "javax.inject" % "1", - "org.slf4j" % "slf4j-api" % "1.7.21", - "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test - ) + autoAPIMappings := true ) } diff --git a/project/build.properties b/project/build.properties index 43b8278c6..7d789d45d 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.11 +sbt.version=0.13.12 \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt index 4fda36aca..482aea5a1 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,2 +1,9 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.4") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.8") + +// Gatling plugin +// http://gatling.io/docs/2.2.2/extensions/sbt_plugin.html +addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.0") + +// sbt-paradox, used for documentation +addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.1") diff --git a/test/ApplicationSpec.scala b/test/ApplicationSpec.scala deleted file mode 100644 index 4492801d5..000000000 --- a/test/ApplicationSpec.scala +++ /dev/null @@ -1,23 +0,0 @@ -import org.scalatestplus.play._ -import play.api.test._ -import play.api.test.Helpers._ - -class ApplicationSpec extends PlaySpec with OneAppPerTest { - - "Routes" should { - "send 404 on a bad request" in { - route(app, FakeRequest(GET, "/boum")).map(status(_)) mustBe Some(NOT_FOUND) - } - } - - "HomeController" should { - "render the index page" in { - val home = route(app, FakeRequest(GET, "/")).get - - status(home) mustBe OK - contentType(home) mustBe Some("text/html") - contentAsString(home) must include ("Your new application is ready.") - } - } - -} From d21550e18c86612c658dcc4b4dd4f21f4bbb5150 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Thu, 15 Sep 2016 11:32:22 -0700 Subject: [PATCH 03/73] Update with link to page. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c4e868fd..b3da8602d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Play REST API -This is the project that goes along with the article. +This is the example project for [Making a REST API in Play](http://developer.lightbend.com/guides/play-rest-api/index.html). ## Appendix From 79f7c0415e00e7263bb8dba4630b7b19decebabd Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Thu, 15 Sep 2016 11:40:07 -0700 Subject: [PATCH 04/73] Better description of microservices problem --- docs/src/main/paradox/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index 1b6ea5b49..5bbf2f289 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -38,7 +38,7 @@ To get more involved and if you have questions, join the [mailing list](https:// ## Microservices vs REST APIs -One thing to note here is that although this guide covers how to make a REST API in Play, it only covers Play itself and deploying Play. Building a REST API in Play does not automatically make it a "microservice" because it does not cover larger scale concerns about microservices such as ensuring resiliency, persistence, distributing work over multiple machines, or monitoring. +One thing to note here is that although this guide covers how to make a REST API in Play, it only covers Play itself and deploying Play. Building a REST API in Play does not automatically make it a "microservice" because it does not cover larger scale concerns about microservices such as ensuring resiliency, consistency, or monitoring. For full scale microservices, you want [Lagom](http://www.lagomframework.com/), which builds on top of Play -- a microservices framework for dealing with the ["data on the outside"](https://blog.acolyer.org/2016/09/13/data-on-the-outside-versus-data-on-the-inside/) problem, set up with persistence and service APIs that ensure that the service always stays up and responsive even in the face of chaos monkeys and network partitions. From 92deca8cc041fb17de24b237fd3013b3dca77ae8 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Thu, 15 Sep 2016 11:46:47 -0700 Subject: [PATCH 05/73] Editing "why is fast" --- docs/src/main/paradox/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index 5bbf2f289..f4025a5bc 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -16,11 +16,11 @@ Note that there’s more involved in a REST API -- monitoring, representation, a Play makes a good REST API implementation because Play does the right thing out of the box. Play makes simple things easy, makes hard things possible, and encourages code that scales because it works in sympathy with the JVM and the underlying hardware. But "safe and does the right thing" is the boring answer. -The fun answer is that Play is **fast**. +The fun answer is that [Play is **fast**](https://www.lightbend.com/blog/why-is-play-framework-so-fast). In fact, Play is so fast that you have to turn off machines so that the rest of your architecture can keep up. The Hootsuite team was able to **reduce the number of servers by 80%** by [switching to Play](https://www.lightbend.com/resources/case-studies-and-stories/how-hootsuite-modernized-its-url-shortener). if you deploy Play with the same infrastructure that you were using for other web frameworks, you are effectively staging a denial of service attack against your own database. -Play is fast because Play is **built on reactive bedrock**. Play starts from a reactive core, and builds on reactive principles all the way from the ground. Play uses a small thread pool, and breaks network packets into a stream of small chunks of data keeps those threads fed with HTTP requests, which means it's fast. and feeds those through Akka Streams, the Reactive Streams implementation designed by the people who invented [Reactive Streams](http://www.reactive-streams.org/) and wrote the [Reactive Manifesto](http://www.reactivemanifesto.org/). +Play is fast because Play is **built on reactive bedrock**. Play starts from a reactive core, and builds on reactive principles all the way from the ground. Play breaks network packets into a stream of small chunks of bytes. It keeps a small pool of work stealing threads, mapped to the number of cores in the machine, and keeps those threads fed with those chunks. Play exposes those byte chunks to the application for body parsing, Server Sent Events and WebSockets through Akka Streams -- the Reactive Streams implementation designed by the people who invented [Reactive Streams](http://www.reactive-streams.org/) and wrote the [Reactive Manifesto](http://www.reactivemanifesto.org/). Linkedin uses Play throughout its infrastructure. It wins on all [four quadrants of scalability](http://www.slideshare.net/brikis98/the-play-framework-at-linkedin/128-Outline1_Getting_started_with_Play2) ([video](https://youtu.be/8z3h4Uv9YbE)). Play's average "request per second" comes in around [tens of k on a basic quad core w/o any intentional tuning](https://twitter.com/kevinbowling1/status/764188720140398592) -- and it only gets better. From 4534851663950f655b55d6207bed5ef7595689a5 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Thu, 15 Sep 2016 11:57:14 -0700 Subject: [PATCH 06/73] Add links to CompletionStage. --- docs/src/main/paradox/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index f4025a5bc..7dffdbdf2 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -20,13 +20,13 @@ The fun answer is that [Play is **fast**](https://www.lightbend.com/blog/why-is- In fact, Play is so fast that you have to turn off machines so that the rest of your architecture can keep up. The Hootsuite team was able to **reduce the number of servers by 80%** by [switching to Play](https://www.lightbend.com/resources/case-studies-and-stories/how-hootsuite-modernized-its-url-shortener). if you deploy Play with the same infrastructure that you were using for other web frameworks, you are effectively staging a denial of service attack against your own database. -Play is fast because Play is **built on reactive bedrock**. Play starts from a reactive core, and builds on reactive principles all the way from the ground. Play breaks network packets into a stream of small chunks of bytes. It keeps a small pool of work stealing threads, mapped to the number of cores in the machine, and keeps those threads fed with those chunks. Play exposes those byte chunks to the application for body parsing, Server Sent Events and WebSockets through Akka Streams -- the Reactive Streams implementation designed by the people who invented [Reactive Streams](http://www.reactive-streams.org/) and wrote the [Reactive Manifesto](http://www.reactivemanifesto.org/). +Play is fast because Play is **built on reactive bedrock**. Play starts from a reactive core, and builds on reactive principles all the way from the ground. Play breaks network packets into a stream of small chunks of bytes. It keeps a small pool of work stealing threads, mapped to the number of cores in the machine, and keeps those threads fed with those chunks. Play exposes those byte chunks to the application for body parsing, Server Sent Events and WebSockets through [Akka Streams](http://doc.akka.io/docs/akka/2.4/scala/stream/stream-introduction.html) -- the Reactive Streams implementation designed by the people who invented [Reactive Streams](http://www.reactive-streams.org/) and wrote the [Reactive Manifesto](http://www.reactivemanifesto.org/). Linkedin uses Play throughout its infrastructure. It wins on all [four quadrants of scalability](http://www.slideshare.net/brikis98/the-play-framework-at-linkedin/128-Outline1_Getting_started_with_Play2) ([video](https://youtu.be/8z3h4Uv9YbE)). Play's average "request per second" comes in around [tens of k on a basic quad core w/o any intentional tuning](https://twitter.com/kevinbowling1/status/764188720140398592) -- and it only gets better. Play provides an easy to use MVC paradigm, including hot-reloading without any JVM bytecode magic or container overhead. Startup time for a developer on Play was **reduced by roughly 7 times** for [Walmart Canada](https://www.lightbend.com/resources/case-studies-and-stories/walmart-boosts-conversions-by-20-with-lightbend-reactive-platform), and using Play **reduced development times by 2x to 3x**. -Play combines this with a **reactive programming API** that lets you write async, non-blocking code in a straightforward fashion without worrying about complex and confusing "callback hell." In either Java or Scala, Play works on the same principle: leverage the asynchronous computation API that the language provides to you. In Play, you work with `java.util.concurrent.CompletionStage` or `scala.concurrent.Future` API directly, and Play passes that asynchronous computation back through the framework. +Play combines this with a **reactive programming API** that lets you write async, non-blocking code in a straightforward fashion without worrying about complex and confusing "callback hell." In both Java or Scala, Play works on the same principle: leverage the asynchronous computation API that the language provides to you. In Play, you work with [`java.util.concurrent.CompletionStage`](https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/changes8.html) or [`scala.concurrent.Future`](http://docs.scala-lang.org/overviews/core/futures.html) API directly, and Play passes that asynchronous computation back through the framework. Finally, Play is modular and extensible. Play works with multiple runtime and compile time dependency injection frameworks like [Guice](https://www.playframework.com/documentation/2.5.x/ScalaDependencyInjection), [Macwire](https://di-in-scala.github.io/), [Dagger](https://github.com/esfand-r/play-java-dagger-dependency-injection#master), and leverages DI principles to integrate authentication and authorization frameworks built on top of Play. From 3b4c223a59e53e45044dabfb94f35793a34fd2e9 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Thu, 15 Sep 2016 12:01:11 -0700 Subject: [PATCH 07/73] String Interpolation Routing DSL --- docs/src/main/paradox/part-1/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/main/paradox/part-1/index.md b/docs/src/main/paradox/part-1/index.md index aa0d9cfc9..216b91ee1 100644 --- a/docs/src/main/paradox/part-1/index.md +++ b/docs/src/main/paradox/part-1/index.md @@ -31,13 +31,13 @@ GET / controllers.HomeController.index() This is useful for situations where a front end service is rendering HTML. However, Play also contains a more powerful routing DSL that we will use for the REST API. -For every HTTP request starting with `/v1/posts`, Play routes it to a dedicated PostRouter class to handle the Posts resource, through the [`conf/routes`](https://github.com/playframework/play-rest-api/blob/master/conf/routes) file: +For every HTTP request starting with `/v1/posts`, Play routes it to a dedicated `PostRouter` class to handle the Posts resource, through the [`conf/routes`](https://github.com/playframework/play-rest-api/blob/master/conf/routes) file: ``` -> /v1/posts v1.post.PostRouter ``` -The PostRouter examines the URL and extracts data to pass along to the controller [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostRouter.scala): +The `PostRouter` examines the URL and extracts data to pass along to the controller [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostRouter.scala): ```scala package v1.post @@ -64,7 +64,7 @@ class PostRouter @Inject()(controller: PostController) } ``` -Play’s [routing DSL](https://www.playframework.com/documentation/2.5.x/ScalaSirdRouter) (aka SIRD) shows how data can be extracted from the URL concisely and cleanly. SIRD is based around HTTP methods and a string interpolated extractor object – this means that when we type the string “/$id” and prefix it with “p”, then the path parameter id can be extracted and used in the block. Naturally, there are also operators to extract queries, regular expressions, and even add custom extractors. If you have a URL as follows: +Play’s [routing DSL](https://www.playframework.com/documentation/2.5.x/ScalaSirdRouter) (technically "String Interpolation Routing DSL", aka SIRD) shows how data can be extracted from the URL concisely and cleanly. SIRD is based around HTTP methods and a string interpolated extractor object – this means that when we type the string “/$id” and prefix it with “p”, then the path parameter id can be extracted and used in the block. Naturally, there are also operators to extract queries, regular expressions, and even add custom extractors. If you have a URL as follows: ``` /posts/?sort=ascending&count=5 From 5ddbe59e4aa095c1337584df3defc3f0a8f62076 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Fri, 16 Sep 2016 11:50:29 -0700 Subject: [PATCH 08/73] Remove unneeded lines --- app/ErrorHandler.scala | 8 +++----- app/Module.scala | 2 -- app/RequestHandler.scala | 1 - app/v1/post/package.scala | 1 - 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/app/ErrorHandler.scala b/app/ErrorHandler.scala index 1b6f2ea10..b16cb92d5 100644 --- a/app/ErrorHandler.scala +++ b/app/ErrorHandler.scala @@ -1,8 +1,8 @@ -import javax.inject.{Inject, Provider, Singleton} +import javax.inject.{Inject, Provider} import play.api._ +import play.api.http.DefaultHttpErrorHandler import play.api.http.Status._ -import play.api.http.{ContentTypes, DefaultHttpErrorHandler, HttpErrorHandlerExceptions} import play.api.libs.json.Json import play.api.mvc.Results._ import play.api.mvc._ @@ -10,7 +10,6 @@ import play.api.routing.Router import play.core.SourceMapper import scala.concurrent._ -import scala.util.control.NonFatal /** * Provides a stripped down error handler that does not use HTML in error pages, and @@ -18,12 +17,11 @@ import scala.util.control.NonFatal * * https://www.playframework.com/documentation/2.5.x/ScalaErrorHandling */ -@Singleton class ErrorHandler(environment: Environment, configuration: Configuration, sourceMapper: Option[SourceMapper] = None, optionRouter: => Option[Router] = None) - extends DefaultHttpErrorHandler(environment, configuration, sourceMapper, optionRouter) with RequestExtractors with Rendering { + extends DefaultHttpErrorHandler(environment, configuration, sourceMapper, optionRouter) { private val logger = org.slf4j.LoggerFactory.getLogger("application.ErrorHandler") diff --git a/app/Module.scala b/app/Module.scala index caa9f2890..e6149073c 100644 --- a/app/Module.scala +++ b/app/Module.scala @@ -5,8 +5,6 @@ import net.codingwell.scalaguice.ScalaModule import play.api.{Configuration, Environment} import v1.post._ -import scala.concurrent.Future - /** * Sets up custom components for Play. * diff --git a/app/RequestHandler.scala b/app/RequestHandler.scala index b2987672b..1eaaa37ed 100644 --- a/app/RequestHandler.scala +++ b/app/RequestHandler.scala @@ -10,7 +10,6 @@ import play.api.routing.Router * https://www.playframework.com/documentation/2.5.x/ScalaHttpRequestHandlers#extending-the-default-request-handler */ class RequestHandler @Inject()(router: Router, - postRouter: v1.post.PostRouter, errorHandler: HttpErrorHandler, configuration: HttpConfiguration, filters: HttpFilters) diff --git a/app/v1/post/package.scala b/app/v1/post/package.scala index d37eac281..982682c93 100644 --- a/app/v1/post/package.scala +++ b/app/v1/post/package.scala @@ -1,7 +1,6 @@ package v1 import play.api.i18n.Messages -import play.api.mvc.Result /** * Package object for post. This is a good place to put implicit conversions. From da00d3b582d2b881dab6d33fd97504d770932711 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Fri, 16 Sep 2016 15:39:41 -0700 Subject: [PATCH 09/73] Update resource introduction --- docs/src/main/paradox/part-1/index.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/src/main/paradox/part-1/index.md b/docs/src/main/paradox/part-1/index.md index aa0d9cfc9..592c60e5e 100644 --- a/docs/src/main/paradox/part-1/index.md +++ b/docs/src/main/paradox/part-1/index.md @@ -1,14 +1,24 @@ # Basics -This guide will walk you through how to make a RESTful API with JSON using [Play 2.5](https://playframework.com). +This guide will walk you through how to make a REST API with JSON using [Play 2.5](https://playframework.com). -To see the associated Github project, please go to [http://github.com/playframework/play-rest-api](http://github.com/playframework/play-rest-api). We're going to be showing an already working Play project with most of the code available under the "app/v1" directory. There will be several different versions of the same project as this series expands, so you can compare different versions of the controller against each other. +To see the associated Github project, please go to [https://github.com/playframework/play-rest-api](https://github.com/playframework/play-rest-api) or clone the project: + +``` +git clone https://github.com/playframework/play-rest-api.git +``` + +We're going to be showing an already working Play project with most of the code available under the "app/v1" directory. There will be several different versions of the same project as this series expands, so you can compare different versions of the project against each other. To run Play on your own local computer, please see the instructions in the [appendix](../appendix.md). +## Introduction + +We'll start off with a REST API that displays information for blog posts. Users should be able to write a title and a body of a blog post and create new blog posts, edit existing blog posts, and delete new blog posts. + ## Modelling a Post Resource -We'll start off with a REST API that displays information for blog posts. This is a resource that will contain all the data to start with -- it will have a unique id, a URL hyperlink that indicates the canonical location of the resource, the title of the blog post, and the body of the blog post. +The way to do this in REST is to model the represented state as a resource. A blog post resource will have a unique id, a URL hyperlink that indicates the canonical location of the resource, the title of the blog post, and the body of the blog post. This resource is represented as a single case class in the Play application [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostResourceHandler.scala#L13): From 912bd30155e1169d2066805b0125519e710a8277 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Tue, 20 Sep 2016 16:24:27 -0700 Subject: [PATCH 10/73] Add scalafmt --- app/ErrorHandler.scala | 44 ++++++++++++++++++--------- app/Module.scala | 13 ++++---- app/RequestHandler.scala | 15 +++++---- app/controllers/HomeController.scala | 4 +-- app/v1/post/PostAction.scala | 37 +++++++++++----------- app/v1/post/PostController.scala | 20 ++++++------ app/v1/post/PostRepository.scala | 12 +++----- app/v1/post/PostResourceHandler.scala | 25 ++++++--------- app/v1/post/PostRouter.scala | 7 ++--- app/v1/post/package.scala | 8 ++--- build.sbt | 12 +++----- docs/build.sbt | 2 +- gatling/simulation/GatlingSpec.scala | 3 +- project/Common.scala | 11 ++++--- project/plugins.sbt | 10 ++++-- 15 files changed, 119 insertions(+), 104 deletions(-) diff --git a/app/ErrorHandler.scala b/app/ErrorHandler.scala index b16cb92d5..c7dc428ea 100644 --- a/app/ErrorHandler.scala +++ b/app/ErrorHandler.scala @@ -12,18 +12,22 @@ import play.core.SourceMapper import scala.concurrent._ /** - * Provides a stripped down error handler that does not use HTML in error pages, and - * prints out debugging output. - * - * https://www.playframework.com/documentation/2.5.x/ScalaErrorHandling - */ + * Provides a stripped down error handler that does not use HTML in error pages, and + * prints out debugging output. + * + * https://www.playframework.com/documentation/2.5.x/ScalaErrorHandling + */ class ErrorHandler(environment: Environment, configuration: Configuration, sourceMapper: Option[SourceMapper] = None, optionRouter: => Option[Router] = None) - extends DefaultHttpErrorHandler(environment, configuration, sourceMapper, optionRouter) { + extends DefaultHttpErrorHandler(environment, + configuration, + sourceMapper, + optionRouter) { - private val logger = org.slf4j.LoggerFactory.getLogger("application.ErrorHandler") + private val logger = + org.slf4j.LoggerFactory.getLogger("application.ErrorHandler") // This maps through Guice so that the above constructor can call methods. @Inject @@ -31,11 +35,17 @@ class ErrorHandler(environment: Environment, configuration: Configuration, sourceMapper: OptionalSourceMapper, router: Provider[Router]) = { - this(environment, configuration, sourceMapper.sourceMapper, Some(router.get)) + this(environment, + configuration, + sourceMapper.sourceMapper, + Some(router.get)) } - override def onClientError(request: RequestHeader, statusCode: Int, message: String): Future[Result] = { - logger.debug(s"onClientError: statusCode = $statusCode, uri = ${request.uri}, message = $message") + override def onClientError(request: RequestHeader, + statusCode: Int, + message: String): Future[Result] = { + logger.debug( + s"onClientError: statusCode = $statusCode, uri = ${request.uri}, message = $message") Future.successful { val result = statusCode match { @@ -48,18 +58,24 @@ class ErrorHandler(environment: Environment, case clientError if statusCode >= 400 && statusCode < 500 => Results.Status(statusCode) case nonClientError => - val msg = s"onClientError invoked with non client error status code $statusCode: $message" + val msg = + s"onClientError invoked with non client error status code $statusCode: $message" throw new IllegalArgumentException(msg) } result } } - override protected def onDevServerError(request: RequestHeader, exception: UsefulException): Future[Result] = { - Future.successful(InternalServerError(Json.obj("exception" -> exception.toString))) + override protected def onDevServerError( + request: RequestHeader, + exception: UsefulException): Future[Result] = { + Future.successful( + InternalServerError(Json.obj("exception" -> exception.toString))) } - override protected def onProdServerError(request: RequestHeader, exception: UsefulException): Future[Result] = { + override protected def onProdServerError( + request: RequestHeader, + exception: UsefulException): Future[Result] = { Future.successful(InternalServerError) } } diff --git a/app/Module.scala b/app/Module.scala index e6149073c..7da2ce9f7 100644 --- a/app/Module.scala +++ b/app/Module.scala @@ -6,12 +6,13 @@ import play.api.{Configuration, Environment} import v1.post._ /** - * Sets up custom components for Play. - * - * https://www.playframework.com/documentation/2.5.x/ScalaDependencyInjection - */ -class Module(environment: Environment, - configuration: Configuration) extends AbstractModule with ScalaModule { + * Sets up custom components for Play. + * + * https://www.playframework.com/documentation/2.5.x/ScalaDependencyInjection + */ +class Module(environment: Environment, configuration: Configuration) + extends AbstractModule + with ScalaModule { override def configure() = { bind[PostRepository].to[PostRepositoryImpl].in[Singleton] diff --git a/app/RequestHandler.scala b/app/RequestHandler.scala index 1eaaa37ed..8bcbab09c 100644 --- a/app/RequestHandler.scala +++ b/app/RequestHandler.scala @@ -5,15 +5,18 @@ import play.api.mvc._ import play.api.routing.Router /** - * Handles all requests. - * - * https://www.playframework.com/documentation/2.5.x/ScalaHttpRequestHandlers#extending-the-default-request-handler - */ + * Handles all requests. + * + * https://www.playframework.com/documentation/2.5.x/ScalaHttpRequestHandlers#extending-the-default-request-handler + */ class RequestHandler @Inject()(router: Router, errorHandler: HttpErrorHandler, configuration: HttpConfiguration, filters: HttpFilters) - extends DefaultHttpRequestHandler(router, errorHandler, configuration, filters) { + extends DefaultHttpRequestHandler(router, + errorHandler, + configuration, + filters) { override def handlerForRequest(request: RequestHeader): (RequestHeader, Handler) = { super.handlerForRequest { @@ -34,7 +37,7 @@ class RequestHandler @Inject()(router: Router, } private def addTrailingSlash(origReq: RequestHeader): RequestHeader = { - if (! origReq.path.endsWith("/")) { + if (!origReq.path.endsWith("/")) { val path = origReq.path + "/" if (origReq.rawQueryString.isEmpty) { origReq.copy(path = path, uri = path) diff --git a/app/controllers/HomeController.scala b/app/controllers/HomeController.scala index d269560ca..35b5972e9 100644 --- a/app/controllers/HomeController.scala +++ b/app/controllers/HomeController.scala @@ -3,8 +3,8 @@ package controllers import play.api.mvc.{Action, Controller} /** - * A very small controller that renders a home page. - */ + * A very small controller that renders a home page. + */ class HomeController extends Controller { def index = Action { implicit request => diff --git a/app/v1/post/PostAction.scala b/app/v1/post/PostAction.scala index 29541f387..6f0fdec93 100644 --- a/app/v1/post/PostAction.scala +++ b/app/v1/post/PostAction.scala @@ -9,31 +9,32 @@ import play.api.mvc._ import scala.concurrent.{ExecutionContext, Future} /** - * A wrapped request for post resources. - * - * This is commonly used to hold request-specific information like - * security credentials, and useful shortcut methods. - */ -class PostRequest[A](request: Request[A], - val messages: Messages) - extends WrappedRequest(request) + * A wrapped request for post resources. + * + * This is commonly used to hold request-specific information like + * security credentials, and useful shortcut methods. + */ +class PostRequest[A](request: Request[A], val messages: Messages) + extends WrappedRequest(request) /** - * The default action for the Post resource. - * - * This is the place to put logging, metrics, to augment - * the request with contextual data, and manipulate the - * result. - */ -class PostAction @Inject()(messagesApi: MessagesApi) - (implicit ec: ExecutionContext) - extends ActionBuilder[PostRequest] with HttpVerbs { + * The default action for the Post resource. + * + * This is the place to put logging, metrics, to augment + * the request with contextual data, and manipulate the + * result. + */ +class PostAction @Inject()(messagesApi: MessagesApi)( + implicit ec: ExecutionContext) + extends ActionBuilder[PostRequest] + with HttpVerbs { type PostRequestBlock[A] = PostRequest[A] => Future[Result] private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) - override def invokeBlock[A](request: Request[A], block: PostRequestBlock[A]): Future[Result] = { + override def invokeBlock[A](request: Request[A], + block: PostRequestBlock[A]): Future[Result] = { if (logger.isTraceEnabled()) { logger.trace(s"invokeBlock: request = $request") } diff --git a/app/v1/post/PostController.scala b/app/v1/post/PostController.scala index 9eeef7dbd..19e70240c 100644 --- a/app/v1/post/PostController.scala +++ b/app/v1/post/PostController.scala @@ -11,12 +11,12 @@ import scala.concurrent.{ExecutionContext, Future} case class PostFormInput(title: String, body: String) /** - * Takes HTTP requests and produces JSON. - */ -class PostController @Inject()(action: PostAction, - handler: PostResourceHandler) - (implicit ec: ExecutionContext) - extends Controller { + * Takes HTTP requests and produces JSON. + */ +class PostController @Inject()( + action: PostAction, + handler: PostResourceHandler)(implicit ec: ExecutionContext) + extends Controller { private val form: Form[PostFormInput] = { import play.api.data.Forms._ @@ -51,20 +51,18 @@ class PostController @Inject()(action: PostAction, } } - private def processJsonPost[A]()(implicit request: PostRequest[A]): Future[Result] = { + private def processJsonPost[A]()( + implicit request: PostRequest[A]): Future[Result] = { def failure(badForm: Form[PostFormInput]) = { Future.successful(BadRequest(badForm.errorsAsJson)) } def success(input: PostFormInput) = { handler.create(input).map { post => - Created(Json.toJson(post)) - .withHeaders(LOCATION -> post.link) + Created(Json.toJson(post)).withHeaders(LOCATION -> post.link) } } form.bindFromRequest().fold(failure, success) } } - - diff --git a/app/v1/post/PostRepository.scala b/app/v1/post/PostRepository.scala index 33167b6c4..8005dfe58 100644 --- a/app/v1/post/PostRepository.scala +++ b/app/v1/post/PostRepository.scala @@ -6,7 +6,7 @@ import scala.concurrent.Future final case class PostData(id: PostId, title: String, body: String) -class PostId private(val underlying: Int) extends AnyVal { +class PostId private (val underlying: Int) extends AnyVal { override def toString: String = underlying.toString } @@ -18,8 +18,8 @@ object PostId { } /** - * A pure non-blocking interface for the PostRepository. - */ + * A pure non-blocking interface for the PostRepository. + */ trait PostRepository { def create(data: PostData): Future[PostId] @@ -28,10 +28,9 @@ trait PostRepository { def get(id: PostId): Future[Option[PostData]] } - /** - * A trivial implementation for the Post Repository. - */ + * A trivial implementation for the Post Repository. + */ @Singleton class PostRepositoryImpl @Inject() extends PostRepository { @@ -67,4 +66,3 @@ class PostRepositoryImpl @Inject() extends PostRepository { } } - diff --git a/app/v1/post/PostResourceHandler.scala b/app/v1/post/PostResourceHandler.scala index 2c042c571..9f1224ca6 100644 --- a/app/v1/post/PostResourceHandler.scala +++ b/app/v1/post/PostResourceHandler.scala @@ -6,20 +6,16 @@ import scala.concurrent.{ExecutionContext, Future} import play.api.libs.json._ - /** - * DTO for displaying post information. - */ -case class PostResource(id: String, - link: String, - title: String, - body: String) + * DTO for displaying post information. + */ +case class PostResource(id: String, link: String, title: String, body: String) object PostResource { /** - * Mapping to write a PostResource out as a JSON value. - */ + * Mapping to write a PostResource out as a JSON value. + */ implicit val implicitWrites = new Writes[PostResource] { def writes(post: PostResource): JsValue = { Json.obj( @@ -33,12 +29,11 @@ object PostResource { } /** - * Controls access to the backend data, returning [[PostResource]] - */ -class PostResourceHandler @Inject()(routerProvider: Provider[PostRouter], - postRepository: PostRepository) - (implicit ec: ExecutionContext) -{ + * Controls access to the backend data, returning [[PostResource]] + */ +class PostResourceHandler @Inject()( + routerProvider: Provider[PostRouter], + postRepository: PostRepository)(implicit ec: ExecutionContext) { def create(postInput: PostFormInput): Future[PostResource] = { val data = PostData(PostId("999"), postInput.title, postInput.body) diff --git a/app/v1/post/PostRouter.scala b/app/v1/post/PostRouter.scala index f9172c93a..809f34ea2 100644 --- a/app/v1/post/PostRouter.scala +++ b/app/v1/post/PostRouter.scala @@ -7,10 +7,9 @@ import play.api.routing.SimpleRouter import play.api.routing.sird._ /** - * Routes and URLs to the PostResource controller. - */ -class PostRouter @Inject()(controller: PostController) - extends SimpleRouter { + * Routes and URLs to the PostResource controller. + */ +class PostRouter @Inject()(controller: PostController) extends SimpleRouter { val prefix = "/v1/posts" def link(id: PostId): String = { diff --git a/app/v1/post/package.scala b/app/v1/post/package.scala index 982682c93..7903bbe43 100644 --- a/app/v1/post/package.scala +++ b/app/v1/post/package.scala @@ -3,13 +3,13 @@ package v1 import play.api.i18n.Messages /** - * Package object for post. This is a good place to put implicit conversions. - */ + * Package object for post. This is a good place to put implicit conversions. + */ package object post { /** - * Converts between PostRequest and Messages automatically. - */ + * Converts between PostRequest and Messages automatically. + */ implicit def requestToMessages[A](implicit r: PostRequest[A]): Messages = { r.messages } diff --git a/build.sbt b/build.sbt index 2d074ec1c..0b6c9161d 100644 --- a/build.sbt +++ b/build.sbt @@ -3,7 +3,8 @@ import sbt.Keys._ lazy val GatlingTest = config("gatling") extend Test // The Play project itself -lazy val root = (project in file(".")).enablePlugins(Common, PlayScala, GatlingPlugin) +lazy val root = (project in file(".")) + .enablePlugins(Common, PlayScala, GatlingPlugin) .configs(GatlingTest) .settings(inConfig(GatlingTest)(Defaults.testSettings): _*) .settings( @@ -11,21 +12,18 @@ lazy val root = (project in file(".")).enablePlugins(Common, PlayScala, GatlingP libraryDependencies ++= Seq( // A useful URL construction library "com.netaporter" %% "scala-uri" % "0.4.14", - // Use scala-guice "net.codingwell" %% "scala-guice" % "4.1.0", - // Add scalatest in for test framework "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test, - // Add Gatling in for laod testing "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % Test, - "io.gatling" % "gatling-test-framework" % "2.2.2" % Test + "io.gatling" % "gatling-test-framework" % "2.2.2" % Test ), scalaSource in GatlingTest := baseDirectory.value / "/gatling/simulation" ) -// Documentation for this project: +// Documentation for this project: // sbt "project docs" "~ paradox" // open docs/target/paradox/site/index.html -lazy val docs = (project in file("docs")).enablePlugins(ParadoxPlugin) \ No newline at end of file +lazy val docs = (project in file("docs")).enablePlugins(ParadoxPlugin) diff --git a/docs/build.sbt b/docs/build.sbt index 7caa997ac..fc34703fa 100644 --- a/docs/build.sbt +++ b/docs/build.sbt @@ -5,4 +5,4 @@ //paradoxTheme := Some("com.lightbend.paradox" % "paradox-theme-lightbend" % "0.2.1-TH2") // Uses the out of the box generic theme. -paradoxTheme := Some(builtinParadoxTheme("generic")) \ No newline at end of file +paradoxTheme := Some(builtinParadoxTheme("generic")) diff --git a/gatling/simulation/GatlingSpec.scala b/gatling/simulation/GatlingSpec.scala index 599d0a01c..c55f948e2 100644 --- a/gatling/simulation/GatlingSpec.scala +++ b/gatling/simulation/GatlingSpec.scala @@ -25,7 +25,8 @@ class GatlingSpec extends Simulation { object Index { - def refreshAfterOneSecond = exec(http("Index").get("/").check(status.is(200))).pause(1) + def refreshAfterOneSecond = + exec(http("Index").get("/").check(status.is(200))).pause(1) val refreshManyTimes = repeat(10000) { refreshAfterOneSecond diff --git a/project/Common.scala b/project/Common.scala index eb63fabe7..598dc3d9f 100644 --- a/project/Common.scala +++ b/project/Common.scala @@ -3,8 +3,8 @@ import sbt._ import sbt.plugins.JvmPlugin /** - * Settings that are comment to all the SBT projects - */ + * Settings that are comment to all the SBT projects + */ object Common extends AutoPlugin { override def trigger = allRequirements override def requires: sbt.Plugins = JvmPlugin @@ -15,15 +15,16 @@ object Common extends AutoPlugin { resolvers += Resolver.typesafeRepo("releases"), javacOptions ++= Seq("-source", "1.8", "-target", "1.8"), scalacOptions ++= Seq( - "-encoding", "UTF-8", // yes, this is 2 args + "-encoding", + "UTF-8", // yes, this is 2 args "-target:jvm-1.8", "-deprecation", "-feature", "-unchecked", "-Xlint", "-Yno-adapted-args", - "-Ywarn-numeric-widen" - //"-Xfatal-warnings" + "-Ywarn-numeric-widen", + "-Xfatal-warnings" ), scalaVersion := "2.11.8", scalacOptions in Test ++= Seq("-Yrangepos"), diff --git a/project/plugins.sbt b/project/plugins.sbt index 482aea5a1..6d8f800d3 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,9 +1,13 @@ // The Play plugin addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.8") -// Gatling plugin +// sbt-paradox, used for documentation +addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.1") + +// Load testing tool: // http://gatling.io/docs/2.2.2/extensions/sbt_plugin.html addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.0") -// sbt-paradox, used for documentation -addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.1") +// Scala formatting: "sbt scalafmt" +// https://olafurpg.github.io/scalafmt +addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.3.1") \ No newline at end of file From 7832be55abfef675408a28e00f8ef948d09577f6 Mon Sep 17 00:00:00 2001 From: Jim Powers Date: Fri, 7 Oct 2016 09:53:32 -0500 Subject: [PATCH 11/73] Add link to example code service --- docs/src/main/paradox/index.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index 7dffdbdf2..921e86d02 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -2,11 +2,14 @@ This is a multi-part guide to walk you through how to make a RESTful API with JSON using [Play 2.5](https://playframework.com). -We’ll demonstrate with a “best practices” REST API that you can clone from [Github](http://github.com/playframework/play-rest-api): +We’ll demonstrate with a “best practices” REST API. You can get source code for this guide two ways: -``` -git clone https://github.com/playframework/play-rest-api.git -``` +* [Pre-packaged bundle](https://example.lightbend.com/v1/download/play-rest-api) +* [From Github](http://github.com/playframework/play-rest-api): + + ``` + git clone https://github.com/playframework/play-rest-api.git + ``` This example is in Scala, but Play also has a [Java API](https://www.playframework.com/documentation/2.5.x/JavaHome) which looks and acts just like the [Scala API](https://www.playframework.com/documentation/2.5.x/ScalaHome). For instructions on running and using the project, please see the [appendix](appendix.md). This project also comes with an integrated [Gatling](http://gatling.io/) load test -- again, instructions are in the [appendix](appendix.md). From de6da771c4645c294444eefec49324770256cbc5 Mon Sep 17 00:00:00 2001 From: Jim Powers Date: Fri, 7 Oct 2016 10:16:35 -0500 Subject: [PATCH 12/73] Make the zip startup a bit more explicit --- docs/src/main/paradox/index.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index 921e86d02..603496118 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -4,7 +4,12 @@ This is a multi-part guide to walk you through how to make a RESTful API with JS We’ll demonstrate with a “best practices” REST API. You can get source code for this guide two ways: -* [Pre-packaged bundle](https://example.lightbend.com/v1/download/play-rest-api) +* Download a pre-packaged bundle with this link [https://example.lightbend.com/v1/download/play-rest-api](https://example.lightbend.com/v1/download/play-rest-api) + ```bash + unzip play-rest-api.zip +cd play-rest-api +./sbt # Or sbt.bat for Windows + ``` * [From Github](http://github.com/playframework/play-rest-api): ``` From 0eaafbc123a80b1fd09f3786e315868cba7cc908 Mon Sep 17 00:00:00 2001 From: Jim Powers Date: Fri, 7 Oct 2016 10:23:36 -0500 Subject: [PATCH 13/73] Better instructions --- docs/src/main/paradox/index.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index 603496118..508d6a40a 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -5,11 +5,21 @@ This is a multi-part guide to walk you through how to make a RESTful API with JS We’ll demonstrate with a “best practices” REST API. You can get source code for this guide two ways: * Download a pre-packaged bundle with this link [https://example.lightbend.com/v1/download/play-rest-api](https://example.lightbend.com/v1/download/play-rest-api) + + * Linux/Mac: ```bash unzip play-rest-api.zip cd play-rest-api -./sbt # Or sbt.bat for Windows +./sbt ``` + * Windows: + + 1. Unzip the download + 2. From a command line `cd` into the directory where you expanded the downloaded `zip` file and run: + ``` + sbt.bat + ``` + * [From Github](http://github.com/playframework/play-rest-api): ``` From e9c263486e03cf2d6ac9a8fb89a78c73a141bbc1 Mon Sep 17 00:00:00 2001 From: Jim Powers Date: Fri, 7 Oct 2016 11:09:16 -0500 Subject: [PATCH 14/73] Fix whitepsace --- docs/src/main/paradox/index.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index 508d6a40a..d41b05147 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -7,24 +7,23 @@ We’ll demonstrate with a “best practices” REST API. You can get source co * Download a pre-packaged bundle with this link [https://example.lightbend.com/v1/download/play-rest-api](https://example.lightbend.com/v1/download/play-rest-api) * Linux/Mac: - ```bash - unzip play-rest-api.zip +```bash +unzip play-rest-api.zip cd play-rest-api ./sbt - ``` +``` * Windows: - + 1. Unzip the download 2. From a command line `cd` into the directory where you expanded the downloaded `zip` file and run: - ``` - sbt.bat - ``` +``` +sbt.bat +``` * [From Github](http://github.com/playframework/play-rest-api): - - ``` - git clone https://github.com/playframework/play-rest-api.git - ``` +``` +git clone https://github.com/playframework/play-rest-api.git +``` This example is in Scala, but Play also has a [Java API](https://www.playframework.com/documentation/2.5.x/JavaHome) which looks and acts just like the [Scala API](https://www.playframework.com/documentation/2.5.x/ScalaHome). For instructions on running and using the project, please see the [appendix](appendix.md). This project also comes with an integrated [Gatling](http://gatling.io/) load test -- again, instructions are in the [appendix](appendix.md). From a74c5760eef40d664e87a6ac641eb2c721040cf1 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Thu, 13 Oct 2016 12:09:24 -0700 Subject: [PATCH 15/73] Refactor for temlate control --- build.sbt | 19 ++++++++----------- docs/build.sbt | 2 ++ project/Common.scala | 1 - 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/build.sbt b/build.sbt index 0b6c9161d..2c7cd0173 100644 --- a/build.sbt +++ b/build.sbt @@ -2,6 +2,14 @@ import sbt.Keys._ lazy val GatlingTest = config("gatling") extend Test +scalaVersion := "2.11.8" + +libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.14" +libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" +libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test +libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % Test +libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.2.2" % Test + // The Play project itself lazy val root = (project in file(".")) .enablePlugins(Common, PlayScala, GatlingPlugin) @@ -9,17 +17,6 @@ lazy val root = (project in file(".")) .settings(inConfig(GatlingTest)(Defaults.testSettings): _*) .settings( name := """play-rest-api""", - libraryDependencies ++= Seq( - // A useful URL construction library - "com.netaporter" %% "scala-uri" % "0.4.14", - // Use scala-guice - "net.codingwell" %% "scala-guice" % "4.1.0", - // Add scalatest in for test framework - "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test, - // Add Gatling in for laod testing - "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % Test, - "io.gatling" % "gatling-test-framework" % "2.2.2" % Test - ), scalaSource in GatlingTest := baseDirectory.value / "/gatling/simulation" ) diff --git a/docs/build.sbt b/docs/build.sbt index fc34703fa..0775a25f2 100644 --- a/docs/build.sbt +++ b/docs/build.sbt @@ -6,3 +6,5 @@ // Uses the out of the box generic theme. paradoxTheme := Some(builtinParadoxTheme("generic")) + +scalaVersion := "2.11.8" \ No newline at end of file diff --git a/project/Common.scala b/project/Common.scala index 598dc3d9f..607d0ab2b 100644 --- a/project/Common.scala +++ b/project/Common.scala @@ -26,7 +26,6 @@ object Common extends AutoPlugin { "-Ywarn-numeric-widen", "-Xfatal-warnings" ), - scalaVersion := "2.11.8", scalacOptions in Test ++= Seq("-Yrangepos"), autoAPIMappings := true ) From efc1dde3986bc73544eb40ec3e34db4b22ece7b9 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Thu, 13 Oct 2016 12:10:57 -0700 Subject: [PATCH 16/73] Updated with template-control on 2016-10-13T19:10:57.709Z File-Pattern: **/build.sbt If-Found-In-Line: "com.typesafe.play" %% "play-slick" Replace-Line-With: libraryDependencies += "com.typesafe.play" %% "play-slick" % "2.0.2" If-Found-In-Line: "com.typesafe.play" %% "play-slick-evolutions" Replace-Line-With: libraryDependencies += "com.typesafe.play" %% "play-slick-evolutions" % "2.0.2" If-Found-In-Line: scala-guice Replace-Line-With: libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" If-Found-In-Line: "com.softwaremill.macwire" %% "macros" Replace-Line-With: libraryDependencies += "com.softwaremill.macwire" %% "macros" % "2.2.2" % "provided" If-Found-In-Line: gatling-charts-highcharts Replace-Line-With: libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % Test If-Found-In-Line: scalaVersion Replace-Line-With: scalaVersion := "2.11.8" If-Found-In-Line: scalatestplus-play Replace-Line-With: libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test If-Found-In-Line: play-bootstrap Replace-Line-With: libraryDependencies += "com.adrianhurt" %% "play-bootstrap" % "1.0-P25-B3" If-Found-In-Line: gatling-test-framework Replace-Line-With: libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.2.2" % Test If-Found-In-Line: "com.typesafe.play" %% "anorm" Replace-Line-With: libraryDependencies += "com.typesafe.play" %% "anorm" % "2.5.0" If-Found-In-Line: com.h2database Replace-Line-With: libraryDependencies += "com.h2database" % "h2" % "1.4.190" If-Found-In-Line: "com.softwaremill.macwire" %% "proxy" Replace-Line-With: libraryDependencies += "com.softwaremill.macwire" %% "proxy" % "2.2.2" If-Found-In-Line: "com.softwaremill.macwire" %% "util" Replace-Line-With: libraryDependencies += "com.softwaremill.macwire" %% "util" % "2.2.2" File-Pattern: **/build.properties If-Found-In-Line: sbt.version Replace-Line-With: sbt.version=0.13.12 File-Pattern: **/application.conf File-Pattern: **/plugins.sbt If-Found-In-Line: sbt-mocha Replace-Line-With: addSbtPlugin("com.typesafe.sbt" % "sbt-mocha" % "1.1.0") If-Found-In-Line: sbt-sassify Replace-Line-With: addSbtPlugin("org.irundaia.sbt" % "sbt-sassify" % "1.4.6") If-Found-In-Line: sbt-less Replace-Line-With: addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.0") If-Found-In-Line: sbt-play-enhancer Replace-Line-With: addSbtPlugin("com.typesafe.sbt" % "sbt-play-enhancer" % "1.1.0") If-Found-In-Line: sbt-play-ebean Replace-Line-With: addSbtPlugin("com.typesafe.sbt" % "sbt-play-ebean" % "3.0.1") If-Found-In-Line: sbt-jshint Replace-Line-With: addSbtPlugin("com.typesafe.sbt" % "sbt-jshint" % "1.0.4") If-Found-In-Line: sbt-coffeescript Replace-Line-With: addSbtPlugin("com.typesafe.sbt" % "sbt-coffeescript" % "1.0.0") If-Found-In-Line: sbt-rjs Replace-Line-With: addSbtPlugin("com.typesafe.sbt" % "sbt-rjs" % "1.0.8") If-Found-In-Line: sbt-plugin Replace-Line-With: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.9") If-Found-In-Line: sbt-digest Replace-Line-With: addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.1") --- docs/build.sbt | 2 +- project/build.properties | 2 +- project/plugins.sbt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/build.sbt b/docs/build.sbt index 0775a25f2..d4ef656ba 100644 --- a/docs/build.sbt +++ b/docs/build.sbt @@ -7,4 +7,4 @@ // Uses the out of the box generic theme. paradoxTheme := Some(builtinParadoxTheme("generic")) -scalaVersion := "2.11.8" \ No newline at end of file +scalaVersion := "2.11.8" diff --git a/project/build.properties b/project/build.properties index 7d789d45d..35c88bab7 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.12 \ No newline at end of file +sbt.version=0.13.12 diff --git a/project/plugins.sbt b/project/plugins.sbt index 6d8f800d3..b67a45f0c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.8") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.9") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.1") @@ -10,4 +10,4 @@ addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.0") // Scala formatting: "sbt scalafmt" // https://olafurpg.github.io/scalafmt -addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.3.1") \ No newline at end of file +addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.3.1") From 55724f3eb04a46977144b23009531adb70f97d4f Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Fri, 14 Oct 2016 19:33:33 -0700 Subject: [PATCH 17/73] Fix appendix link --- docs/src/main/paradox/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index d41b05147..9ec4ea2e1 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -25,7 +25,7 @@ sbt.bat git clone https://github.com/playframework/play-rest-api.git ``` -This example is in Scala, but Play also has a [Java API](https://www.playframework.com/documentation/2.5.x/JavaHome) which looks and acts just like the [Scala API](https://www.playframework.com/documentation/2.5.x/ScalaHome). For instructions on running and using the project, please see the [appendix](appendix.md). This project also comes with an integrated [Gatling](http://gatling.io/) load test -- again, instructions are in the [appendix](appendix.md). +This example is in Scala, but Play also has a [Java API](https://www.playframework.com/documentation/2.5.x/JavaHome) which looks and acts just like the [Scala API](https://www.playframework.com/documentation/2.5.x/ScalaHome). For instructions on running and using the project, please see the [[appendix]]. This project also comes with an integrated [Gatling](http://gatling.io/) load test -- again, instructions are in the [[appendix]]. Note that there’s more involved in a REST API -- monitoring, representation, and managing access to back end resources -- that we'll cover in subsequent posts. But first, let's address why Play is so effective as a REST API. From 3ecba47403509166d2d4521724e3e8041d7138b4 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 19 Oct 2016 15:08:07 -0400 Subject: [PATCH 18/73] add download_url property so we can display download button --- build.sbt | 5 ++++- docs/build.sbt | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/build.sbt b/build.sbt index 2c7cd0173..1f5ce27de 100644 --- a/build.sbt +++ b/build.sbt @@ -23,4 +23,7 @@ lazy val root = (project in file(".")) // Documentation for this project: // sbt "project docs" "~ paradox" // open docs/target/paradox/site/index.html -lazy val docs = (project in file("docs")).enablePlugins(ParadoxPlugin) +lazy val docs = (project in file("docs")).enablePlugins(ParadoxPlugin). + settings( + paradoxProperties += ("download_url" -> "https://example.lightbend.com/v1/download/play-rest-api") + ) diff --git a/docs/build.sbt b/docs/build.sbt index d4ef656ba..6c07112d6 100644 --- a/docs/build.sbt +++ b/docs/build.sbt @@ -1,8 +1,7 @@ // You will need private bintray credentials to publish this with Lightbend theme // credentials += Credentials("Bintray", "dl.bintray.com", "", "") -//resolvers += "bintray-typesafe-internal-maven-releases" at "https://dl.bintray.com/typesafe/internal-maven-releases/" -//libraryDependencies += "com.lightbend.paradox" % "paradox-theme-lightbend" % "0.2.1-TH2" -//paradoxTheme := Some("com.lightbend.paradox" % "paradox-theme-lightbend" % "0.2.1-TH2") +// resolvers += "bintray-typesafe-internal-maven-releases" at "https://dl.bintray.com/typesafe/internal-maven-releases/" +// paradoxTheme := Some("com.lightbend.paradox" % "paradox-theme-lightbend" % "0.2.3") // Uses the out of the box generic theme. paradoxTheme := Some(builtinParadoxTheme("generic")) From 5efed82e26936b36828f0ed50698db5006696492 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Fri, 28 Oct 2016 18:14:27 -0700 Subject: [PATCH 19/73] Updated with template-control on 2016-10-29T01:14:27.816Z **/build.properties: sbt.version=0.13.13 --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index 35c88bab7..27e88aa11 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.12 +sbt.version=0.13.13 From 62d34d0790a915177d249ed76beb49b9df492156 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Wed, 9 Nov 2016 21:26:37 -0800 Subject: [PATCH 20/73] Updated with template-control on 2016-11-10T05:26:37.584Z /LICENSE: wrote /LICENSE --- LICENSE | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/LICENSE b/LICENSE index 4baedcb95..b018ae2bc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,8 +1,7 @@ -This software is licensed under the Apache 2 license, quoted below. +License +------- +Written in 2016 by Lightbend -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with -the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. +To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an -"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific -language governing permissions and limitations under the License. \ No newline at end of file +You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see . From 0374f627bc4e59383273a439687edd9e679539b6 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Fri, 18 Nov 2016 16:08:11 -0800 Subject: [PATCH 21/73] Updated with template-control on 2016-11-19T00:08:11.410Z /LICENSE: wrote /LICENSE **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.10") --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index b67a45f0c..6d4cbc854 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.9") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.10") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.1") From d1b272f72f2e68f4f49285666a8dada4a5560d7b Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Tue, 7 Feb 2017 15:15:15 -0800 Subject: [PATCH 22/73] Updated with template-control on 2017-02-07T23:15:15.224Z /LICENSE: wrote /LICENSE **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.12") --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 6d4cbc854..763df6b70 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.10") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.12") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.1") From 31f81a2e646a2a851684e355edfc0f09bf6fc2b6 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Sat, 18 Feb 2017 15:46:36 -0800 Subject: [PATCH 23/73] Upgrade Play 2.6.0 --- app/RequestHandler.scala | 11 +++++++++-- app/controllers/HomeController.scala | 6 ++++-- app/v1/post/PostAction.scala | 9 ++++++--- build.sbt | 6 ++++-- conf/secure.conf | 2 -- docs/build.sbt | 1 - project/plugins.sbt | 4 ++-- 7 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/RequestHandler.scala b/app/RequestHandler.scala index 8bcbab09c..7a4fdd1f7 100644 --- a/app/RequestHandler.scala +++ b/app/RequestHandler.scala @@ -2,6 +2,7 @@ import javax.inject.Inject import play.api.http._ import play.api.mvc._ +import play.api.mvc.request.{RequestFactory, RequestTarget} import play.api.routing.Router /** @@ -40,9 +41,15 @@ class RequestHandler @Inject()(router: Router, if (!origReq.path.endsWith("/")) { val path = origReq.path + "/" if (origReq.rawQueryString.isEmpty) { - origReq.copy(path = path, uri = path) + origReq.withTarget( + RequestTarget(path = path, uriString = path, queryString = Map()) + ) } else { - origReq.copy(path = path, uri = path + s"?${origReq.rawQueryString}") + origReq.withTarget( + RequestTarget(path = path, + uriString = origReq.uri, + queryString = origReq.queryString) + ) } } else { origReq diff --git a/app/controllers/HomeController.scala b/app/controllers/HomeController.scala index 35b5972e9..92eba5f3f 100644 --- a/app/controllers/HomeController.scala +++ b/app/controllers/HomeController.scala @@ -1,11 +1,13 @@ package controllers -import play.api.mvc.{Action, Controller} +import javax.inject.Inject + +import play.api.mvc._ /** * A very small controller that renders a home page. */ -class HomeController extends Controller { +class HomeController @Inject()(cc: ControllerComponents) extends AbstractController(cc) { def index = Action { implicit request => Ok(views.html.index()) diff --git a/app/v1/post/PostAction.scala b/app/v1/post/PostAction.scala index 6f0fdec93..55146d1dd 100644 --- a/app/v1/post/PostAction.scala +++ b/app/v1/post/PostAction.scala @@ -24,11 +24,13 @@ class PostRequest[A](request: Request[A], val messages: Messages) * the request with contextual data, and manipulate the * result. */ -class PostAction @Inject()(messagesApi: MessagesApi)( - implicit ec: ExecutionContext) - extends ActionBuilder[PostRequest] +class PostAction @Inject()(messagesApi: MessagesApi, playBodyParsers: PlayBodyParsers) + (implicit val executionContext: ExecutionContext) + extends ActionBuilder[PostRequest, AnyContent] with HttpVerbs { + val parser: BodyParser[AnyContent] = playBodyParsers.anyContent + type PostRequestBlock[A] = PostRequest[A] => Future[Result] private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) @@ -51,4 +53,5 @@ class PostAction @Inject()(messagesApi: MessagesApi)( } } } + } diff --git a/build.sbt b/build.sbt index 1f5ce27de..08990bcda 100644 --- a/build.sbt +++ b/build.sbt @@ -4,9 +4,11 @@ lazy val GatlingTest = config("gatling") extend Test scalaVersion := "2.11.8" -libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.14" +libraryDependencies += guice +libraryDependencies += "org.joda" % "joda-convert" % "1.8" +libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.16" libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" -libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test +libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "2.0.0-M2" % Test libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % Test libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.2.2" % Test diff --git a/conf/secure.conf b/conf/secure.conf index ff9d0720a..be67ccbb3 100644 --- a/conf/secure.conf +++ b/conf/secure.conf @@ -1,8 +1,6 @@ # Set up Play for HTTPS and locked down allowed hosts. # Nothing in here is required for REST, but it's a good default. play { - crypto.secret = "changeme" - http { cookies.strict = true diff --git a/docs/build.sbt b/docs/build.sbt index 6c07112d6..12d8b4d78 100644 --- a/docs/build.sbt +++ b/docs/build.sbt @@ -6,4 +6,3 @@ // Uses the out of the box generic theme. paradoxTheme := Some(builtinParadoxTheme("generic")) -scalaVersion := "2.11.8" diff --git a/project/plugins.sbt b/project/plugins.sbt index 763df6b70..85f68896e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,8 +1,8 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.12") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-M1") // sbt-paradox, used for documentation -addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.1") +addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.9") // Load testing tool: // http://gatling.io/docs/2.2.2/extensions/sbt_plugin.html From da2f3ed89b49cebcf6cdbb7ca81af2fa02f2c80e Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Sat, 18 Feb 2017 19:39:20 -0800 Subject: [PATCH 24/73] Updated with template-control on 2017-02-19T03:39:20.313Z /LICENSE: wrote /LICENSE **/build.sbt: scalaVersion := "2.12.1" --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 08990bcda..dd9c2c927 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import sbt.Keys._ lazy val GatlingTest = config("gatling") extend Test -scalaVersion := "2.11.8" +scalaVersion := "2.12.1" libraryDependencies += guice libraryDependencies += "org.joda" % "joda-convert" % "1.8" From d9a5128df9190c757fb9f953764407b81575c27e Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Tue, 28 Mar 2017 13:57:27 -0700 Subject: [PATCH 25/73] Updated with template-control on 2017-03-28T20:57:27.836Z /LICENSE: wrote /LICENSE **/build.sbt: libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.0.0-M2" % Test **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-M3") --- build.sbt | 2 +- project/plugins.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index dd9c2c927..f27eb7784 100644 --- a/build.sbt +++ b/build.sbt @@ -8,7 +8,7 @@ libraryDependencies += guice libraryDependencies += "org.joda" % "joda-convert" % "1.8" libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.16" libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" -libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "2.0.0-M2" % Test +libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.0.0-M2" % Test libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % Test libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.2.2" % Test diff --git a/project/plugins.sbt b/project/plugins.sbt index 85f68896e..eb66b550e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-M1") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-M3") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.9") From bf7264d314e36e2f4499454b150f53d6e5b9c69e Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Tue, 4 Apr 2017 12:48:10 -0700 Subject: [PATCH 26/73] Add tests --- .travis.yml | 20 ++++++++++++++++++++ app/Module.scala | 2 +- build.sbt | 6 +++--- test/controllers/HomeControllerSpec.scala | 20 ++++++++++++++++++++ 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 .travis.yml create mode 100644 test/controllers/HomeControllerSpec.scala diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..a6aa69a11 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: scala +dist: trusty +sudo: true +group: beta +scala: +- 2.11.8 +jdk: +- oraclejdk8 +cache: + directories: + - "$HOME/.ivy2/cache" + - "$HOME/.sbt/boot/" +before_cache: +- rm -rf $HOME/.ivy2/cache/com.typesafe.play/* +- rm -rf $HOME/.ivy2/cache/scala_*/sbt_*/com.typesafe.play/* +- find $HOME/.ivy2/cache -name "ivydata-*.properties" -print0 | xargs -n10 -0 rm +- find $HOME/.sbt -name "*.lock" -delete +notifications: + slack: + secure: PskU6+VapjwI01Ty8Ya+5imNpD34hzzG4sYx1tIMvkg6F2x7JfYKDihotYVCdJbNYiPTbmOr529Iv7q+31WvMzmzD9knH9hEhCz6Ojali5FziTdUYfAOuQcgeI+tKtmHk/T6ks6I2ksFzJrRmanlhjMo+ENblIyB92kgvZWzlkX/pERjmMmvL9HH9eU3KxA2BxIRvVE6YyrhqnZJYAk0CqwIDwtKFePIMQaBJQkDBWVlryyVBDggItp1fqGEt4Zxt92eE0rkWTko3ejx0kjsiOLOqluhi8TekrZpvQJFZbIfP/2RRlv6JkItzt6BZz2iyCaQKWv6BUnUDwdjTvXfy/zriH0SSvKbxQqla0KCd8W33XAYzj8L6YvGeImsiOaviOemjdgHJLxg8MaTvB/Jyzno1S/A8qeE+wXy8++dqviAxKwGAzVpQ9KDSaKu/HVhXlPJCOO/i9Ut7O+SHlA3vSGgRTnI7GgIZgCn6FAKaeQO5ExxUzgxTsB+N+WonHlDGPxU2Jr8VAbj2jcMAOJhWYUPLxFT4JRGZeycK6SF+WOukQGFDRFL7trgDzhSDzZaj/FIuUHiv5Ih+ZkxoDTWZb3gXopNgAc/5Hhtx8YyKLZJ5G1V8+FV0/OLdFlgqRyIdrmoSDxu/EG3F/0NBpY856YhKc6zFMe/feL4RQnFFWU= \ No newline at end of file diff --git a/app/Module.scala b/app/Module.scala index 7da2ce9f7..6e8589c4b 100644 --- a/app/Module.scala +++ b/app/Module.scala @@ -8,7 +8,7 @@ import v1.post._ /** * Sets up custom components for Play. * - * https://www.playframework.com/documentation/2.5.x/ScalaDependencyInjection + * https://www.playframework.com/documentation/latest/ScalaDependencyInjection */ class Module(environment: Environment, configuration: Configuration) extends AbstractModule diff --git a/build.sbt b/build.sbt index f27eb7784..5113bc738 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import sbt.Keys._ lazy val GatlingTest = config("gatling") extend Test -scalaVersion := "2.12.1" +scalaVersion := "2.11.8" libraryDependencies += guice libraryDependencies += "org.joda" % "joda-convert" % "1.8" @@ -11,14 +11,14 @@ libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.0.0-M2" % Test libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % Test libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.2.2" % Test - + // The Play project itself lazy val root = (project in file(".")) .enablePlugins(Common, PlayScala, GatlingPlugin) .configs(GatlingTest) .settings(inConfig(GatlingTest)(Defaults.testSettings): _*) .settings( - name := """play-rest-api""", + name := """play-scala-rest-api-example""", scalaSource in GatlingTest := baseDirectory.value / "/gatling/simulation" ) diff --git a/test/controllers/HomeControllerSpec.scala b/test/controllers/HomeControllerSpec.scala new file mode 100644 index 000000000..28d1dbdcf --- /dev/null +++ b/test/controllers/HomeControllerSpec.scala @@ -0,0 +1,20 @@ +import org.scalatestplus.play._ +import org.scalatestplus.play.guice._ +import play.api.test._ +import play.api.test.Helpers._ +import play.api.test.CSRFTokenHelper._ + +class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest { + + "HomeController" should { + + "render the index page" in { + val request = FakeRequest(GET, "/").withHeaders(HOST -> "localhost:9000").withCSRFToken + val home = route(app, request).get + + contentAsString(home) must include ("This is a placeholder page to show you the REST API.") + } + + } + +} \ No newline at end of file From d38f8e335e2960695d98c9e4c697d4c37cae7cc5 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Sat, 8 Apr 2017 21:09:19 -0700 Subject: [PATCH 27/73] Updated with template-control on 2017-04-09T04:09:19.466Z /LICENSE: wrote /LICENSE **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-M4") --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index eb66b550e..cca536cd1 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-M3") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-M4") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.9") From 957748ae5859ba7611aa2152446615ab6d7e1535 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Thu, 13 Apr 2017 15:27:54 -0700 Subject: [PATCH 28/73] Update scalatestplus-play --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 5113bc738..fb54cd8ed 100644 --- a/build.sbt +++ b/build.sbt @@ -8,7 +8,7 @@ libraryDependencies += guice libraryDependencies += "org.joda" % "joda-convert" % "1.8" libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.16" libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" -libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.0.0-M2" % Test +libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.0.0-M3" % Test libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % Test libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.2.2" % Test From f74f94509da874793ad439080e9b26b8b5ad3a4f Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Tue, 18 Apr 2017 15:00:25 -0700 Subject: [PATCH 29/73] Updated with template-control on 2017-04-18T22:00:25.701Z /LICENSE: wrote /LICENSE **/build.sbt: scalaVersion := "2.12.2" **/build.properties: sbt.version=0.13.15 --- build.sbt | 2 +- project/build.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index fb54cd8ed..f3ea16a87 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import sbt.Keys._ lazy val GatlingTest = config("gatling") extend Test -scalaVersion := "2.11.8" +scalaVersion := "2.12.2" libraryDependencies += guice libraryDependencies += "org.joda" % "joda-convert" % "1.8" diff --git a/project/build.properties b/project/build.properties index 27e88aa11..64317fdae 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.13 +sbt.version=0.13.15 From bdc3da7b419fdf998377bc1ba96a882f679b6bd1 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Fri, 28 Apr 2017 13:27:59 -0700 Subject: [PATCH 30/73] Upgrade with 2.6.x features --- app/v1/post/PostAction.scala | 57 --------------- app/v1/post/PostActionBuilder.scala | 106 ++++++++++++++++++++++++++++ app/v1/post/PostController.scala | 39 +++++----- app/v1/post/package.scala | 16 ----- build.sbt | 6 +- conf/logback.xml | 20 ++++++ project/plugins.sbt | 2 +- 7 files changed, 150 insertions(+), 96 deletions(-) delete mode 100644 app/v1/post/PostAction.scala create mode 100644 app/v1/post/PostActionBuilder.scala delete mode 100644 app/v1/post/package.scala diff --git a/app/v1/post/PostAction.scala b/app/v1/post/PostAction.scala deleted file mode 100644 index 55146d1dd..000000000 --- a/app/v1/post/PostAction.scala +++ /dev/null @@ -1,57 +0,0 @@ -package v1.post - -import javax.inject.Inject - -import play.api.http.HttpVerbs -import play.api.i18n.{Messages, MessagesApi} -import play.api.mvc._ - -import scala.concurrent.{ExecutionContext, Future} - -/** - * A wrapped request for post resources. - * - * This is commonly used to hold request-specific information like - * security credentials, and useful shortcut methods. - */ -class PostRequest[A](request: Request[A], val messages: Messages) - extends WrappedRequest(request) - -/** - * The default action for the Post resource. - * - * This is the place to put logging, metrics, to augment - * the request with contextual data, and manipulate the - * result. - */ -class PostAction @Inject()(messagesApi: MessagesApi, playBodyParsers: PlayBodyParsers) - (implicit val executionContext: ExecutionContext) - extends ActionBuilder[PostRequest, AnyContent] - with HttpVerbs { - - val parser: BodyParser[AnyContent] = playBodyParsers.anyContent - - type PostRequestBlock[A] = PostRequest[A] => Future[Result] - - private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) - - override def invokeBlock[A](request: Request[A], - block: PostRequestBlock[A]): Future[Result] = { - if (logger.isTraceEnabled()) { - logger.trace(s"invokeBlock: request = $request") - } - - val messages = messagesApi.preferred(request) - val future = block(new PostRequest(request, messages)) - - future.map { result => - request.method match { - case GET | HEAD => - result.withHeaders("Cache-Control" -> s"max-age: 100") - case other => - result - } - } - } - -} diff --git a/app/v1/post/PostActionBuilder.scala b/app/v1/post/PostActionBuilder.scala new file mode 100644 index 000000000..a7838bb2d --- /dev/null +++ b/app/v1/post/PostActionBuilder.scala @@ -0,0 +1,106 @@ +package v1.post + +import javax.inject.Inject + +import net.logstash.logback.marker.LogstashMarker +import play.api.{Logger, MarkerContext} +import play.api.http.{FileMimeTypes, HttpVerbs} +import play.api.i18n.{Langs, MessagesApi} +import play.api.mvc._ + +import scala.concurrent.{ExecutionContext, Future} +import scala.language.implicitConversions + +/** + * A wrapped request for post resources. + * + * This is commonly used to hold request-specific information like + * security credentials, and useful shortcut methods. + */ +trait PostRequestHeader extends MessagesRequestHeader with PreferredMessagesProvider +class PostRequest[A](request: Request[A], val messagesApi: MessagesApi) extends WrappedRequest(request) with PostRequestHeader + +/** + * Provides an implicit marker that will show the request in all logger statements. + */ +trait RequestMarkerContext { + import net.logstash.logback.marker.Markers + + private def marker(tuple: (String, Any)) = Markers.append(tuple._1, tuple._2) + + private implicit class RichLogstashMarker(marker1: LogstashMarker) { + def &&(marker2: LogstashMarker): LogstashMarker = marker1.and(marker2) + } + + implicit def requestHeaderToMarkerContext(implicit request: RequestHeader): MarkerContext = { + MarkerContext { + marker("id" -> request.id) && marker("host" -> request.host) && marker("remoteAddress" -> request.remoteAddress) + } + } + +} + +/** + * The action builder for the Post resource. + * + * This is the place to put logging, metrics, to augment + * the request with contextual data, and manipulate the + * result. + */ +class PostActionBuilder @Inject()(messagesApi: MessagesApi, playBodyParsers: PlayBodyParsers) + (implicit val executionContext: ExecutionContext) + extends ActionBuilder[PostRequest, AnyContent] + with RequestMarkerContext + with HttpVerbs { + + val parser: BodyParser[AnyContent] = playBodyParsers.anyContent + + type PostRequestBlock[A] = PostRequest[A] => Future[Result] + + private val logger = Logger(this.getClass) + + override def invokeBlock[A](request: Request[A], + block: PostRequestBlock[A]): Future[Result] = { + // Convert to marker context and use request in block + implicit val markerContext: MarkerContext = requestHeaderToMarkerContext(request) + logger.trace(s"invokeBlock: ") + + val future = block(new PostRequest(request, messagesApi)) + + future.map { result => + request.method match { + case GET | HEAD => + result.withHeaders("Cache-Control" -> s"max-age: 100") + case other => + result + } + } + } +} + +/** + * Packages up the component dependencies for the post controller. + * + * This is a good way to minimize the surface area exposed to the controller, so the + * controller only has to have one thing injected. + */ +case class PostControllerComponents @Inject()(postActionBuilder: PostActionBuilder, + postResourceHandler: PostResourceHandler, + actionBuilder: DefaultActionBuilder, + parsers: PlayBodyParsers, + messagesApi: MessagesApi, + langs: Langs, + fileMimeTypes: FileMimeTypes, + executionContext: scala.concurrent.ExecutionContext) + extends ControllerComponents + +/** + * Exposes actions and handler to the PostController by wiring the injected state into the base class. + */ +class PostBaseController @Inject()(pcc: PostControllerComponents) extends BaseController { + override protected def controllerComponents: ControllerComponents = pcc + + def PostAction: PostActionBuilder = pcc.postActionBuilder + + def postResourceHandler: PostResourceHandler = pcc.postResourceHandler +} \ No newline at end of file diff --git a/app/v1/post/PostController.scala b/app/v1/post/PostController.scala index 19e70240c..b62a786da 100644 --- a/app/v1/post/PostController.scala +++ b/app/v1/post/PostController.scala @@ -2,6 +2,7 @@ package v1.post import javax.inject.Inject +import play.api.Logger import play.api.data.Form import play.api.libs.json.Json import play.api.mvc._ @@ -13,10 +14,10 @@ case class PostFormInput(title: String, body: String) /** * Takes HTTP requests and produces JSON. */ -class PostController @Inject()( - action: PostAction, - handler: PostResourceHandler)(implicit ec: ExecutionContext) - extends Controller { +class PostController @Inject()(cc: PostControllerComponents)(implicit ec: ExecutionContext) + extends PostBaseController(cc) with RequestMarkerContext { + + private val logger = Logger(getClass) private val form: Form[PostFormInput] = { import play.api.data.Forms._ @@ -29,36 +30,32 @@ class PostController @Inject()( ) } - def index: Action[AnyContent] = { - action.async { implicit request => - handler.find.map { posts => - Ok(Json.toJson(posts)) - } + def index: Action[AnyContent] = PostAction.async { implicit request => + logger.trace("index: ") + postResourceHandler.find.map { posts => + Ok(Json.toJson(posts)) } } - def process: Action[AnyContent] = { - action.async { implicit request => - processJsonPost() - } + def process: Action[AnyContent] = PostAction.async { implicit request => + logger.trace("process: ") + processJsonPost() } - def show(id: String): Action[AnyContent] = { - action.async { implicit request => - handler.lookup(id).map { post => - Ok(Json.toJson(post)) - } + def show(id: String): Action[AnyContent] = PostAction.async { implicit request => + logger.trace(s"show: id = $id") + postResourceHandler.lookup(id).map { post => + Ok(Json.toJson(post)) } } - private def processJsonPost[A]()( - implicit request: PostRequest[A]): Future[Result] = { + private def processJsonPost[A]()(implicit request: PostRequest[A]): Future[Result] = { def failure(badForm: Form[PostFormInput]) = { Future.successful(BadRequest(badForm.errorsAsJson)) } def success(input: PostFormInput) = { - handler.create(input).map { post => + postResourceHandler.create(input).map { post => Created(Json.toJson(post)).withHeaders(LOCATION -> post.link) } } diff --git a/app/v1/post/package.scala b/app/v1/post/package.scala deleted file mode 100644 index 7903bbe43..000000000 --- a/app/v1/post/package.scala +++ /dev/null @@ -1,16 +0,0 @@ -package v1 - -import play.api.i18n.Messages - -/** - * Package object for post. This is a good place to put implicit conversions. - */ -package object post { - - /** - * Converts between PostRequest and Messages automatically. - */ - implicit def requestToMessages[A](implicit r: PostRequest[A]): Messages = { - r.messages - } -} diff --git a/build.sbt b/build.sbt index f3ea16a87..e244f5681 100644 --- a/build.sbt +++ b/build.sbt @@ -2,12 +2,16 @@ import sbt.Keys._ lazy val GatlingTest = config("gatling") extend Test -scalaVersion := "2.12.2" +// This must be set to 2.11.11 because Gatling does not run on 2.12.2 +scalaVersion in ThisBuild := "2.11.11" libraryDependencies += guice libraryDependencies += "org.joda" % "joda-convert" % "1.8" +libraryDependencies += "net.logstash.logback" % "logstash-logback-encoder" % "4.9" + libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.16" libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" + libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.0.0-M3" % Test libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % Test libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.2.2" % Test diff --git a/conf/logback.xml b/conf/logback.xml index fa73fe6fa..e895d6152 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -10,6 +10,20 @@ + + ${application.home:-.}/logs/application.json + + + + + + + + + + + + ${application.home:-.}/logs/metrics.log @@ -28,15 +42,21 @@ + + + + + + diff --git a/project/plugins.sbt b/project/plugins.sbt index cca536cd1..8c98ee560 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-M4") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-M5") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.9") From 57c912cef6bab4db223c3fb3d540df1e3f5feb24 Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Fri, 28 Apr 2017 14:02:57 -0700 Subject: [PATCH 31/73] Use distinct execution context --- .travis.yml | 2 +- app/ErrorHandler.scala | 2 +- app/v1/post/PostActionBuilder.scala | 2 +- app/v1/post/PostController.scala | 2 +- app/v1/post/PostRepository.scala | 33 ++++++++++++++++++--------- app/v1/post/PostResourceHandler.scala | 9 ++++---- conf/application.conf | 10 ++++++++ conf/logback.xml | 3 ++- 8 files changed, 43 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index a6aa69a11..2af1d83d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ dist: trusty sudo: true group: beta scala: -- 2.11.8 +- 2.11.11 jdk: - oraclejdk8 cache: diff --git a/app/ErrorHandler.scala b/app/ErrorHandler.scala index c7dc428ea..8bfb6d21d 100644 --- a/app/ErrorHandler.scala +++ b/app/ErrorHandler.scala @@ -15,7 +15,7 @@ import scala.concurrent._ * Provides a stripped down error handler that does not use HTML in error pages, and * prints out debugging output. * - * https://www.playframework.com/documentation/2.5.x/ScalaErrorHandling + * https://www.playframework.com/documentation/latest/ScalaErrorHandling */ class ErrorHandler(environment: Environment, configuration: Configuration, diff --git a/app/v1/post/PostActionBuilder.scala b/app/v1/post/PostActionBuilder.scala index a7838bb2d..4de22802f 100644 --- a/app/v1/post/PostActionBuilder.scala +++ b/app/v1/post/PostActionBuilder.scala @@ -97,7 +97,7 @@ case class PostControllerComponents @Inject()(postActionBuilder: PostActionBuild /** * Exposes actions and handler to the PostController by wiring the injected state into the base class. */ -class PostBaseController @Inject()(pcc: PostControllerComponents) extends BaseController { +class PostBaseController @Inject()(pcc: PostControllerComponents) extends BaseController with RequestMarkerContext { override protected def controllerComponents: ControllerComponents = pcc def PostAction: PostActionBuilder = pcc.postActionBuilder diff --git a/app/v1/post/PostController.scala b/app/v1/post/PostController.scala index b62a786da..1627ff0f0 100644 --- a/app/v1/post/PostController.scala +++ b/app/v1/post/PostController.scala @@ -15,7 +15,7 @@ case class PostFormInput(title: String, body: String) * Takes HTTP requests and produces JSON. */ class PostController @Inject()(cc: PostControllerComponents)(implicit ec: ExecutionContext) - extends PostBaseController(cc) with RequestMarkerContext { + extends PostBaseController(cc) { private val logger = Logger(getClass) diff --git a/app/v1/post/PostRepository.scala b/app/v1/post/PostRepository.scala index 8005dfe58..e1b7672be 100644 --- a/app/v1/post/PostRepository.scala +++ b/app/v1/post/PostRepository.scala @@ -2,6 +2,10 @@ package v1.post import javax.inject.{Inject, Singleton} +import akka.actor.ActorSystem +import play.api.libs.concurrent.CustomExecutionContext +import play.api.{Logger, MarkerContext} + import scala.concurrent.Future final case class PostData(id: PostId, title: String, body: String) @@ -17,24 +21,31 @@ object PostId { } } + +class PostExecutionContext @Inject()(actorSystem: ActorSystem) extends CustomExecutionContext(actorSystem, "repository.dispatcher") + /** * A pure non-blocking interface for the PostRepository. */ trait PostRepository { - def create(data: PostData): Future[PostId] + def create(data: PostData)(implicit mc: MarkerContext): Future[PostId] - def list(): Future[Iterable[PostData]] + def list()(implicit mc: MarkerContext): Future[Iterable[PostData]] - def get(id: PostId): Future[Option[PostData]] + def get(id: PostId)(implicit mc: MarkerContext): Future[Option[PostData]] } /** * A trivial implementation for the Post Repository. + * + * A custom execution context is used here to establish that blocking operations should be + * executed in a different thread than Play's ExecutionContext, which is used for CPU bound tasks + * such as rendering. */ @Singleton -class PostRepositoryImpl @Inject() extends PostRepository { +class PostRepositoryImpl @Inject()()(implicit ec: PostExecutionContext) extends PostRepository { - private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) + private val logger = Logger(this.getClass) private val postList = List( PostData(PostId("1"), "title 1", "blog post 1"), @@ -44,22 +55,22 @@ class PostRepositoryImpl @Inject() extends PostRepository { PostData(PostId("5"), "title 5", "blog post 5") ) - override def list(): Future[Iterable[PostData]] = { - Future.successful { + override def list()(implicit mc: MarkerContext): Future[Iterable[PostData]] = { + Future { logger.trace(s"list: ") postList } } - override def get(id: PostId): Future[Option[PostData]] = { - Future.successful { + override def get(id: PostId)(implicit mc: MarkerContext): Future[Option[PostData]] = { + Future { logger.trace(s"get: id = $id") postList.find(post => post.id == id) } } - def create(data: PostData): Future[PostId] = { - Future.successful { + def create(data: PostData)(implicit mc: MarkerContext): Future[PostId] = { + Future { logger.trace(s"create: data = $data") data.id } diff --git a/app/v1/post/PostResourceHandler.scala b/app/v1/post/PostResourceHandler.scala index 9f1224ca6..44fd637af 100644 --- a/app/v1/post/PostResourceHandler.scala +++ b/app/v1/post/PostResourceHandler.scala @@ -2,8 +2,9 @@ package v1.post import javax.inject.{Inject, Provider} -import scala.concurrent.{ExecutionContext, Future} +import play.api.MarkerContext +import scala.concurrent.{ExecutionContext, Future} import play.api.libs.json._ /** @@ -35,7 +36,7 @@ class PostResourceHandler @Inject()( routerProvider: Provider[PostRouter], postRepository: PostRepository)(implicit ec: ExecutionContext) { - def create(postInput: PostFormInput): Future[PostResource] = { + def create(postInput: PostFormInput)(implicit mc: MarkerContext): Future[PostResource] = { val data = PostData(PostId("999"), postInput.title, postInput.body) // We don't actually create the post, so return what we have postRepository.create(data).map { id => @@ -43,7 +44,7 @@ class PostResourceHandler @Inject()( } } - def lookup(id: String): Future[Option[PostResource]] = { + def lookup(id: String)(implicit mc: MarkerContext): Future[Option[PostResource]] = { val postFuture = postRepository.get(PostId(id)) postFuture.map { maybePostData => maybePostData.map { postData => @@ -52,7 +53,7 @@ class PostResourceHandler @Inject()( } } - def find: Future[Iterable[PostResource]] = { + def find(implicit mc: MarkerContext): Future[Iterable[PostResource]] = { postRepository.list().map { postDataList => postDataList.map(postData => createPostResource(postData)) } diff --git a/conf/application.conf b/conf/application.conf index e1212f531..25a012bb3 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -1,2 +1,12 @@ include "secure" +# db connections = ((physical_core_count * 2) + effective_spindle_count) +fixedConnectionPool = 5 + +repository.dispatcher { + executor = "thread-pool-executor" + throughput = 1 + thread-pool-executor { + fixed-pool-size = ${fixedConnectionPool} + } +} \ No newline at end of file diff --git a/conf/logback.xml b/conf/logback.xml index e895d6152..57adc2ac0 100644 --- a/conf/logback.xml +++ b/conf/logback.xml @@ -14,9 +14,10 @@ ${application.home:-.}/logs/application.json - + + From 403b48f05711433a960e352cdce7e63afea48ddd Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Mon, 1 May 2017 11:22:06 -0700 Subject: [PATCH 32/73] Fix ECS url and add note to Java API example --- README.md | 2 +- docs/src/main/paradox/index.md | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b3da8602d..4552981b4 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Once you have sbt installed, the following at the command prompt will start up P sbt run ``` -Play will start up on the HTTP port at http://localhost:9000/. You don't need to reploy or reload anything -- changing any source code while the server is running will automatically recompile and hot-reload the application on the next HTTP request. +Play will start up on the HTTP port at http://localhost:9000/. You don't need to deploy or reload anything -- changing any source code while the server is running will automatically recompile and hot-reload the application on the next HTTP request. ### Usage diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index 9ec4ea2e1..f40649785 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -4,12 +4,12 @@ This is a multi-part guide to walk you through how to make a RESTful API with JS We’ll demonstrate with a “best practices” REST API. You can get source code for this guide two ways: -* Download a pre-packaged bundle with this link [https://example.lightbend.com/v1/download/play-rest-api](https://example.lightbend.com/v1/download/play-rest-api) +* Download a pre-packaged bundle with this link [https://example.lightbend.com/v1/download/play-scala-rest-api-example](https://example.lightbend.com/v1/download/play-scala-rest-api-example) * Linux/Mac: ```bash -unzip play-rest-api.zip -cd play-rest-api +unzip play-scala-rest-api-example.zip +cd play-scala-rest-api-example ./sbt ``` * Windows: @@ -20,12 +20,13 @@ cd play-rest-api sbt.bat ``` -* [From Github](http://github.com/playframework/play-rest-api): +* [From Github](https://github.com/playframework/play-scala-rest-api-example/tree/2.5.x): ``` -git clone https://github.com/playframework/play-rest-api.git +git clone https://github.com/playframework/play-scala-rest-api-example.git +git checkout 2.5.x ``` -This example is in Scala, but Play also has a [Java API](https://www.playframework.com/documentation/2.5.x/JavaHome) which looks and acts just like the [Scala API](https://www.playframework.com/documentation/2.5.x/ScalaHome). For instructions on running and using the project, please see the [[appendix]]. This project also comes with an integrated [Gatling](http://gatling.io/) load test -- again, instructions are in the [[appendix]]. +This example is in Scala, but Play also has a [Java API](https://www.playframework.com/documentation/2.5.x/JavaHome) which looks and acts just like the [Scala API](https://www.playframework.com/documentation/2.5.x/ScalaHome), and has a corresponding [play-java-rest-api-example](https://github.com/playframework/play-java-rest-api-example) project. For instructions on running and using the project, please see the [[appendix]]. This project also comes with an integrated [Gatling](http://gatling.io/) load test -- again, instructions are in the appendix. Note that there’s more involved in a REST API -- monitoring, representation, and managing access to back end resources -- that we'll cover in subsequent posts. But first, let's address why Play is so effective as a REST API. From 2e70be8b6363ccf369119a97e4bd1d0a15eaf35f Mon Sep 17 00:00:00 2001 From: Will Sargent Date: Tue, 2 May 2017 10:41:18 -0700 Subject: [PATCH 33/73] Updated with template-control on 2017-05-02T17:41:18.656Z /LICENSE: wrote /LICENSE --- conf/application.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/application.conf b/conf/application.conf index 25a012bb3..15aa560a9 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -9,4 +9,4 @@ repository.dispatcher { thread-pool-executor { fixed-pool-size = ${fixedConnectionPool} } -} \ No newline at end of file +} From d6bb5ce3ec200cf810828040397c05684b4cbf07 Mon Sep 17 00:00:00 2001 From: Greg Methvin Date: Sun, 4 Jun 2017 00:24:58 +0200 Subject: [PATCH 34/73] Updated with template-control on 2017-06-03T16:01:09.662Z (#70) **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-RC2") --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 8c98ee560..aa0cb0e52 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-M5") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-RC2") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.9") From f5ae3c11ac5355cb0a46a893f0f101146dd6991d Mon Sep 17 00:00:00 2001 From: Greg Methvin Date: Fri, 23 Jun 2017 02:18:36 +0000 Subject: [PATCH 35/73] Updated with template-control on 2017-06-23T02:18:35.951Z **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0") --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index aa0cb0e52..e06b41519 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0-RC2") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.9") From 512704e32a6ea9cc9b7cc660165fa98cdf510196 Mon Sep 17 00:00:00 2001 From: Greg Methvin Date: Fri, 7 Jul 2017 00:45:45 +0000 Subject: [PATCH 36/73] Updated with template-control on 2017-07-07T00:45:45.210Z **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.1") **/plugins.sbt: addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.1") --- project/plugins.sbt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index e06b41519..3522ac914 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,12 +1,12 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.0") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.1") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.9") // Load testing tool: // http://gatling.io/docs/2.2.2/extensions/sbt_plugin.html -addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.0") +addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.1") // Scala formatting: "sbt scalafmt" // https://olafurpg.github.io/scalafmt From 8382b60d0d1ed8a9791414170c264d94b16d2e3f Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 20 Jul 2017 00:07:10 -0300 Subject: [PATCH 37/73] Updated with template-control on 2017-07-20T01:10:02.605Z (#81) **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.2") --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 3522ac914..d499150c1 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.1") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.2") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.9") From b16f997569c9e5e35805ffdd2113722a6b2f8919 Mon Sep 17 00:00:00 2001 From: Rich Dougherty Date: Sat, 12 Aug 2017 16:30:12 +1200 Subject: [PATCH 38/73] Updated with template-control on 2017-08-12T02:43:20.409Z (#82) **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.3") --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index d499150c1..e2485ccea 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.2") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.3") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.9") From 61541b26a88806ce5409c9e43cab8c265e0414bd Mon Sep 17 00:00:00 2001 From: Greg Methvin Date: Thu, 14 Sep 2017 23:29:36 +0000 Subject: [PATCH 39/73] Updated with template-control on 2017-09-14T23:29:36.820Z **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.5") --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index e2485ccea..7de223529 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.3") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.5") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.9") From b298f6762aaebdc489e3d8328ebd0a7371c74a22 Mon Sep 17 00:00:00 2001 From: Greg Methvin Date: Thu, 26 Oct 2017 00:18:12 -0700 Subject: [PATCH 40/73] Update sbt plugins --- project/plugins.sbt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 7de223529..b7a0767fb 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,12 +2,11 @@ addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.5") // sbt-paradox, used for documentation -addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.2.9") +addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.1") // Load testing tool: // http://gatling.io/docs/2.2.2/extensions/sbt_plugin.html addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.1") // Scala formatting: "sbt scalafmt" -// https://olafurpg.github.io/scalafmt -addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "0.3.1") +addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.12") From db76ca40626c792b0c5ca1d9482428bd375908ff Mon Sep 17 00:00:00 2001 From: Greg Methvin Date: Thu, 5 Oct 2017 23:19:01 +0000 Subject: [PATCH 41/73] Updated with template-control on 2017-10-05T23:19:01.694Z **/build.properties: sbt.version=1.0.2 **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.6") **/plugins.sbt: addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.2") --- project/build.properties | 2 +- project/plugins.sbt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/project/build.properties b/project/build.properties index 64317fdae..b7dd3cb2a 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.15 +sbt.version=1.0.2 diff --git a/project/plugins.sbt b/project/plugins.sbt index b7a0767fb..6625e2b30 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,12 +1,12 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.5") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.6") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.1") // Load testing tool: // http://gatling.io/docs/2.2.2/extensions/sbt_plugin.html -addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.1") +addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.2") // Scala formatting: "sbt scalafmt" addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.12") From 5eb7a8423371877f272f0765c2e01e8e7a0560be Mon Sep 17 00:00:00 2001 From: Greg Methvin Date: Tue, 7 Nov 2017 11:11:30 -0800 Subject: [PATCH 42/73] Updated with template-control on 2017-11-02T02:51:11.409Z (#92) **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.7") --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 6625e2b30..766c029b8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.6") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.7") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.1") From 353071ecfc56a9826a310922d3f621ec72bd515a Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Mon, 27 Nov 2017 15:19:01 -0200 Subject: [PATCH 43/73] Update dependencies (#95) Fix #94 --- README.md | 2 +- build.sbt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4552981b4..9632efffc 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Start Play in production mode, by [staging the application](https://www.playfram ``` sbt stage cd target/universal/stage -bin/play-rest-api -Dplay.crypto.secret=testing +bin/play-rest-api-example -Dplay.crypto.secret=testing ``` Then you'll start the Gatling load test up (it's already integrated into the project): diff --git a/build.sbt b/build.sbt index e244f5681..bccb2142d 100644 --- a/build.sbt +++ b/build.sbt @@ -6,13 +6,13 @@ lazy val GatlingTest = config("gatling") extend Test scalaVersion in ThisBuild := "2.11.11" libraryDependencies += guice -libraryDependencies += "org.joda" % "joda-convert" % "1.8" -libraryDependencies += "net.logstash.logback" % "logstash-logback-encoder" % "4.9" +libraryDependencies += "org.joda" % "joda-convert" % "1.9.2" +libraryDependencies += "net.logstash.logback" % "logstash-logback-encoder" % "4.11" libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.16" -libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.0" +libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.1" -libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.0.0-M3" % Test +libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % Test libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.2.2" % Test From 22d130ad781f26052b2b483dea28b2103c28a604 Mon Sep 17 00:00:00 2001 From: Justin Pihony Date: Fri, 8 Dec 2017 21:43:01 -0500 Subject: [PATCH 44/73] Fix broken appendix link to be html instead of md --- docs/src/main/paradox/part-1/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/paradox/part-1/index.md b/docs/src/main/paradox/part-1/index.md index 1c2055cbf..16a43fe95 100644 --- a/docs/src/main/paradox/part-1/index.md +++ b/docs/src/main/paradox/part-1/index.md @@ -10,7 +10,7 @@ git clone https://github.com/playframework/play-rest-api.git We're going to be showing an already working Play project with most of the code available under the "app/v1" directory. There will be several different versions of the same project as this series expands, so you can compare different versions of the project against each other. -To run Play on your own local computer, please see the instructions in the [appendix](../appendix.md). +To run Play on your own local computer, please see the instructions in the [appendix](../appendix.html). ## Introduction From 3e6cf2c2b2081ed081b9f33f0a85887881293d3d Mon Sep 17 00:00:00 2001 From: Greg Methvin Date: Sat, 9 Dec 2017 03:16:28 +0000 Subject: [PATCH 45/73] Updated with template-control on 2017-12-09T03:16:28.024Z **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.9") --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 766c029b8..1ba3c73ca 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.7") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.9") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.1") From 41ed3420e12c585431708b391a6a426f1c81a237 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 19 Dec 2017 17:11:33 -0200 Subject: [PATCH 46/73] Add Gradle configuration and Java 9 support (#100) --- .gitignore | 2 + .travis.yml | 28 +++- README.md | 20 +-- app/RequestHandler.scala | 2 +- app/v1/post/PostActionBuilder.scala | 1 - build.gradle | 65 +++++++++ build.sbt | 7 +- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 172 +++++++++++++++++++++++ gradlew.bat | 84 +++++++++++ project/Common.scala | 3 +- project/build.properties | 2 +- scripts/script-helper | 13 ++ scripts/test-gradle | 13 ++ scripts/test-sbt | 8 ++ 16 files changed, 401 insertions(+), 24 deletions(-) create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 scripts/script-helper create mode 100755 scripts/test-gradle create mode 100755 scripts/test-sbt diff --git a/.gitignore b/.gitignore index 8fddb041c..3e83b1491 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ +build logs target /.idea /.idea_modules /.classpath +/.gradle /.project /.settings /RUNNING_PID diff --git a/.travis.yml b/.travis.yml index 2af1d83d9..74cc8fd60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,36 @@ language: scala -dist: trusty -sudo: true -group: beta scala: -- 2.11.11 +# When updating Scala versions, also check the excludes +# in build matrix below. +- 2.11.12 +- 2.12.4 jdk: - oraclejdk8 +- oraclejdk9 +env: + matrix: + - SCRIPT=scripts/test-sbt + - SCRIPT=scripts/test-gradle +script: +- $SCRIPT cache: directories: - "$HOME/.ivy2/cache" - - "$HOME/.sbt/boot/" + - "$HOME/.gradle/caches" before_cache: - rm -rf $HOME/.ivy2/cache/com.typesafe.play/* - rm -rf $HOME/.ivy2/cache/scala_*/sbt_*/com.typesafe.play/* - find $HOME/.ivy2/cache -name "ivydata-*.properties" -print0 | xargs -n10 -0 rm -- find $HOME/.sbt -name "*.lock" -delete + +# Exclude some combinations from build matrix. See: +# https://docs.travis-ci.com/user/customizing-the-build/#Build-Matrix +matrix: + exclude: + - scala: 2.11.12 + jdk: oraclejdk9 + +# See https://blog.travis-ci.com/2014-03-13-slack-notifications/ +# created with travis encrypt command line tool notifications: slack: secure: PskU6+VapjwI01Ty8Ya+5imNpD34hzzG4sYx1tIMvkg6F2x7JfYKDihotYVCdJbNYiPTbmOr529Iv7q+31WvMzmzD9knH9hEhCz6Ojali5FziTdUYfAOuQcgeI+tKtmHk/T6ks6I2ksFzJrRmanlhjMo+ENblIyB92kgvZWzlkX/pERjmMmvL9HH9eU3KxA2BxIRvVE6YyrhqnZJYAk0CqwIDwtKFePIMQaBJQkDBWVlryyVBDggItp1fqGEt4Zxt92eE0rkWTko3ejx0kjsiOLOqluhi8TekrZpvQJFZbIfP/2RRlv6JkItzt6BZz2iyCaQKWv6BUnUDwdjTvXfy/zriH0SSvKbxQqla0KCd8W33XAYzj8L6YvGeImsiOaviOemjdgHJLxg8MaTvB/Jyzno1S/A8qeE+wXy8++dqviAxKwGAzVpQ9KDSaKu/HVhXlPJCOO/i9Ut7O+SHlA3vSGgRTnI7GgIZgCn6FAKaeQO5ExxUzgxTsB+N+WonHlDGPxU2Jr8VAbj2jcMAOJhWYUPLxFT4JRGZeycK6SF+WOukQGFDRFL7trgDzhSDzZaj/FIuUHiv5Ih+ZkxoDTWZb3gXopNgAc/5Hhtx8YyKLZJ5G1V8+FV0/OLdFlgqRyIdrmoSDxu/EG3F/0NBpY856YhKc6zFMe/feL4RQnFFWU= \ No newline at end of file diff --git a/README.md b/README.md index 9632efffc..78ece9800 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Play REST API +[![Build Status](https://travis-ci.org/playframework/play-scala-rest-api-example.svg?branch=2.6.x)](https://travis-ci.org/playframework/play-scala-rest-api-example) + This is the example project for [Making a REST API in Play](http://developer.lightbend.com/guides/play-rest-api/index.html). ## Appendix @@ -10,35 +12,35 @@ You need to download and install sbt for this application to run. Once you have sbt installed, the following at the command prompt will start up Play in development mode: -``` +```bash sbt run ``` -Play will start up on the HTTP port at http://localhost:9000/. You don't need to deploy or reload anything -- changing any source code while the server is running will automatically recompile and hot-reload the application on the next HTTP request. +Play will start up on the HTTP port at . You don't need to deploy or reload anything -- changing any source code while the server is running will automatically recompile and hot-reload the application on the next HTTP request. ### Usage If you call the same URL from the command line, you’ll see JSON. Using httpie, we can execute the command: -``` +```bash http --verbose http://localhost:9000/v1/posts ``` and get back: -``` +```routes GET /v1/posts HTTP/1.1 ``` Likewise, you can also send a POST directly as JSON: -``` +```bash http --verbose POST http://localhost:9000/v1/posts title="hello" body="world" ``` and get: -``` +```routes POST /v1/posts HTTP/1.1 ``` @@ -48,7 +50,7 @@ The best way to see what Play can do is to run a load test. We've included Gatl Start Play in production mode, by [staging the application](https://www.playframework.com/documentation/2.5.x/Deploying) and running the play script:s -``` +```bash sbt stage cd target/universal/stage bin/play-rest-api-example -Dplay.crypto.secret=testing @@ -56,7 +58,7 @@ bin/play-rest-api-example -Dplay.crypto.secret=testing Then you'll start the Gatling load test up (it's already integrated into the project): -``` +```bash sbt gatling:test ``` @@ -64,7 +66,7 @@ For best results, start the gatling load test up on another machine so you do no Once the test completes, you'll see an HTML file containing the load test chart: -``` +```bash ./rest-api/target/gatling/gatlingspec-1472579540405/index.html ``` diff --git a/app/RequestHandler.scala b/app/RequestHandler.scala index 7a4fdd1f7..8db497cc4 100644 --- a/app/RequestHandler.scala +++ b/app/RequestHandler.scala @@ -2,7 +2,7 @@ import javax.inject.Inject import play.api.http._ import play.api.mvc._ -import play.api.mvc.request.{RequestFactory, RequestTarget} +import play.api.mvc.request.RequestTarget import play.api.routing.Router /** diff --git a/app/v1/post/PostActionBuilder.scala b/app/v1/post/PostActionBuilder.scala index 4de22802f..f981e3149 100644 --- a/app/v1/post/PostActionBuilder.scala +++ b/app/v1/post/PostActionBuilder.scala @@ -9,7 +9,6 @@ import play.api.i18n.{Langs, MessagesApi} import play.api.mvc._ import scala.concurrent.{ExecutionContext, Future} -import scala.language.implicitConversions /** * A wrapped request for post resources. diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..e5d97395f --- /dev/null +++ b/build.gradle @@ -0,0 +1,65 @@ +plugins { + id 'play' + id 'idea' + id "com.github.lkishalmi.gatling" version "0.7.1" +} + +def playVersion = '2.6.9' +def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.11") +def gatlingVersion = "2.3.0" + +model { + components { + play { + platform play: playVersion, scala: scalaVersion, java: '1.8' + injectedRoutesGenerator = true + + sources { + twirlTemplates { + defaultImports = TwirlImports.SCALA + } + } + } + } +} + +project.sourceSets { + gatling { + scala.srcDirs = ["gatling"] + } +} + +gatling { + sourceRoot = "gatling" + simulationsDir = "gatling" + toolVersion = gatlingVersion +} + +dependencies { + play "com.typesafe.play:play-guice_$scalaVersion:$playVersion" + play "com.typesafe.play:play-logback_$scalaVersion:$playVersion" + play "com.typesafe.play:filters-helpers_$scalaVersion:$playVersion" + + play "org.joda:joda-convert:1.9.2" + play "net.logstash.logback:logstash-logback-encoder:4.11" + + play "com.netaporter:scala-uri_$scalaVersion:0.4.16" + play "net.codingwell:scala-guice_$scalaVersion:4.1.1" + + playTest "org.scalatestplus.play:scalatestplus-play_$scalaVersion:3.1.2" + playTest "io.gatling.highcharts:gatling-charts-highcharts:$gatlingVersion" + playTest "io.gatling:gatling-test-framework:$gatlingVersion" +} + +repositories { + jcenter() + maven { + name "lightbend-maven-releases" + url "https://repo.lightbend.com/lightbend/maven-release" + } + ivy { + name "lightbend-ivy-release" + url "https://repo.lightbend.com/lightbend/ivy-releases" + layout "ivy" + } +} diff --git a/build.sbt b/build.sbt index bccb2142d..d3bfda0b4 100644 --- a/build.sbt +++ b/build.sbt @@ -2,8 +2,7 @@ import sbt.Keys._ lazy val GatlingTest = config("gatling") extend Test -// This must be set to 2.11.11 because Gatling does not run on 2.12.2 -scalaVersion in ThisBuild := "2.11.11" +scalaVersion in ThisBuild := "2.12.4" libraryDependencies += guice libraryDependencies += "org.joda" % "joda-convert" % "1.9.2" @@ -13,8 +12,8 @@ libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.16" libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.1" libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test -libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % Test -libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.2.2" % Test +libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.3.0" % Test +libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.3.0" % Test // The Play project itself lazy val root = (project in file(".")) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..01b8bf6b1f99cad9213fc495b33ad5bbab8efd20 GIT binary patch literal 54329 zcmagFV|ZrKvM!pAZQHhO+qP}9lTNj?q^^Y^VFp)SH8qbSJ)2BQ2giqeFT zAwqu@)c?v~^Z#E_K}1nTQbJ9gQ9<%vVRAxVj)8FwL5_iTdUB>&m3fhE=kRWl;g`&m z!W5kh{WsV%fO*%je&j+Lv4xxK~zsEYQls$Q-p&dwID|A)!7uWtJF-=Tm1{V@#x*+kUI$=%KUuf2ka zjiZ{oiL1MXE2EjciJM!jrjFNwCh`~hL>iemrqwqnX?T*MX;U>>8yRcZb{Oy+VKZos zLiFKYPw=LcaaQt8tj=eoo3-@bG_342HQ%?jpgAE?KCLEHC+DmjxAfJ%Og^$dpC8Xw zAcp-)tfJm}BPNq_+6m4gBgBm3+CvmL>4|$2N$^Bz7W(}fz1?U-u;nE`+9`KCLuqg} zwNstNM!J4Uw|78&Y9~9>MLf56to!@qGkJw5Thx%zkzj%Ek9Nn1QA@8NBXbwyWC>9H z#EPwjMNYPigE>*Ofz)HfTF&%PFj$U6mCe-AFw$U%-L?~-+nSXHHKkdgC5KJRTF}`G zE_HNdrE}S0zf4j{r_f-V2imSqW?}3w-4=f@o@-q+cZgaAbZ((hn))@|eWWhcT2pLpTpL!;_5*vM=sRL8 zqU##{U#lJKuyqW^X$ETU5ETeEVzhU|1m1750#f}38_5N9)B_2|v@1hUu=Kt7-@dhA zq_`OMgW01n`%1dB*}C)qxC8q;?zPeF_r;>}%JYmlER_1CUbKa07+=TV45~symC*g8 zW-8(gag#cAOuM0B1xG8eTp5HGVLE}+gYTmK=`XVVV*U!>H`~j4+ROIQ+NkN$LY>h4 zqpwdeE_@AX@PL};e5vTn`Ro(EjHVf$;^oiA%@IBQq>R7_D>m2D4OwwEepkg}R_k*M zM-o;+P27087eb+%*+6vWFCo9UEGw>t&WI17Pe7QVuoAoGHdJ(TEQNlJOqnjZ8adCb zI`}op16D@v7UOEo%8E-~m?c8FL1utPYlg@m$q@q7%mQ4?OK1h%ODjTjFvqd!C z-PI?8qX8{a@6d&Lb_X+hKxCImb*3GFemm?W_du5_&EqRq!+H?5#xiX#w$eLti-?E$;Dhu`{R(o>LzM4CjO>ICf z&DMfES#FW7npnbcuqREgjPQM#gs6h>`av_oEWwOJZ2i2|D|0~pYd#WazE2Bbsa}X@ zu;(9fi~%!VcjK6)?_wMAW-YXJAR{QHxrD5g(ou9mR6LPSA4BRG1QSZT6A?kelP_g- zH(JQjLc!`H4N=oLw=f3{+WmPA*s8QEeEUf6Vg}@!xwnsnR0bl~^2GSa5vb!Yl&4!> zWb|KQUsC$lT=3A|7vM9+d;mq=@L%uWKwXiO9}a~gP4s_4Yohc!fKEgV7WbVo>2ITbE*i`a|V!^p@~^<={#?Gz57 zyPWeM2@p>D*FW#W5Q`1`#5NW62XduP1XNO(bhg&cX`-LYZa|m-**bu|>}S;3)eP8_ zpNTnTfm8 ze+7wDH3KJ95p)5tlwk`S7mbD`SqHnYD*6`;gpp8VdHDz%RR_~I_Ar>5)vE-Pgu7^Y z|9Px+>pi3!DV%E%4N;ii0U3VBd2ZJNUY1YC^-e+{DYq+l@cGtmu(H#Oh%ibUBOd?C z{y5jW3v=0eV0r@qMLgv1JjZC|cZ9l9Q)k1lLgm))UR@#FrJd>w^`+iy$c9F@ic-|q zVHe@S2UAnc5VY_U4253QJxm&Ip!XKP8WNcnx9^cQ;KH6PlW8%pSihSH2(@{2m_o+m zr((MvBja2ctg0d0&U5XTD;5?d?h%JcRJp{_1BQW1xu&BrA3(a4Fh9hon-ly$pyeHq zG&;6q?m%NJ36K1Sq_=fdP(4f{Hop;_G_(i?sPzvB zDM}>*(uOsY0I1j^{$yn3#U(;B*g4cy$-1DTOkh3P!LQ;lJlP%jY8}Nya=h8$XD~%Y zbV&HJ%eCD9nui-0cw!+n`V~p6VCRqh5fRX z8`GbdZ@73r7~myQLBW%db;+BI?c-a>Y)m-FW~M=1^|<21_Sh9RT3iGbO{o-hpN%d6 z7%++#WekoBOP^d0$$|5npPe>u3PLvX_gjH2x(?{&z{jJ2tAOWTznPxv-pAv<*V7r$ z6&glt>7CAClWz6FEi3bToz-soY^{ScrjwVPV51=>n->c(NJngMj6TyHty`bfkF1hc zkJS%A@cL~QV0-aK4>Id!9dh7>0IV;1J9(myDO+gv76L3NLMUm9XyPauvNu$S<)-|F zZS}(kK_WnB)Cl`U?jsdYfAV4nrgzIF@+%1U8$poW&h^c6>kCx3;||fS1_7JvQT~CV zQ8Js+!p)3oW>Df(-}uqC`Tcd%E7GdJ0p}kYj5j8NKMp(KUs9u7?jQ94C)}0rba($~ zqyBx$(1ae^HEDG`Zc@-rXk1cqc7v0wibOR4qpgRDt#>-*8N3P;uKV0CgJE2SP>#8h z=+;i_CGlv+B^+$5a}SicVaSeaNn29K`C&=}`=#Nj&WJP9Xhz4mVa<+yP6hkrq1vo= z1rX4qg8dc4pmEvq%NAkpMK>mf2g?tg_1k2%v}<3`$6~Wlq@ItJ*PhHPoEh1Yi>v57 z4k0JMO)*=S`tKvR5gb-(VTEo>5Y>DZJZzgR+j6{Y`kd|jCVrg!>2hVjz({kZR z`dLlKhoqT!aI8=S+fVp(5*Dn6RrbpyO~0+?fy;bm$0jmTN|t5i6rxqr4=O}dY+ROd zo9Et|x}!u*xi~>-y>!M^+f&jc;IAsGiM_^}+4|pHRn{LThFFpD{bZ|TA*wcGm}XV^ zr*C6~@^5X-*R%FrHIgo-hJTBcyQ|3QEj+cSqp#>&t`ZzB?cXM6S(lRQw$I2?m5=wd z78ki`R?%;o%VUhXH?Z#(uwAn9$m`npJ=cA+lHGk@T7qq_M6Zoy1Lm9E0UUysN)I_x zW__OAqvku^>`J&CB=ie@yNWsaFmem}#L3T(x?a`oZ+$;3O-icj2(5z72Hnj=9Z0w% z<2#q-R=>hig*(t0^v)eGq2DHC%GymE-_j1WwBVGoU=GORGjtaqr0BNigOCqyt;O(S zKG+DoBsZU~okF<7ahjS}bzwXxbAxFfQAk&O@>LsZMsZ`?N?|CDWM(vOm%B3CBPC3o z%2t@%H$fwur}SSnckUm0-k)mOtht`?nwsDz=2#v=RBPGg39i#%odKq{K^;bTD!6A9 zskz$}t)sU^=a#jLZP@I=bPo?f-L}wpMs{Tc!m7-bi!Ldqj3EA~V;4(dltJmTXqH0r z%HAWKGutEc9vOo3P6Q;JdC^YTnby->VZ6&X8f{obffZ??1(cm&L2h7q)*w**+sE6dG*;(H|_Q!WxU{g)CeoT z(KY&bv!Usc|m+Fqfmk;h&RNF|LWuNZ!+DdX*L=s-=_iH=@i` z?Z+Okq^cFO4}_n|G*!)Wl_i%qiMBaH8(WuXtgI7EO=M>=i_+;MDjf3aY~6S9w0K zUuDO7O5Ta6+k40~xh~)D{=L&?Y0?c$s9cw*Ufe18)zzk%#ZY>Tr^|e%8KPb0ht`b( zuP@8#Ox@nQIqz9}AbW0RzE`Cf>39bOWz5N3qzS}ocxI=o$W|(nD~@EhW13Rj5nAp; zu2obEJa=kGC*#3=MkdkWy_%RKcN=?g$7!AZ8vBYKr$ePY(8aIQ&yRPlQ=mudv#q$q z4%WzAx=B{i)UdLFx4os?rZp6poShD7Vc&mSD@RdBJ=_m^&OlkEE1DFU@csgKcBifJ zz4N7+XEJhYzzO=86 z#%eBQZ$Nsf2+X0XPHUNmg#(sNt^NW1Y0|M(${e<0kW6f2q5M!2YE|hSEQ*X-%qo(V zHaFwyGZ0on=I{=fhe<=zo{=Og-_(to3?cvL4m6PymtNsdDINsBh8m>a%!5o3s(en) z=1I z6O+YNertC|OFNqd6P=$gMyvmfa`w~p9*gKDESFqNBy(~Zw3TFDYh}$iudn)9HxPBi zdokK@o~nu?%imcURr5Y~?6oo_JBe}t|pU5qjai|#JDyG=i^V~7+a{dEnO<(y>ahND#_X_fcEBNiZ)uc&%1HVtx8Ts z*H_Btvx^IhkfOB#{szN*n6;y05A>3eARDXslaE>tnLa>+`V&cgho?ED+&vv5KJszf zG4@G;7i;4_bVvZ>!mli3j7~tPgybF5|J6=Lt`u$D%X0l}#iY9nOXH@(%FFJLtzb%p zzHfABnSs;v-9(&nzbZytLiqqDIWzn>JQDk#JULcE5CyPq_m#4QV!}3421haQ+LcfO*>r;rg6K|r#5Sh|y@h1ao%Cl)t*u`4 zMTP!deC?aL7uTxm5^nUv#q2vS-5QbBKP|drbDXS%erB>fYM84Kpk^au99-BQBZR z7CDynflrIAi&ahza+kUryju5LR_}-Z27g)jqOc(!Lx9y)e z{cYc&_r947s9pteaa4}dc|!$$N9+M38sUr7h(%@Ehq`4HJtTpA>B8CLNO__@%(F5d z`SmX5jbux6i#qc}xOhumzbAELh*Mfr2SW99=WNOZRZgoCU4A2|4i|ZVFQt6qEhH#B zK_9G;&h*LO6tB`5dXRSBF0hq0tk{2q__aCKXYkP#9n^)@cq}`&Lo)1KM{W+>5mSed zKp~=}$p7>~nK@va`vN{mYzWN1(tE=u2BZhga5(VtPKk(*TvE&zmn5vSbjo zZLVobTl%;t@6;4SsZ>5+U-XEGUZGG;+~|V(pE&qqrp_f~{_1h@5ZrNETqe{bt9ioZ z#Qn~gWCH!t#Ha^n&fT2?{`}D@s4?9kXj;E;lWV9Zw8_4yM0Qg-6YSsKgvQ*fF{#Pq z{=(nyV>#*`RloBVCs;Lp*R1PBIQOY=EK4CQa*BD0MsYcg=opP?8;xYQDSAJBeJpw5 zPBc_Ft9?;<0?pBhCmOtWU*pN*;CkjJ_}qVic`}V@$TwFi15!mF1*m2wVX+>5p%(+R zQ~JUW*zWkalde{90@2v+oVlkxOZFihE&ZJ){c?hX3L2@R7jk*xjYtHi=}qb+4B(XJ z$gYcNudR~4Kz_WRq8eS((>ALWCO)&R-MXE+YxDn9V#X{_H@j616<|P(8h(7z?q*r+ zmpqR#7+g$cT@e&(%_|ipI&A%9+47%30TLY(yuf&*knx1wNx|%*H^;YB%ftt%5>QM= z^i;*6_KTSRzQm%qz*>cK&EISvF^ovbS4|R%)zKhTH_2K>jP3mBGn5{95&G9^a#4|K zv+!>fIsR8z{^x4)FIr*cYT@Q4Z{y}};rLHL+atCgHbfX*;+k&37DIgENn&=k(*lKD zG;uL-KAdLn*JQ?@r6Q!0V$xXP=J2i~;_+i3|F;_En;oAMG|I-RX#FwnmU&G}w`7R{ z788CrR-g1DW4h_`&$Z`ctN~{A)Hv_-Bl!%+pfif8wN32rMD zJDs$eVWBYQx1&2sCdB0!vU5~uf)=vy*{}t{2VBpcz<+~h0wb7F3?V^44*&83Z2#F` z32!rd4>uc63rQP$3lTH3zb-47IGR}f)8kZ4JvX#toIpXH`L%NnPDE~$QI1)0)|HS4 zVcITo$$oWWwCN@E-5h>N?Hua!N9CYb6f8vTFd>h3q5Jg-lCI6y%vu{Z_Uf z$MU{{^o~;nD_@m2|E{J)q;|BK7rx%`m``+OqZAqAVj-Dy+pD4-S3xK?($>wn5bi90CFAQ+ACd;&m6DQB8_o zjAq^=eUYc1o{#+p+ zn;K<)Pn*4u742P!;H^E3^Qu%2dM{2slouc$AN_3V^M7H_KY3H)#n7qd5_p~Za7zAj|s9{l)RdbV9e||_67`#Tu*c<8!I=zb@ z(MSvQ9;Wrkq6d)!9afh+G`!f$Ip!F<4ADdc*OY-y7BZMsau%y?EN6*hW4mOF%Q~bw z2==Z3^~?q<1GTeS>xGN-?CHZ7a#M4kDL zQxQr~1ZMzCSKFK5+32C%+C1kE#(2L=15AR!er7GKbp?Xd1qkkGipx5Q~FI-6zt< z*PTpeVI)Ngnnyaz5noIIgNZtb4bQdKG{Bs~&tf)?nM$a;7>r36djllw%hQxeCXeW^ z(i6@TEIuxD<2ulwLTt|&gZP%Ei+l!(%p5Yij6U(H#HMkqM8U$@OKB|5@vUiuY^d6X zW}fP3;Kps6051OEO(|JzmVU6SX(8q>*yf*x5QoxDK={PH^F?!VCzES_Qs>()_y|jg6LJlJWp;L zKM*g5DK7>W_*uv}{0WUB0>MHZ#oJZmO!b3MjEc}VhsLD~;E-qNNd?x7Q6~v zR=0$u>Zc2Xr}>x_5$-s#l!oz6I>W?lw;m9Ae{Tf9eMX;TI-Wf_mZ6sVrMnY#F}cDd z%CV*}fDsXUF7Vbw>PuDaGhu631+3|{xp<@Kl|%WxU+vuLlcrklMC!Aq+7n~I3cmQ! z`e3cA!XUEGdEPSu``&lZEKD1IKO(-VGvcnSc153m(i!8ohi`)N2n>U_BemYJ`uY>8B*Epj!oXRLV}XK}>D*^DHQ7?NY*&LJ9VSo`Ogi9J zGa;clWI8vIQqkngv2>xKd91K>?0`Sw;E&TMg&6dcd20|FcTsnUT7Yn{oI5V4@Ow~m zz#k~8TM!A9L7T!|colrC0P2WKZW7PNj_X4MfESbt<-soq*0LzShZ}fyUx!(xIIDwx zRHt^_GAWe0-Vm~bDZ(}XG%E+`XhKpPlMBo*5q_z$BGxYef8O!ToS8aT8pmjbPq)nV z%x*PF5ZuSHRJqJ!`5<4xC*xb2vC?7u1iljB_*iUGl6+yPyjn?F?GOF2_KW&gOkJ?w z3e^qc-te;zez`H$rsUCE0<@7PKGW?7sT1SPYWId|FJ8H`uEdNu4YJjre`8F*D}6Wh z|FQ`xf7yiphHIAkU&OYCn}w^ilY@o4larl?^M7&8YI;hzBIsX|i3UrLsx{QDKwCX< zy;a>yjfJ6!sz`NcVi+a!Fqk^VE^{6G53L?@Tif|j!3QZ0fk9QeUq8CWI;OmO-Hs+F zuZ4sHLA3{}LR2Qlyo+{d@?;`tpp6YB^BMoJt?&MHFY!JQwoa0nTSD+#Ku^4b{5SZVFwU9<~APYbaLO zu~Z)nS#dxI-5lmS-Bnw!(u15by(80LlC@|ynj{TzW)XcspC*}z0~8VRZq>#Z49G`I zgl|C#H&=}n-ajxfo{=pxPV(L*7g}gHET9b*s=cGV7VFa<;Htgjk>KyW@S!|z`lR1( zGSYkEl&@-bZ*d2WQ~hw3NpP=YNHF^XC{TMG$Gn+{b6pZn+5=<()>C!N^jncl0w6BJ zdHdnmSEGK5BlMeZD!v4t5m7ct7{k~$1Ie3GLFoHjAH*b?++s<|=yTF+^I&jT#zuMx z)MLhU+;LFk8bse|_{j+d*a=&cm2}M?*arjBPnfPgLwv)86D$6L zLJ0wPul7IenMvVAK$z^q5<^!)7aI|<&GGEbOr=E;UmGOIa}yO~EIr5xWU_(ol$&fa zR5E(2vB?S3EvJglTXdU#@qfDbCYs#82Yo^aZN6`{Ex#M)easBTe_J8utXu(fY1j|R z9o(sQbj$bKU{IjyhosYahY{63>}$9_+hWxB3j}VQkJ@2$D@vpeRSldU?&7I;qd2MF zSYmJ>zA(@N_iK}m*AMPIJG#Y&1KR)6`LJ83qg~`Do3v^B0>fU&wUx(qefuTgzFED{sJ65!iw{F2}1fQ3= ziFIP{kezQxmlx-!yo+sC4PEtG#K=5VM9YIN0z9~c4XTX?*4e@m;hFM!zVo>A`#566 z>f&3g94lJ{r)QJ5m7Xe3SLau_lOpL;A($wsjHR`;xTXgIiZ#o&vt~ zGR6KdU$FFbLfZCC3AEu$b`tj!9XgOGLSV=QPIYW zjI!hSP#?8pn0@ezuenOzoka8!8~jXTbiJ6+ZuItsWW03uzASFyn*zV2kIgPFR$Yzm zE<$cZlF>R8?Nr2_i?KiripBc+TGgJvG@vRTY2o?(_Di}D30!k&CT`>+7ry2!!iC*X z<@=U0_C#16=PN7bB39w+zPwDOHX}h20Ap);dx}kjXX0-QkRk=cr};GYsjSvyLZa-t zzHONWddi*)RDUH@RTAsGB_#&O+QJaaL+H<<9LLSE+nB@eGF1fALwjVOl8X_sdOYme z0lk!X=S(@25=TZHR7LlPp}fY~yNeThMIjD}pd9+q=j<_inh0$>mIzWVY+Z9p<{D^#0Xk+b_@eNSiR8;KzSZ#7lUsk~NGMcB8C2c=m2l5paHPq`q{S(kdA7Z1a zyfk2Y;w?^t`?@yC5Pz9&pzo}Hc#}mLgDmhKV|PJ3lKOY(Km@Fi2AV~CuET*YfUi}u zfInZnqDX(<#vaS<^fszuR=l)AbqG{}9{rnyx?PbZz3Pyu!eSJK`uwkJU!ORQXy4x83r!PNgOyD33}}L=>xX_93l6njNTuqL8J{l%*3FVn3MG4&Fv*`lBXZ z?=;kn6HTT^#SrPX-N)4EZiIZI!0ByXTWy;;J-Tht{jq1mjh`DSy7yGjHxIaY%*sTx zuy9#9CqE#qi>1misx=KRWm=qx4rk|}vd+LMY3M`ow8)}m$3Ggv&)Ri*ON+}<^P%T5 z_7JPVPfdM=Pv-oH<tecoE}(0O7|YZc*d8`Uv_M*3Rzv7$yZnJE6N_W=AQ3_BgU_TjA_T?a)U1csCmJ&YqMp-lJe`y6>N zt++Bi;ZMOD%%1c&-Q;bKsYg!SmS^#J@8UFY|G3!rtyaTFb!5@e(@l?1t(87ln8rG? z--$1)YC~vWnXiW3GXm`FNSyzu!m$qT=Eldf$sMl#PEfGmzQs^oUd=GIQfj(X=}dw+ zT*oa0*oS%@cLgvB&PKIQ=Ok?>x#c#dC#sQifgMwtAG^l3D9nIg(Zqi;D%807TtUUCL3_;kjyte#cAg?S%e4S2W>9^A(uy8Ss0Tc++ZTjJw1 z&Em2g!3lo@LlDyri(P^I8BPpn$RE7n*q9Q-c^>rfOMM6Pd5671I=ZBjAvpj8oIi$! zl0exNl(>NIiQpX~FRS9UgK|0l#s@#)p4?^?XAz}Gjb1?4Qe4?j&cL$C8u}n)?A@YC zfmbSM`Hl5pQFwv$CQBF=_$Sq zxsV?BHI5bGZTk?B6B&KLdIN-40S426X3j_|ceLla*M3}3gx3(_7MVY1++4mzhH#7# zD>2gTHy*%i$~}mqc#gK83288SKp@y3wz1L_e8fF$Rb}ex+`(h)j}%~Ld^3DUZkgez zOUNy^%>>HHE|-y$V@B}-M|_{h!vXpk01xaD%{l{oQ|~+^>rR*rv9iQen5t?{BHg|% zR`;S|KtUb!X<22RTBA4AAUM6#M?=w5VY-hEV)b`!y1^mPNEoy2K)a>OyA?Q~Q*&(O zRzQI~y_W=IPi?-OJX*&&8dvY0zWM2%yXdFI!D-n@6FsG)pEYdJbuA`g4yy;qrgR?G z8Mj7gv1oiWq)+_$GqqQ$(ZM@#|0j7})=#$S&hZwdoijFI4aCFLVI3tMH5fLreZ;KD zqA`)0l~D2tuIBYOy+LGw&hJ5OyE+@cnZ0L5+;yo2pIMdt@4$r^5Y!x7nHs{@>|W(MzJjATyWGNwZ^4j+EPU0RpAl-oTM@u{lx*i0^yyWPfHt6QwPvYpk9xFMWfBFt!+Gu6TlAmr zeQ#PX71vzN*_-xh&__N`IXv6`>CgV#eA_%e@7wjgkj8jlKzO~Ic6g$cT`^W{R{606 zCDP~+NVZ6DMO$jhL~#+!g*$T!XW63#(ngDn#Qwy71yj^gazS{e;3jGRM0HedGD@pt z?(ln3pCUA(ekqAvvnKy0G@?-|-dh=eS%4Civ&c}s%wF@0K5Bltaq^2Os1n6Z3%?-Q zAlC4goQ&vK6TpgtzkHVt*1!tBYt-`|5HLV1V7*#45Vb+GACuU+QB&hZ=N_flPy0TY zR^HIrdskB#<$aU;HY(K{a3(OQa$0<9qH(oa)lg@Uf>M5g2W0U5 zk!JSlhrw8quBx9A>RJ6}=;W&wt@2E$7J=9SVHsdC?K(L(KACb#z)@C$xXD8^!7|uv zZh$6fkq)aoD}^79VqdJ!Nz-8$IrU(_-&^cHBI;4 z^$B+1aPe|LG)C55LjP;jab{dTf$0~xbXS9!!QdcmDYLbL^jvxu2y*qnx2%jbL%rB z{aP85qBJe#(&O~Prk%IJARcdEypZ)vah%ZZ%;Zk{eW(U)Bx7VlzgOi8)x z`rh4l`@l_Ada7z&yUK>ZF;i6YLGwI*Sg#Fk#Qr0Jg&VLax(nNN$u-XJ5=MsP3|(lEdIOJ7|(x3iY;ea)5#BW*mDV%^=8qOeYO&gIdJVuLLN3cFaN=xZtFB=b zH{l)PZl_j^u+qx@89}gAQW7ofb+k)QwX=aegihossZq*+@PlCpb$rpp>Cbk9UJO<~ zDjlXQ_Ig#W0zdD3&*ei(FwlN#3b%FSR%&M^ywF@Fr>d~do@-kIS$e%wkIVfJ|Ohh=zc zF&Rnic^|>@R%v?@jO}a9;nY3Qrg_!xC=ZWUcYiA5R+|2nsM*$+c$TOs6pm!}Z}dfM zGeBhMGWw3$6KZXav^>YNA=r6Es>p<6HRYcZY)z{>yasbC81A*G-le8~QoV;rtKnkx z;+os8BvEe?0A6W*a#dOudsv3aWs?d% z0oNngyVMjavLjtjiG`!007#?62ClTqqU$@kIY`=x^$2e>iqIy1>o|@Tw@)P)B8_1$r#6>DB_5 zmaOaoE~^9TolgDgooKFuEFB#klSF%9-~d2~_|kQ0Y{Ek=HH5yq9s zDq#1S551c`kSiWPZbweN^A4kWiP#Qg6er1}HcKv{fxb1*BULboD0fwfaNM_<55>qM zETZ8TJDO4V)=aPp_eQjX%||Ud<>wkIzvDlpNjqW>I}W!-j7M^TNe5JIFh#-}zAV!$ICOju8Kx)N z0vLtzDdy*rQN!7r>Xz7rLw8J-(GzQlYYVH$WK#F`i_i^qVlzTNAh>gBWKV@XC$T-` z3|kj#iCquDhiO7NKum07i|<-NuVsX}Q}mIP$jBJDMfUiaWR3c|F_kWBMw0_Sr|6h4 zk`_r5=0&rCR^*tOy$A8K;@|NqwncjZ>Y-75vlpxq%Cl3EgH`}^^~=u zoll6xxY@a>0f%Ddpi;=cY}fyG!K2N-dEyXXmUP5u){4VnyS^T4?pjN@Ot4zjL(Puw z_U#wMH2Z#8Pts{olG5Dy0tZj;N@;fHheu>YKYQU=4Bk|wcD9MbA`3O4bj$hNRHwzb zSLcG0SLV%zywdbuwl(^E_!@&)TdXge4O{MRWk2RKOt@!8E{$BU-AH(@4{gxs=YAz9LIob|Hzto0}9cWoz6Tp2x0&xi#$ zHh$dwO&UCR1Ob2w00-2eG7d4=cN(Y>0R#$q8?||q@iTi+7-w-xR%uMr&StFIthC<# zvK(aPduwuNB}oJUV8+Zl)%cnfsHI%4`;x6XW^UF^e4s3Z@S<&EV8?56Wya;HNs0E> z`$0dgRdiUz9RO9Au3RmYq>K#G=X%*_dUbSJHP`lSfBaN8t-~@F>)BL1RT*9I851A3 z<-+Gb#_QRX>~av#Ni<#zLswtu-c6{jGHR>wflhKLzC4P@b%8&~u)fosoNjk4r#GvC zlU#UU9&0Hv;d%g72Wq?Ym<&&vtA3AB##L}=ZjiTR4hh7J)e>ei} zt*u+>h%MwN`%3}b4wYpV=QwbY!jwfIj#{me)TDOG`?tI!%l=AwL2G@9I~}?_dA5g6 zCKgK(;6Q0&P&K21Tx~k=o6jwV{dI_G+Ba*Zts|Tl6q1zeC?iYJTb{hel*x>^wb|2RkHkU$!+S4OU4ZOKPZjV>9OVsqNnv5jK8TRAE$A&^yRwK zj-MJ3Pl?)KA~fq#*K~W0l4$0=8GRx^9+?w z!QT8*-)w|S^B0)ZeY5gZPI2G(QtQf?DjuK(s^$rMA!C%P22vynZY4SuOE=wX2f8$R z)A}mzJi4WJnZ`!bHG1=$lwaxm!GOnRbR15F$nRC-M*H<*VfF|pQw(;tbSfp({>9^5 zw_M1-SJ9eGF~m(0dvp*P8uaA0Yw+EkP-SWqu zqal$hK8SmM7#Mrs0@OD+%_J%H*bMyZiWAZdsIBj#lkZ!l2c&IpLu(5^T0Ge5PHzR} zn;TXs$+IQ_&;O~u=Jz+XE0wbOy`=6>m9JVG} zJ~Kp1e5m?K3x@@>!D)piw^eMIHjD4RebtR`|IlckplP1;r21wTi8v((KqNqn%2CB< zifaQc&T}*M&0i|LW^LgdjIaX|o~I$`owHolRqeH_CFrqCUCleN130&vH}dK|^kC>) z-r2P~mApHotL4dRX$25lIcRh_*kJaxi^%ZN5-GAAMOxfB!6flLPY-p&QzL9TE%ho( zRwftE3sy5<*^)qYzKkL|rE>n@hyr;xPqncY6QJ8125!MWr`UCWuC~A#G1AqF1@V$kv>@NBvN&2ygy*{QvxolkRRb%Ui zsmKROR%{*g*WjUUod@@cS^4eF^}yQ1>;WlGwOli z+Y$(8I`0(^d|w>{eaf!_BBM;NpCoeem2>J}82*!em=}}ymoXk>QEfJ>G(3LNA2-46 z5PGvjr)Xh9>aSe>vEzM*>xp{tJyZox1ZRl}QjcvX2TEgNc^(_-hir@Es>NySoa1g^ zFow_twnHdx(j?Q_3q51t3XI7YlJ4_q&(0#)&a+RUy{IcBq?)eaWo*=H2UUVIqtp&lW9JTJiP&u zw8+4vo~_IJXZIJb_U^&=GI1nSD%e;P!c{kZALNCm5c%%oF+I3DrA63_@4)(v4(t~JiddILp7jmoy+>cD~ivwoctFfEL zP*#2Rx?_&bCpX26MBgp^4G>@h`Hxc(lnqyj!*t>9sOBcXN(hTwEDpn^X{x!!gPX?1 z*uM$}cYRwHXuf+gYTB}gDTcw{TXSOUU$S?8BeP&sc!Lc{{pEv}x#ELX>6*ipI1#>8 zKes$bHjiJ1OygZge_ak^Hz#k;=od1wZ=o71ba7oClBMq>Uk6hVq|ePPt)@FM5bW$I z;d2Or@wBjbTyZj|;+iHp%Bo!Vy(X3YM-}lasMItEV_QrP-Kk_J4C>)L&I3Xxj=E?| zsAF(IfVQ4w+dRRnJ>)}o^3_012YYgFWE)5TT=l2657*L8_u1KC>Y-R{7w^S&A^X^U}h20jpS zQsdeaA#WIE*<8KG*oXc~$izYilTc#z{5xhpXmdT-YUnGh9v4c#lrHG6X82F2-t35} zB`jo$HjKe~E*W$=g|j&P>70_cI`GnOQ;Jp*JK#CT zuEGCn{8A@bC)~0%wsEv?O^hSZF*iqjO~_h|>xv>PO+?525Nw2472(yqS>(#R)D7O( zg)Zrj9n9$}=~b00=Wjf?E418qP-@8%MQ%PBiCTX=$B)e5cHFDu$LnOeJ~NC;xmOk# z>z&TbsK>Qzk)!88lNI8fOE2$Uxso^j*1fz>6Ot49y@=po)j4hbTIcVR`ePHpuJSfp zxaD^Dn3X}Na3@<_Pc>a;-|^Pon(>|ytG_+U^8j_JxP=_d>L$Hj?|0lz>_qQ#a|$+( z(x=Lipuc8p4^}1EQhI|TubffZvB~lu$zz9ao%T?%ZLyV5S9}cLeT?c} z>yCN9<04NRi~1oR)CiBakoNhY9BPnv)kw%*iv8vdr&&VgLGIs(-FbJ?d_gfbL2={- zBk4lkdPk~7+jIxd4{M(-W1AC_WcN&Oza@jZoj zaE*9Y;g83#m(OhA!w~LNfUJNUuRz*H-=$s*z+q+;snKPRm9EptejugC-@7-a-}Tz0 z@KHra#Y@OXK+KsaSN9WiGf?&jlZ!V7L||%KHP;SLksMFfjkeIMf<1e~t?!G3{n)H8 zQAlFY#QwfKuj;l@<$YDATAk;%PtD%B(0<|8>rXU< zJ66rkAVW_~Dj!7JGdGGi4NFuE?7ZafdMxIh65Sz7yQoA7fBZCE@WwysB=+`kT^LFX zz8#FlSA5)6FG9(qL3~A24mpzL@@2D#>0J7mMS1T*9UJ zvOq!!a(%IYY69+h45CE?(&v9H4FCr>gK0>mK~F}5RdOuH2{4|}k@5XpsX7+LZo^Qa4sH5`eUj>iffoBVm+ zz4Mtf`h?NW$*q1yr|}E&eNl)J``SZvTf6Qr*&S%tVv_OBpbjnA0&Vz#(;QmGiq-k! zgS0br4I&+^2mgA15*~Cd00cXLYOLA#Ep}_)eED>m+K@JTPr_|lSN}(OzFXQSBc6fM z@f-%2;1@BzhZa*LFV z-LrLmkmB%<<&jEURBEW>soaZ*rSIJNwaV%-RSaCZi4X)qYy^PxZ=oL?6N-5OGOMD2 z;q_JK?zkwQ@b3~ln&sDtT5SpW9a0q+5Gm|fpVY2|zqlNYBR}E5+ahgdj!CvK$Tlk0 z9g$5N;aar=CqMsudQV>yb4l@hN(9Jcc=1(|OHsqH6|g=K-WBd8GxZ`AkT?OO z-z_Ued-??Z*R4~L7jwJ%-`s~FK|qNAJ;EmIVDVpk{Lr7T4l{}vL)|GuUuswe9c5F| zv*5%u01hlv08?00Vpwyk*Q&&fY8k6MjOfpZfKa@F-^6d=Zv|0@&4_544RP5(s|4VPVP-f>%u(J@23BHqo2=zJ#v9g=F!cP((h zpt0|(s++ej?|$;2PE%+kc6JMmJjDW)3BXvBK!h!E`8Y&*7hS{c_Z?4SFP&Y<3evqf z9-ke+bSj$%Pk{CJlJbWwlBg^mEC^@%Ou?o>*|O)rl&`KIbHrjcpqsc$Zqt0^^F-gU2O=BusO+(Op}!jNzLMc zT;0YT%$@ClS%V+6lMTfhuzzxomoat=1H?1$5Ei7&M|gxo`~{UiV5w64Np6xV zVK^nL$)#^tjhCpTQMspXI({TW^U5h&Wi1Jl8g?P1YCV4=%ZYyjSo#5$SX&`r&1PyC zzc;uzCd)VTIih|8eNqFNeBMe#j_FS6rq81b>5?aXg+E#&$m++Gz9<+2)h=K(xtn}F ziV{rmu+Y>A)qvF}ms}4X^Isy!M&1%$E!rTO~5(p+8{U6#hWu>(Ll1}eD64Xa>~73A*538wry?v$vW z>^O#FRdbj(k0Nr&)U`Tl(4PI*%IV~;ZcI2z&rmq=(k^}zGOYZF3b2~Klpzd2eZJl> zB=MOLwI1{$RxQ7Y4e30&yOx?BvAvDkTBvWPpl4V8B7o>4SJn*+h1Ms&fHso%XLN5j z-zEwT%dTefp~)J_C8;Q6i$t!dnlh-!%haR1X_NuYUuP-)`IGWjwzAvp!9@h`kPZhf zwLwFk{m3arCdx8rD~K2`42mIN4}m%OQ|f)4kf%pL?Af5Ul<3M2fv>;nlhEPR8b)u} zIV*2-wyyD%%) zl$G@KrC#cUwoL?YdQyf9WH)@gWB{jd5w4evI& zOFF)p_D8>;3-N1z6mES!OPe>B^<;9xsh)){Cw$Vs-ez5nXS95NOr3s$IU;>VZSzKn zBvub8_J~I%(DozZW@{)Vp37-zevxMRZ8$8iRfwHmYvyjOxIOAF2FUngKj289!(uxY zaClWm!%x&teKmr^ABrvZ(ikx{{I-lEzw5&4t3P0eX%M~>$wG0ZjA4Mb&op+0$#SO_ z--R`>X!aqFu^F|a!{Up-iF(K+alKB{MNMs>e(i@Tpy+7Z-dK%IEjQFO(G+2mOb@BO zP>WHlS#fSQm0et)bG8^ZDScGnh-qRKIFz zfUdnk=m){ej0i(VBd@RLtRq3Ep=>&2zZ2%&vvf?Iex01hx1X!8U+?>ER;yJlR-2q4 z;Y@hzhEC=d+Le%=esE>OQ!Q|E%6yG3V_2*uh&_nguPcZ{q?DNq8h_2ahaP6=pP-+x zK!(ve(yfoYC+n(_+chiJ6N(ZaN+XSZ{|H{TR1J_s8x4jpis-Z-rlRvRK#U%SMJ(`C z?T2 zF(NNfO_&W%2roEC2j#v*(nRgl1X)V-USp-H|CwFNs?n@&vpRcj@W@xCJwR6@T!jt377?XjZ06=`d*MFyTdyvW!`mQm~t3luzYzvh^F zM|V}rO>IlBjZc}9Z zd$&!tthvr>5)m;5;96LWiAV0?t)7suqdh0cZis`^Pyg@?t>Ms~7{nCU;z`Xl+raSr zXpp=W1oHB*98s!Tpw=R5C)O{{Inl>9l7M*kq%#w9a$6N~v?BY2GKOVRkXYCgg*d

<5G2M1WZP5 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/project/Common.scala b/project/Common.scala index 607d0ab2b..c085c3a5a 100644 --- a/project/Common.scala +++ b/project/Common.scala @@ -23,8 +23,7 @@ object Common extends AutoPlugin { "-unchecked", "-Xlint", "-Yno-adapted-args", - "-Ywarn-numeric-widen", - "-Xfatal-warnings" + "-Ywarn-numeric-widen" ), scalacOptions in Test ++= Seq("-Yrangepos"), autoAPIMappings := true diff --git a/project/build.properties b/project/build.properties index b7dd3cb2a..394cb75cf 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.0.2 +sbt.version=1.0.4 diff --git a/scripts/script-helper b/scripts/script-helper new file mode 100644 index 000000000..ac39d9a51 --- /dev/null +++ b/scripts/script-helper @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e +set -o pipefail + +java_version=$(java -version 2>&1 | java -version 2>&1 | awk -F '"' '/version/ {print $2}') + +if [[ $java_version = 9* ]] ; then + echo "The build is using Java 9 ($java_version). We need additional JVM parameters" + export _JAVA_OPTIONS="$_JAVA_OPTIONS --add-modules=java.xml.bind" +else + echo "The build is NOT using Java 9 ($java_version). No addional JVM params needed." +fi diff --git a/scripts/test-gradle b/scripts/test-gradle new file mode 100755 index 000000000..298c7ebe1 --- /dev/null +++ b/scripts/test-gradle @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +. "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/script-helper" + +# Using cut because TRAVIS_SCALA_VERSION is the full Scala +# version (for example 2.12.4), but Gradle expects just the +# binary version (for example 2.12) +scala_binary_version=$(echo $TRAVIS_SCALA_VERSION | cut -c1-4) + +echo "+------------------------------+" +echo "| Executing tests using Gradle |" +echo "+------------------------------+" +./gradlew -Dscala.binary.version=$scala_binary_version test -i --stacktrace diff --git a/scripts/test-sbt b/scripts/test-sbt new file mode 100755 index 000000000..0425367b1 --- /dev/null +++ b/scripts/test-sbt @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +. "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/script-helper" + +echo "+----------------------------+" +echo "| Executing tests using sbt |" +echo "+----------------------------+" +sbt ++$TRAVIS_SCALA_VERSION test From a12f67b2ca51d0cfd5dcd8f4de182e4804d469f5 Mon Sep 17 00:00:00 2001 From: Greg Methvin Date: Thu, 21 Dec 2017 20:59:17 +0000 Subject: [PATCH 47/73] Updated with template-control on 2017-12-21T20:59:17.361Z **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.10") --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index 1ba3c73ca..280f04884 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.9") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.10") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.1") From d44f52d15066b79fe615c498a8728d1d0de6864d Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 2 Jan 2018 15:46:31 -0200 Subject: [PATCH 48/73] Upgrade branch 2.6.x using TemplateControl (#102) * Updated with template-control on 2017-12-22T16:49:12.815Z **/test-gradle: ./gradlew -Dscala.binary.version=$scala_binary_version check -i --stacktrace * Add run permission to test-gradle script --- scripts/test-gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test-gradle b/scripts/test-gradle index 298c7ebe1..84a051a20 100755 --- a/scripts/test-gradle +++ b/scripts/test-gradle @@ -10,4 +10,4 @@ scala_binary_version=$(echo $TRAVIS_SCALA_VERSION | cut -c1-4) echo "+------------------------------+" echo "| Executing tests using Gradle |" echo "+------------------------------+" -./gradlew -Dscala.binary.version=$scala_binary_version test -i --stacktrace +./gradlew -Dscala.binary.version=$scala_binary_version check -i --stacktrace From d800db284f70b44e5ef743ab4526d867a60c86fd Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 9 Jan 2018 21:02:23 -0200 Subject: [PATCH 49/73] Upgrade branch 2.6.x using TemplateControl (#103) * Updated with template-control on 2018-01-09T20:17:24.520Z **/build.properties: sbt.version=1.1.0 **build.gradle: def playVersion = "2.6.10" **build.gradle: def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") * test-gradle-permissions --- build.gradle | 4 ++-- project/build.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index e5d97395f..e19cc0832 100644 --- a/build.gradle +++ b/build.gradle @@ -4,8 +4,8 @@ plugins { id "com.github.lkishalmi.gatling" version "0.7.1" } -def playVersion = '2.6.9' -def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.11") +def playVersion = "2.6.10" +def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") def gatlingVersion = "2.3.0" model { diff --git a/project/build.properties b/project/build.properties index 394cb75cf..8b697bbb9 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.0.4 +sbt.version=1.1.0 From 3dc4afc87d9aacc548aee236954a3241547cda48 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 11 Jan 2018 19:20:09 -0200 Subject: [PATCH 50/73] Fix cross build to sbt 1.1.0 (#104) * Fix cross build to sbt 1.1.0 * Fix gatling dependency --- build.sbt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index d3bfda0b4..fb1c09760 100644 --- a/build.sbt +++ b/build.sbt @@ -4,6 +4,13 @@ lazy val GatlingTest = config("gatling") extend Test scalaVersion in ThisBuild := "2.12.4" +crossScalaVersions := Seq("2.11.12", "2.12.4") + +def gatlingVersion(scalaBinVer: String): String = scalaBinVer match { + case "2.11" => "2.2.5" + case "2.12" => "2.3.0" +} + libraryDependencies += guice libraryDependencies += "org.joda" % "joda-convert" % "1.9.2" libraryDependencies += "net.logstash.logback" % "logstash-logback-encoder" % "4.11" @@ -12,8 +19,8 @@ libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.16" libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.1" libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test -libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.3.0" % Test -libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.3.0" % Test +libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % gatlingVersion(scalaBinaryVersion.value) % Test +libraryDependencies += "io.gatling" % "gatling-test-framework" % gatlingVersion(scalaBinaryVersion.value) % Test // The Play project itself lazy val root = (project in file(".")) From 256311603b1d929d738290f3914e368d10698682 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Fri, 12 Jan 2018 15:13:48 -0200 Subject: [PATCH 51/73] Upgrade branch 2.6.x using TemplateControl (#105) * Updated with template-control on 2018-01-11T21:32:27.807Z **build.sbt: libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.3.0" % Test **build.sbt: libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.3.0" % Test **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.11") **build.gradle: def playVersion = "2.6.11" * test-gradle-permissions * Reverting gatling version changes --- build.gradle | 2 +- project/plugins.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index e19cc0832..f364dfc9a 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id "com.github.lkishalmi.gatling" version "0.7.1" } -def playVersion = "2.6.10" +def playVersion = "2.6.11" def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") def gatlingVersion = "2.3.0" diff --git a/project/plugins.sbt b/project/plugins.sbt index 280f04884..3198c2f58 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.10") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.11") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.1") From 1b79d89664039bbfaa6e0abcaed4cc4f838c2abd Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Fri, 2 Mar 2018 18:19:18 -0300 Subject: [PATCH 52/73] Upgrade branch 2.6.x using TemplateControl (#108) * Updated with template-control on 2018-03-02T18:56:28.780Z **build.sbt: libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.3.0" % Test **build.sbt: libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.3.0" % Test **/build.properties: sbt.version=1.1.1 **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.12") **build.gradle: def playVersion = "2.6.12" * Revert gatling version changes --- build.gradle | 2 +- project/build.properties | 2 +- project/plugins.sbt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index f364dfc9a..f48a6e091 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id "com.github.lkishalmi.gatling" version "0.7.1" } -def playVersion = "2.6.11" +def playVersion = "2.6.12" def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") def gatlingVersion = "2.3.0" diff --git a/project/build.properties b/project/build.properties index 8b697bbb9..31334bbd3 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.1.0 +sbt.version=1.1.1 diff --git a/project/plugins.sbt b/project/plugins.sbt index 3198c2f58..c43f3431f 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.11") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.12") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.1") From 750ad3050ade86b74d1ea814ccbbe6f221482f22 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Mon, 9 Apr 2018 14:57:07 -0300 Subject: [PATCH 53/73] Upgrade branch 2.6.x using TemplateControl (#109) * Updated with template-control on 2018-04-06T19:34:58.318Z **build.sbt: libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.3.0" % Test **build.sbt: libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.3.0" % Test **/build.properties: sbt.version=1.1.2 **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.13") **build.gradle: def playVersion = "2.6.13" * Revert gatling change --- build.gradle | 2 +- project/build.properties | 2 +- project/plugins.sbt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index f48a6e091..e49afece9 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id "com.github.lkishalmi.gatling" version "0.7.1" } -def playVersion = "2.6.12" +def playVersion = "2.6.13" def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") def gatlingVersion = "2.3.0" diff --git a/project/build.properties b/project/build.properties index 31334bbd3..05313438a 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.1.1 +sbt.version=1.1.2 diff --git a/project/plugins.sbt b/project/plugins.sbt index c43f3431f..3f1a7f8e0 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.12") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.13") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.1") From fa9f9cf6a2893c883555f132fa5d084b4cfbd5c8 Mon Sep 17 00:00:00 2001 From: Rich Dougherty Date: Mon, 28 May 2018 11:54:09 +1200 Subject: [PATCH 54/73] Updated with template-control on 2018-05-27T23:54:09.491Z **build.sbt: libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.3.1" % Test **build.sbt: libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.3.1" % Test **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.15") **build.gradle: def playVersion = "2.6.15" --- build.gradle | 2 +- build.sbt | 4 ++-- project/plugins.sbt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index e49afece9..5becdf167 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id "com.github.lkishalmi.gatling" version "0.7.1" } -def playVersion = "2.6.13" +def playVersion = "2.6.15" def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") def gatlingVersion = "2.3.0" diff --git a/build.sbt b/build.sbt index fb1c09760..8df68a30a 100644 --- a/build.sbt +++ b/build.sbt @@ -19,8 +19,8 @@ libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.16" libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.1" libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test -libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % gatlingVersion(scalaBinaryVersion.value) % Test -libraryDependencies += "io.gatling" % "gatling-test-framework" % gatlingVersion(scalaBinaryVersion.value) % Test +libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.3.1" % Test +libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.3.1" % Test // The Play project itself lazy val root = (project in file(".")) diff --git a/project/plugins.sbt b/project/plugins.sbt index 3f1a7f8e0..1af9cd7a1 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.13") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.15") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.1") From 0f43e0734ffa24b1404f7a3a85ebbd51fcb584d0 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Mon, 28 May 2018 11:48:12 -0400 Subject: [PATCH 55/73] Revert gatling version change and update Scala 2.12 --- build.sbt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.sbt b/build.sbt index 8df68a30a..91d074a25 100644 --- a/build.sbt +++ b/build.sbt @@ -2,13 +2,13 @@ import sbt.Keys._ lazy val GatlingTest = config("gatling") extend Test -scalaVersion in ThisBuild := "2.12.4" +scalaVersion in ThisBuild := "2.12.6" -crossScalaVersions := Seq("2.11.12", "2.12.4") +crossScalaVersions := Seq("2.11.12", "2.12.6") def gatlingVersion(scalaBinVer: String): String = scalaBinVer match { case "2.11" => "2.2.5" - case "2.12" => "2.3.0" + case "2.12" => "2.3.1" } libraryDependencies += guice @@ -19,8 +19,8 @@ libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.16" libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.1" libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test -libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.3.1" % Test -libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.3.1" % Test +libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % gatlingVersion(scalaBinaryVersion.value) % Test +libraryDependencies += "io.gatling" % "gatling-test-framework" % gatlingVersion(scalaBinaryVersion.value) % Test // The Play project itself lazy val root = (project in file(".")) From ca5dfbdaa61c7ef41841625b4aa84d81fc57a139 Mon Sep 17 00:00:00 2001 From: Arnout Engelen Date: Fri, 8 Jun 2018 16:22:18 +0200 Subject: [PATCH 56/73] Use @ref to link to other .md pages (#93) --- docs/src/main/paradox/index.md | 4 ++-- docs/src/main/paradox/part-1/index.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index f40649785..63e184ff0 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -64,7 +64,7 @@ With that caveat, let's start working with Play! @@@index -* [Basics](part-1/index.md) -* [Appendix](appendix.md) +* @ref[Basics](part-1/index.md) +* @ref[Appendix](appendix.md) @@@ diff --git a/docs/src/main/paradox/part-1/index.md b/docs/src/main/paradox/part-1/index.md index 16a43fe95..1783f3ef1 100644 --- a/docs/src/main/paradox/part-1/index.md +++ b/docs/src/main/paradox/part-1/index.md @@ -10,7 +10,7 @@ git clone https://github.com/playframework/play-rest-api.git We're going to be showing an already working Play project with most of the code available under the "app/v1" directory. There will be several different versions of the same project as this series expands, so you can compare different versions of the project against each other. -To run Play on your own local computer, please see the instructions in the [appendix](../appendix.html). +To run Play on your own local computer, please see the instructions in the @ref[appendix](../appendix.md). ## Introduction From 9742862c1e83b1ba6227e4f52050ed4f9817b495 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Fri, 8 Jun 2018 10:34:21 -0400 Subject: [PATCH 57/73] Update documentation to reflect Play 2.6 changes (#114) * Update documentation to reflect Play 2.6 changes Also update links to source code and documentation. * Trim trailing whitespaces --- app/v1/post/PostActionBuilder.scala | 2 +- docs/src/main/paradox/appendix.md | 33 ++- docs/src/main/paradox/index.md | 36 ++- docs/src/main/paradox/part-1/index.md | 377 +++++++++++++------------- 4 files changed, 233 insertions(+), 215 deletions(-) diff --git a/app/v1/post/PostActionBuilder.scala b/app/v1/post/PostActionBuilder.scala index f981e3149..a3e2f33c7 100644 --- a/app/v1/post/PostActionBuilder.scala +++ b/app/v1/post/PostActionBuilder.scala @@ -52,7 +52,7 @@ class PostActionBuilder @Inject()(messagesApi: MessagesApi, playBodyParsers: Pla with RequestMarkerContext with HttpVerbs { - val parser: BodyParser[AnyContent] = playBodyParsers.anyContent + override val parser: BodyParser[AnyContent] = playBodyParsers.anyContent type PostRequestBlock[A] = PostRequest[A] => Future[Result] diff --git a/docs/src/main/paradox/appendix.md b/docs/src/main/paradox/appendix.md index a81ef378d..0a2e967b5 100644 --- a/docs/src/main/paradox/appendix.md +++ b/docs/src/main/paradox/appendix.md @@ -7,14 +7,14 @@ This appendix covers how to download, run, use and load test Play. You will need a JDK 1.8 that is more recent than b20. You can download the JDK from [here](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html). -You will need to have git installed. +You will need to have [git](https://git-scm.com/) installed. ## Downloading -You can download the example project from Github: +You can clone the example project from Github: -``` -git clone https://github.com/playframework/play-rest-api.git +```bash +git clone https://github.com/playframework/play-scala-rest-api-example.git ``` ## Running @@ -23,21 +23,21 @@ You need to download and install sbt for this application to run. You can do th Once you have sbt installed, the following at the command prompt will download any required library dependencies, and start up Play in development mode: -``` +```bash sbt run ``` -Play will start up on the HTTP port at http://localhost:9000/. You don't need to reploy or reload anything -- changing any source code while the server is running will automatically recompile and hot-reload the application on the next HTTP request. You can read more about using Play [here](https://www.playframework.com/documentation/2.5.x/PlayConsole). +Play will start up on the HTTP port at . You don't need to deploy or reload anything -- changing any source code while the server is running will automatically recompile and hot-reload the application on the next HTTP request. You can read more about using Play [here](https://www.playframework.com/documentation/latest/PlayConsole). ## Usage If you call the same URL from the command line, you’ll see JSON. Using [httpie](https://httpie.org/), we can execute the command: -``` +```bash http --verbose http://localhost:9000/v1/posts ``` -and get back: +And get back: ``` GET /v1/posts HTTP/1.1 @@ -45,7 +45,7 @@ GET /v1/posts HTTP/1.1 Likewise, you can also send a POST directly as JSON: -``` +```bash http --verbose POST http://localhost:9000/v1/posts title="hello" body="world" ``` @@ -57,19 +57,18 @@ POST /v1/posts HTTP/1.1 ## Load Testing -The best way to see what Play can do is to run a load test. We've included Gatling in this test project for integrated load testing. +The best way to see what Play can do is to run a load test. We've included [Gatling](https://gatling.io/) in this test project for integrated load testing. -Start Play in production mode, by [staging the application](https://www.playframework.com/documentation/2.5.x/Deploying) and running the play scripts: +Start Play in production mode, by [staging the application](https://www.playframework.com/documentation/latest/Deploying) and running the play scripts: -``` +```bash sbt stage -cd target/universal/stage -bin/play-rest-api -Dplay.crypto.secret=testing +./target/universal/stage/bin/play-scala-rest-api-example -Dplay.http.secret.key=testing ``` Then you'll start the Gatling load test up (it's already integrated into the project): -``` +```bash sbt gatling:test ``` @@ -77,8 +76,8 @@ For best results, start the gatling load test up on another machine so you do no Once the test completes, you'll see an HTML file containing the load test chart: -``` - ./rest-api/target/gatling/gatlingspec-1472579540405/index.html +```bash +./rest-api/target/gatling/gatlingspec-1472579540405/index.html ``` That will contain your load test results. diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index 63e184ff0..d88aae9e5 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -1,32 +1,38 @@ # Making a REST API with Play -This is a multi-part guide to walk you through how to make a RESTful API with JSON using [Play 2.5](https://playframework.com). +This is a multi-part guide to walk you through how to make a RESTful API with JSON using [Play Framework](https://playframework.com). -We’ll demonstrate with a “best practices” REST API. You can get source code for this guide two ways: +We’ll demonstrate with a "best practices" REST API. You can get source code for this guide two ways: -* Download a pre-packaged bundle with this link [https://example.lightbend.com/v1/download/play-scala-rest-api-example](https://example.lightbend.com/v1/download/play-scala-rest-api-example) +## From Lightbend Tech Hub + +Download a pre-packaged bundle with this link [https://example.lightbend.com/v1/download/play-scala-rest-api-example](https://example.lightbend.com/v1/download/play-scala-rest-api-example) + +### Linux/Mac - * Linux/Mac: ```bash unzip play-scala-rest-api-example.zip cd play-scala-rest-api-example ./sbt ``` - * Windows: - 1. Unzip the download - 2. From a command line `cd` into the directory where you expanded the downloaded `zip` file and run: -``` +### Windows + +1. Unzip the download +1. From a command line `cd` into the directory where you expanded the downloaded `zip` file and run: + +```bash sbt.bat ``` -* [From Github](https://github.com/playframework/play-scala-rest-api-example/tree/2.5.x): -``` +## [From Github](https://github.com/playframework/play-scala-rest-api-example/tree/2.6.x): + +```bash git clone https://github.com/playframework/play-scala-rest-api-example.git -git checkout 2.5.x +git checkout 2.6.x ``` -This example is in Scala, but Play also has a [Java API](https://www.playframework.com/documentation/2.5.x/JavaHome) which looks and acts just like the [Scala API](https://www.playframework.com/documentation/2.5.x/ScalaHome), and has a corresponding [play-java-rest-api-example](https://github.com/playframework/play-java-rest-api-example) project. For instructions on running and using the project, please see the [[appendix]]. This project also comes with an integrated [Gatling](http://gatling.io/) load test -- again, instructions are in the appendix. +This example is in Scala, but Play also has a [Java API](https://www.playframework.com/documentation/latest/JavaHome) which looks and acts just like the [Scala API](https://www.playframework.com/documentation/latest/ScalaHome), and has a corresponding [play-java-rest-api-example](https://github.com/playframework/play-java-rest-api-example) project. For instructions on running and using the project, please see the [[appendix]]. This project also comes with an integrated [Gatling](http://gatling.io/) load test -- again, instructions are in the appendix. Note that there’s more involved in a REST API -- monitoring, representation, and managing access to back end resources -- that we'll cover in subsequent posts. But first, let's address why Play is so effective as a REST API. @@ -46,13 +52,13 @@ Play provides an easy to use MVC paradigm, including hot-reloading without any J Play combines this with a **reactive programming API** that lets you write async, non-blocking code in a straightforward fashion without worrying about complex and confusing "callback hell." In both Java or Scala, Play works on the same principle: leverage the asynchronous computation API that the language provides to you. In Play, you work with [`java.util.concurrent.CompletionStage`](https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/changes8.html) or [`scala.concurrent.Future`](http://docs.scala-lang.org/overviews/core/futures.html) API directly, and Play passes that asynchronous computation back through the framework. -Finally, Play is modular and extensible. Play works with multiple runtime and compile time dependency injection frameworks like [Guice](https://www.playframework.com/documentation/2.5.x/ScalaDependencyInjection), [Macwire](https://di-in-scala.github.io/), [Dagger](https://github.com/esfand-r/play-java-dagger-dependency-injection#master), and leverages DI principles to integrate authentication and authorization frameworks built on top of Play. +Finally, Play is modular and extensible. Play works with multiple runtime and compile time dependency injection frameworks like [Guice](https://www.playframework.com/documentation/latest/ScalaDependencyInjection), [Macwire](https://di-in-scala.github.io/), [Dagger](https://github.com/playframework/play-java-dagger2-example), and leverages DI principles to integrate authentication and authorization frameworks built on top of Play. ## Community -To learn more about Play, check out the [Play tutorials](https://playframework.com/documentation/2.5.x/Tutorials) and see more examples and blog posts about Play, including streaming [Server Side Events](https://github.com/playframework/play-streaming-scala) and first class [WebSocket support](https://github.com/playframework/play-websocket-scala). +To learn more about Play, check out the [Play tutorials](https://playframework.com/documentation/latest/Tutorials) and see more examples and blog posts about Play, including streaming [Server Side Events](https://github.com/playframework/play-streaming-scala) and first class [WebSocket support](https://github.com/playframework/play-websocket-scala). -To get more involved and if you have questions, join the [mailing list](https://groups.google.com/forum/#!forum/play-framework) at and follow [PlayFramework on Twitter](https://twitter.com/playframework). +To get more involved and if you have questions, join the [forums](https://discuss.playframework.com) at and follow [PlayFramework on Twitter](https://twitter.com/playframework). ## Microservices vs REST APIs diff --git a/docs/src/main/paradox/part-1/index.md b/docs/src/main/paradox/part-1/index.md index 1783f3ef1..99af52a51 100644 --- a/docs/src/main/paradox/part-1/index.md +++ b/docs/src/main/paradox/part-1/index.md @@ -1,16 +1,16 @@ # Basics -This guide will walk you through how to make a REST API with JSON using [Play 2.5](https://playframework.com). +This guide will walk you through how to make a REST API with JSON using [Play Framework](https://playframework.com). -To see the associated Github project, please go to [https://github.com/playframework/play-rest-api](https://github.com/playframework/play-rest-api) or clone the project: +To see the associated Github project, please go to or clone the project: -``` -git clone https://github.com/playframework/play-rest-api.git +```bash +git clone https://github.com/playframework/play-scala-rest-api-example.git ``` -We're going to be showing an already working Play project with most of the code available under the "app/v1" directory. There will be several different versions of the same project as this series expands, so you can compare different versions of the project against each other. +We're going to be showing an already working Play project with most of the code available under the `app/v1` directory. There will be several different versions of the same project as this series expands, so you can compare different versions of the project against each other. -To run Play on your own local computer, please see the instructions in the @ref[appendix](../appendix.md). +To run Play on your own local computer, please see the instructions in the @ref[appendix](../appendix.md). ## Introduction @@ -20,14 +20,18 @@ We'll start off with a REST API that displays information for blog posts. Users The way to do this in REST is to model the represented state as a resource. A blog post resource will have a unique id, a URL hyperlink that indicates the canonical location of the resource, the title of the blog post, and the body of the blog post. -This resource is represented as a single case class in the Play application [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostResourceHandler.scala#L13): +This resource is represented as a single case class in the Play application [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostResourceHandler.scala#L13): ```scala -case class PostResource(id: String, link: String, - title: String, body: String) +case class PostResource( + id: String, + link: String, + title: String, + body: String +) ``` -This resource is mapped to and from JSON on the front end using Play, and is mapped to and from a persistent datastore on the backend using a handler. +This resource is mapped to and from JSON on the front end using Play, and is mapped to and from a persistent datastore on the backend using a handler. Play handles HTTP routing and representation for the REST API and makes it easy to write a non-blocking, asynchronous API that is an order of magnitude more efficient than other web application frameworks. @@ -41,46 +45,53 @@ GET / controllers.HomeController.index() This is useful for situations where a front end service is rendering HTML. However, Play also contains a more powerful routing DSL that we will use for the REST API. -For every HTTP request starting with `/v1/posts`, Play routes it to a dedicated `PostRouter` class to handle the Posts resource, through the [`conf/routes`](https://github.com/playframework/play-rest-api/blob/master/conf/routes) file: +For every HTTP request starting with `/v1/posts`, Play routes it to a dedicated `PostRouter` class to handle the Posts resource, through the [`conf/routes`](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/conf/routes) file: ``` -> /v1/posts v1.post.PostRouter ``` -The `PostRouter` examines the URL and extracts data to pass along to the controller [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostRouter.scala): +The `PostRouter` examines the URL and extracts data to pass along to the controller [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostRouter.scala): ```scala package v1.post + import javax.inject.Inject -import play.api.mvc._ import play.api.routing.Router.Routes import play.api.routing.SimpleRouter import play.api.routing.sird._ -class PostRouter @Inject()(controller: PostController) - extends SimpleRouter -{ +class PostRouter @Inject()(controller: PostController) extends SimpleRouter { + val prefix = "/v1/posts" + + def link(id: PostId): String = { + import com.netaporter.uri.dsl._ + val url = prefix / id.toString + url.toString() + } + override def routes: Routes = { case GET(p"/") => - controller.index - + controller.index + case POST(p"/") => controller.process case GET(p"/$id") => controller.show(id) } + } ``` -Play’s [routing DSL](https://www.playframework.com/documentation/2.5.x/ScalaSirdRouter) (technically "String Interpolation Routing DSL", aka SIRD) shows how data can be extracted from the URL concisely and cleanly. SIRD is based around HTTP methods and a string interpolated extractor object – this means that when we type the string “/$id” and prefix it with “p”, then the path parameter id can be extracted and used in the block. Naturally, there are also operators to extract queries, regular expressions, and even add custom extractors. If you have a URL as follows: +Play’s [routing DSL](https://www.playframework.com/documentation/latest/ScalaSirdRouter) (technically "String Interpolation Routing DSL", aka SIRD) shows how data can be extracted from the URL concisely and cleanly. SIRD is based around HTTP methods and a string interpolated extractor object – this means that when we type the string “/$id” and prefix it with “p”, then the path parameter id can be extracted and used in the block. Naturally, there are also operators to extract queries, regular expressions, and even add custom extractors. If you have a URL as follows: ``` /posts/?sort=ascending&count=5 ``` -then you can extract the "sort" and "count" parameters in a single line: +Then you can extract the "sort" and "count" parameters in a single line: ```scala GET("/" ? q_?"sort=$sort" & q_?”count=${ int(count) }") @@ -90,142 +101,133 @@ SIRD is especially useful in a REST API where there can be many possible query p ## Using a Controller -The PostRouter has a PostController injected into it through standard [JSR-330 dependency injection](https://github.com/google/guice/wiki/JSR330) [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostRouter.scala#L12): +The `PostRouter` has a `PostController` injected into it through standard [JSR-330 dependency injection](https://github.com/google/guice/wiki/JSR330) [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostRouter.scala#L12): ```scala -class PostRouter @Inject()(controller: PostController) - extends SimpleRouter +class PostRouter @Inject()(controller: PostController) extends SimpleRouter ``` -Before heading into the PostController, let's discuss how controllers work in Play. +Before heading into the `PostController`, let's discuss how controllers work in Play. -A controller [handles the work of processing](https://www.playframework.com/documentation/2.5.x/ScalaActions) the HTTP request into an HTTP response in the context of an Action: it's where page rendering and HTML form processing happen. A controller extends [`play.api.mvc.Controller`](https://playframework.com/documentation/2.5.x/api/scala/index.html#play.api.mvc.Controller), which contains a number of utility methods and constants for working with HTTP. In particular, a Controller contains Result objects such as Ok and Redirect, and HeaderNames like ACCEPT. +A controller [handles the work of processing](https://www.playframework.com/documentation/latest/ScalaActions) the HTTP request into an HTTP response in the context of an Action: it's where page rendering and HTML form processing happen. A controller extends [`play.api.mvc.BaseController`](https://www.playframework.com/documentation/latest/api/scala/index.html#play.api.mvc.BaseController), which contains a number of utility methods and constants for working with HTTP. In particular, a `Controller` contains `Result` objects such as `Ok` and `Redirect`, and `HeaderNames` like `ACCEPT`. -The methods in a controller consist of a method returning an [Action](https://playframework.com/documentation/2.5.x/api/scala/index.html#play.api.mvc.Action). The Action provides the "engine" to Play. +The methods in a controller consist of a method returning an [Action](https://www.playframework.com/documentation/latest/api/scala/index.html#play.api.mvc.Action). The Action provides the "engine" to Play. -Using the action, the controller passes in a block of code that takes a [`Request`](https://playframework.com/documentation/2.5.x/api/scala/index.html#play.api.mvc.Request) passed in as implicit – this means that any in-scope method that takes an implicit request as a parameter will use this request automatically. Then, the block must return either a [`Result`](https://playframework.com/documentation/2.5.x/api/scala/index.html#play.api.mvc.Result), or a [`Future[Result]`](http://www.scala-lang.org/api/current/index.html#scala.concurrent.Future), depending on whether or not the action was called as `action { ... }` or [`action.async { ... }`](https://www.playframework.com/documentation/2.5.x/ScalaAsync#How-to-create-a-Future[Result]). - -### Handling GET Requests +Using the action, the controller passes in a block of code that takes a [`Request`](https://www.playframework.com/documentation/latest/api/scala/index.html#play.api.mvc.Request) passed in as implicit – this means that any in-scope method that takes an implicit request as a parameter will use this request automatically. Then, the block must return either a [`Result`](https://www.playframework.com/documentation/latest/api/scala/index.html#play.api.mvc.Result), or a [`Future[Result]`](http://www.scala-lang.org/api/current/index.html#scala.concurrent.Future), depending on whether or not the action was called as `action { ... }` or [`action.async { ... }`](https://www.playframework.com/documentation/latest/ScalaAsync#How-to-create-a-Future[Result]). +### Handling GET Requests Here's a simple example of a Controller: - + ```scala import javax.inject.Inject import play.api.mvc._ import scala.concurrent._ -class MyController extends Controller { +class MyController @Inject()(val controllerComponents: ControllerComponents) extends BaseController { - def index1: Action[AnyContent] = { - Action { implicit request => - val r: Result = Ok("hello world") - r - } + def index1: Action[AnyContent] = Action { implicit request => + val r: Result = Ok("hello world") + r } - def asyncIndex: Action[AnyContent] = { - Action.async { implicit request => - val r: Future[Result] = Future.successful(Ok("hello world")) - r - } + def asyncIndex: Action[AnyContent] = Action.async { implicit request => + val r: Future[Result] = Future.successful(Ok("hello world")) + r } } ``` -In this example, `index1` and `asyncIndex` have exactly the same behavior. Internally, it makes no difference whether we call `Result` or `Future[Result]` -- Play is non-blocking all the way through. +In this example, `index1` and `asyncIndex` have exactly the same behavior. Internally, it makes no difference whether we call `Result` or `Future[Result]` -- Play is non-blocking all the way through. -However, if you're already working with `Future`, async makes it easier to pass that `Future` around. You can read more about this in the [handling asynchronous results](https://www.playframework.com/documentation/2.5.x/ScalaAsync) section of the Play documentation. +However, if you're already working with `Future`, async makes it easier to pass that `Future` around. You can read more about this in the [handling asynchronous results](https://www.playframework.com/documentation/latest/ScalaAsync) section of the Play documentation. -The PostController methods dealing with GET requests is [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostController.scala): +The PostController methods dealing with GET requests is [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostController.scala). Let's take a look at the most important parts: ```scala -class PostController @Inject()(action: PostAction, - handler: PostResourceHandler) - (implicit ec: ExecutionContext) - extends Controller { - - def index: Action[AnyContent] = { - action.async { implicit request => - handler.find.map { posts => - Ok(Json.toJson(posts)) - } - } - } - - def show(id: String): Action[AnyContent] = { - action.async { implicit request => - handler.lookup(id).map { post => - Ok(Json.toJson(post)) - } - } - } +package v1.post + +import javax.inject.Inject +import play.api.Logger +import play.api.data.Form +import play.api.libs.json.Json +import play.api.mvc._ + +import scala.concurrent.{ ExecutionContext, Future } + +class PostController @Inject()(cc: PostControllerComponents)(implicit ec: ExecutionContext) + extends PostBaseController(cc) { + + def index: Action[AnyContent] = PostAction.async { implicit request => + logger.trace("index: ") + postResourceHandler.find.map { posts => + Ok(Json.toJson(posts)) + } + } + + def show(id: String): Action[AnyContent] = PostAction.async { implicit request => + logger.trace(s"show: id = $id") + postResourceHandler.lookup(id).map { post => + Ok(Json.toJson(post)) + } + } } ``` -Let's take `show` as an example. Here, the action defines a workflow for a request that maps to a single resource, i.e. `GET /v1/posts/123`. +Let's take `show` as an example. Here, the action defines a workflow for a request that maps to a single resource, i.e. `GET /v1/posts/123`. ```scala -def show(id: String): Action[AnyContent] = { - action.async { implicit request => - handler.lookup(id).map { post => - Ok(Json.toJson(post)) - } +def show(id: String): Action[AnyContent] = PostAction.async { implicit request => + logger.trace(s"show: id = $id") + postResourceHandler.lookup(id).map { post => + Ok(Json.toJson(post)) } } ``` -The id is passed in as a String, and the handler looks up and returns a `PostResource`. The `Ok()` sends back a `Result` with a status code of "200 OK", containing a response body consisting of the `PostResource` serialized as JSON. +The `id` is passed in as a `String`, and the handler looks up and returns a `PostResource`. The `Ok()` sends back a `Result` with a status code of "200 OK", containing a response body consisting of the `PostResource` serialized as JSON. ### Processing Form Input -Handling a POST request is also easy and is done through the `process` method: +Handling a `POST` request is also easy and is done through the `process` method: ```scala -class PostController @Inject()(action: PostAction, - handler: PostResourceHandler) - (implicit ec: ExecutionContext) - extends Controller { - - private val form: Form[PostFormInput] = { - import play.api.data.Forms._ - - Form( - mapping( - "title" -> nonEmptyText, - "body" -> text - )(PostFormInput.apply)(PostFormInput.unapply) - ) - } +private val form: Form[PostFormInput] = { + import play.api.data.Forms._ - def process: Action[AnyContent] = { - action.async { implicit request => - processJsonPost() - } - } + Form( + mapping( + "title" -> nonEmptyText, + "body" -> text + )(PostFormInput.apply)(PostFormInput.unapply) + ) +} - private def processJsonPost[A]()(implicit request: PostRequest[A]): Future[Result] = { - def failure(badForm: Form[PostFormInput]) = { - Future.successful(BadRequest(badForm.errorsAsJson)) - } +def process: Action[AnyContent] = PostAction.async { implicit request => + logger.trace("process: ") + processJsonPost() +} - def success(input: PostFormInput) = { - handler.create(input).map { post => - Created(Json.toJson(post)) - .withHeaders(LOCATION -> post.link) - } - } +private def processJsonPost[A]()(implicit request: PostRequest[A]): Future[Result] = { + def failure(badForm: Form[PostFormInput]) = { + Future.successful(BadRequest(badForm.errorsAsJson)) + } - form.bindFromRequest().fold(failure, success) + def success(input: PostFormInput) = { + postResourceHandler.create(input).map { post => + Created(Json.toJson(post)).withHeaders(LOCATION -> post.link) + } } + + form.bindFromRequest().fold(failure, success) } ``` -Here, the `process` action is an action wrapper, and `processJsonPost` does most of the work. In `processJsonPost`, we get to the [form processing](https://www.playframework.com/documentation/2.5.x/ScalaForms) part of the code. +Here, the `process` action is an action wrapper, and `processJsonPost` does most of the work. In `processJsonPost`, we get to the [form processing](https://www.playframework.com/documentation/latest/ScalaForms) part of the code. -Here, `form.bindFromRequest()` will map input from the HTTP request to a [`play.api.data.Form`](https://www.playframework.com/documentation/2.5.x/api/scala/index.html#play.api.data.Form), and handles form validation and error reporting. +Here, `form.bindFromRequest()` will map input from the HTTP request to a [`play.api.data.Form`](https://www.playframework.com/documentation/latest/api/scala/index.html#play.api.data.Form), and handles form validation and error reporting. If the `PostFormInput` passes validation, it's passed to the resource handler, using the `success` method. If the form processing fails, then the `failure` method is called and the `FormError` is returned in JSON format. @@ -242,7 +244,7 @@ private val form: Form[PostFormInput] = { } ``` -The form binds to the HTTP request using the names in the mapping -- "title" and "body" to the `PostFormInput` case class [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostController.scala#L11). +The form binds to the HTTP request using the names in the mapping -- `title` and `body` to the `PostFormInput` case class [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostController.scala#L12). ```scala case class PostFormInput(title: String, body: String) @@ -252,48 +254,46 @@ That's all you need to do to handle a basic web application! As with most thing ## Using Actions -We saw in the `PostController` that each method is connected to an Action through the "action.async" method [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostController.scala#L32): +We saw in the `PostController` that each method is connected to an Action through the `PostAction.async` method [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostController.scala#L33): ```scala - def index: Action[AnyContent] = { - action.async { implicit request => - handler.find.map { posts => - Ok(Json.toJson(posts)) - } - } +def index: Action[AnyContent] = PostAction.async { implicit request => + logger.trace("index: ") + postResourceHandler.find.map { posts => + Ok(Json.toJson(posts)) } +} ``` -The action.async takes a function, and comes from the class parameter "action", which we can see is of type `PostAction` [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostController.scala#L16): - -```scala -class PostController @Inject()(action: PostAction [...]) -``` +The `PostAction.async` is a [custom action builder](https://www.playframework.com/documentation/2.6.x/ScalaActionsComposition#Custom-action-builders) defined [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostActionBuilder.scala#L49-L53) that can handle `PostRequest`s (see definition [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostActionBuilder.scala#L20)): -`PostAction` is an ActionBuilder. It is involved in each action in the controller -- it mediates the paperwork involved with processing a request into a response, adding context to the request and enriching the response with headers and cookies. ActionBuilders are essential for handling authentication, authorization and monitoring functionality. +`PostAction` is involved in each action in the controller -- it mediates the paperwork involved with processing a request into a response, adding context to the request and enriching the response with headers and cookies. ActionBuilders are essential for handling authentication, authorization and monitoring functionality. -ActionBuilders work through a process called [action composition](https://www.playframework.com/documentation/2.5.x/ScalaActionsComposition). The ActionBuilder class has a method called `invokeBlock` that takes in a `Request` and a function (also known as a block, lambda or closure) that accepts a `Request` of a given type, and produces a `Future[Result]`. +ActionBuilders work through a process called [action composition](https://www.playframework.com/documentation/latest/ScalaActionsComposition). The ActionBuilder class has a method called `invokeBlock` that takes in a `Request` and a function (also known as a block, lambda or closure) that accepts a `Request` of a given type, and produces a `Future[Result]`. -So, if you want to work with an `Action` that has a "FooRequest" that has a Foo attached, it's easy: +So, if you want to work with an `Action` that has a "FooRequest" that has a Foo attached, it's easy: ```scala class FooRequest[A](request: Request[A], val foo: Foo) extends WrappedRequest(request) -class FooAction extends ActionBuilder[FooRequest] { +class FooAction @Inject()(parsers: PlayBodyParsers)(implicit val executionContext: ExecutionContext) extends ActionBuilder[FooRequest, AnyContent] { + type FooRequestBlock[A] = FooRequest[A] => Future[Result] - override def invokeBlock[A](request: Request[A], block: FooRequestBlock[A]) = { - block(new FooRequest[A](request, new Foo)) + override def parser: BodyParser[AnyContent] = parsers.defaultBodyParser + + override def invokeBlock[A](request: Request[A], block: FooRequestBlock[A]): Future[Result] = { + block(new FooRequest[A](request, Foo())) } } ``` -You create an `ActionBuilder[FooRequest]`, override `invokeBlock`, and then call the function with an instance of `FooRequest`. +You create an `ActionBuilder[FooRequest, AnyContent]`, override `invokeBlock`, and then call the function with an instance of `FooRequest`. Then, when you call `fooAction`, the request type is `FooRequest`: ```scala -fooAction { request: FooRequest => +fooAction { request: FooRequest => Ok(request.foo.toString) } ``` @@ -302,29 +302,37 @@ And `request.foo` will be added automatically. You can keep composing action builders inside each other, so you don't have to layer all the functionality in one single ActionBuilder, or you can create a custom `ActionBuilder` for each package you work with, according to your taste. For the purposes of this blog post, we'll keep everything together in a single class. -You can see PostAction [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostAction.scala): +You can see `PostAction` builder [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostActionBuilder.scala#L49-L78): ```scala -class PostRequest[A](request: Request[A], - val messages: Messages) - extends WrappedRequest(request) +trait PostRequestHeader extends MessagesRequestHeader with PreferredMessagesProvider +class PostRequest[A](request: Request[A], val messagesApi: MessagesApi) extends WrappedRequest(request) with PostRequestHeader + +class PostActionBuilder @Inject()(messagesApi: MessagesApi, playBodyParsers: PlayBodyParsers) + (implicit val executionContext: ExecutionContext) + extends ActionBuilder[PostRequest, AnyContent] + with RequestMarkerContext + with HttpVerbs { + + + val parser: BodyParser[AnyContent] = playBodyParsers.anyContent -class PostAction @Inject()(messagesApi: MessagesApi) - (implicit ec: ExecutionContext) - extends ActionBuilder[PostRequest] with HttpVerbs { type PostRequestBlock[A] = PostRequest[A] => Future[Result] - private val logger = org.slf4j.LoggerFactory.getLogger(this.getClass) - override def invokeBlock[A](request: Request[A], - block: PostRequestBlock[A]) = { - if (logger.isTraceEnabled()) { - logger.trace(s"invokeBlock: request = $request") - } + private val logger = Logger(this.getClass) + + + override def invokeBlock[A](request: Request[A], + block: PostRequestBlock[A]): Future[Result] = { + // Convert to marker context and use request in block + implicit val markerContext: MarkerContext = requestHeaderToMarkerContext(request) + logger.trace(s"invokeBlock: ") + + + val future = block(new PostRequest(request, messagesApi)) - val messages = messagesApi.preferred(request) - val future = block(new PostRequest(request, messages)) future.map { result => request.method match { @@ -338,78 +346,83 @@ class PostAction @Inject()(messagesApi: MessagesApi) } ``` -`PostAction` does a couple of different things here. The first thing it does is to log the request as it comes in. Next, it pulls out the localized `Messages` for the request, and adds that to a `PostRequest` , and runs the function, returning a `Future[Result]`. +`PostAction` does a couple of different things here. The first thing it does is to log the request as it comes in. Next, it pulls out `MessagesApi` for the request, and adds that to a `PostRequest` , and runs the function, returning a `Future[Result]`. -When the future completes, we map the result so we can replace it with a slightly different result. We compare the result's method against `HttpVerbs`, and if it's a GET or HEAD, we append a Cache-Control header with a max-age directive. We need an `ExecutionContext` for `future.map` operations, so we pass in the default execution context implicitly at the top of the class. +When the future completes, we map the result so we can replace it with a slightly different result. We compare the result's method against `HttpVerbs`, and if it's a GET or HEAD, we append a `Cache-Control` header with a `max-age` directive. We need an `ExecutionContext` for `future.map` operations, so we pass in the default execution context implicitly at the top of the class. -Now that we have a `PostRequest`, we can call "request.messages" explicitly from any action in the controller, for free, and we can append information to the result after the user action has been completed. +Now that we have a `PostRequest`, we can call "request.messagesApi" explicitly from any action in the controller, for free, and we can append information to the result after the user action has been completed. ## Converting resources with PostResourceHandler The `PostResourceHandler` is responsible for converting backend data from a repository into a `PostResource`. We won't go into detail on the `PostRepository` details for now, only that it returns data in an backend-centric state. -A REST resource has information that a backend repository does not -- it knows about the operations available on the resource, and contains URI information that a single backend may not have. As such, we want to be able to change the representation that we use internally without changing the resource that we expose publicly. +A REST resource has information that a backend repository does not -- it knows about the operations available on the resource, and contains URI information that a single backend may not have. As such, we want to be able to change the representation that we use internally without changing the resource that we expose publicly. -You can see the `PostResourceHandler` [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostResourceHandler.scala): +You can see the `PostResourceHandler` [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostResourceHandler.scala#L35-L66): ```scala -class PostResourceHandler @Inject()(routerProvider: Provider[PostRouter], - postRepository: PostRepository) - (implicit ec: ExecutionContext) -{ - - def create(postInput: PostFormInput): Future[PostResource] = { - val data = PostData(PostId("999"), postInput.title, postInput.body) - postRepository.create(data).map { id => - createPostResource(data) - } - } - - def lookup(id: String): Future[Option[PostResource]] = { - val postFuture = postRepository.get(PostId(id)) - postFuture.map { maybePostData => - maybePostData.map { postData => - createPostResource(postData) - } - } - } - - def find: Future[Iterable[PostResource]] = { - postRepository.list().map { postDataList => - postDataList.map(postData => createPostResource(postData)) - } - } - - private def createPostResource(p: PostData): PostResource = { - PostResource(p.id.toString, routerProvider.get.link(p.id), p.title, p.body) - } +class PostResourceHandler @Inject()( + routerProvider: Provider[PostRouter], + postRepository: PostRepository)(implicit ec: ExecutionContext) { + + + def create(postInput: PostFormInput)(implicit mc: MarkerContext): Future[PostResource] = { + val data = PostData(PostId("999"), postInput.title, postInput.body) + // We don't actually create the post, so return what we have + postRepository.create(data).map { id => + createPostResource(data) + } + } + + + def lookup(id: String)(implicit mc: MarkerContext): Future[Option[PostResource]] = { + val postFuture = postRepository.get(PostId(id)) + postFuture.map { maybePostData => + maybePostData.map { postData => + createPostResource(postData) + } + } + } + + + def find(implicit mc: MarkerContext): Future[Iterable[PostResource]] = { + postRepository.list().map { postDataList => + postDataList.map(postData => createPostResource(postData)) + } + } + + + private def createPostResource(p: PostData): PostResource = { + PostResource(p.id.toString, routerProvider.get.link(p.id), p.title, p.body) + } } ``` - -Here, it's a straight conversion in `createPostResource`, with the only hook being that the router provides the resource's URL, since it's something that `PostData` doesn't have itself. +Here, it's a straight conversion in `createPostResource`, with the only hook being that the router provides the resource's URL, since it's something that `PostData` does not have itself. ## Rendering Content as JSON -Play handles the work of converting a `PostResource` through [Play JSON](https://www.playframework.com/documentation/2.5.x/ScalaJson). Play JSON provides a DSL that looks up the conversion for the `PostResource` singleton object, so you don't need to declare it at the use point. +Play handles the work of converting a `PostResource` through [Play JSON](https://www.playframework.com/documentation/latest/ScalaJson). Play JSON provides a DSL that looks up the conversion for the `PostResource` singleton object, so you don't need to declare it at the use point. -You can see the `PostResource` object [here](https://github.com/playframework/play-rest-api/blob/master/app/v1/post/PostResourceHandler.scala#L18): +You can see the `PostResource` object [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostResourceHandler.scala#L15-L30): ```scala object PostResource { + implicit val implicitWrites = new Writes[PostResource] { def writes(post: PostResource): JsValue = { Json.obj( "id" -> post.id, "link" -> post.link, "title" -> post.title, - "body" -> post.body) + "body" -> post.body + ) } } } ``` -Once the implicit is defined in the companion object, then it will be looked up automatically when handed an instance of the class. This means that when the controller converts to JSON, the conversion will just work, without any additional imports or setup. +Once the implicit is defined in the companion object, then it will be looked up automatically when handed an instance of the class. This means that when the controller converts to JSON, the conversion will just work, without any additional imports or setup. ```scala val json: JsValue = Json.toJson(post) From c84c827e19cda8ee581c475547fa8c2ef3a6440a Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Fri, 8 Jun 2018 13:31:52 -0400 Subject: [PATCH 58/73] Fix format issues (#115) * Correct format details * Update paradox --- docs/src/main/paradox/index.md | 10 +++++----- project/plugins.sbt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/main/paradox/index.md b/docs/src/main/paradox/index.md index d88aae9e5..403973dba 100644 --- a/docs/src/main/paradox/index.md +++ b/docs/src/main/paradox/index.md @@ -8,7 +8,7 @@ We’ll demonstrate with a "best practices" REST API. You can get source code f Download a pre-packaged bundle with this link [https://example.lightbend.com/v1/download/play-scala-rest-api-example](https://example.lightbend.com/v1/download/play-scala-rest-api-example) -### Linux/Mac +**Linux/Mac:** ```bash unzip play-scala-rest-api-example.zip @@ -16,7 +16,7 @@ cd play-scala-rest-api-example ./sbt ``` -### Windows +**Windows:** 1. Unzip the download 1. From a command line `cd` into the directory where you expanded the downloaded `zip` file and run: @@ -66,11 +66,11 @@ One thing to note here is that although this guide covers how to make a REST API For full scale microservices, you want [Lagom](http://www.lagomframework.com/), which builds on top of Play -- a microservices framework for dealing with the ["data on the outside"](https://blog.acolyer.org/2016/09/13/data-on-the-outside-versus-data-on-the-inside/) problem, set up with persistence and service APIs that ensure that the service always stays up and responsive even in the face of chaos monkeys and network partitions. -With that caveat, let's start working with Play! +With that caveat, let's @ref[start working with Play](part-1/index.md)! @@@index -* @ref[Basics](part-1/index.md) -* @ref[Appendix](appendix.md) +* [Basics](part-1/index.md) +* [Appendix](appendix.md) @@@ diff --git a/project/plugins.sbt b/project/plugins.sbt index 1af9cd7a1..0961393a9 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,7 +2,7 @@ addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.15") // sbt-paradox, used for documentation -addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.1") +addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.3") // Load testing tool: // http://gatling.io/docs/2.2.2/extensions/sbt_plugin.html From a79031668318d893d00a2f794d1f02e11fe1eb99 Mon Sep 17 00:00:00 2001 From: sullis Date: Fri, 8 Jun 2018 14:06:36 -0700 Subject: [PATCH 59/73] sbt 1.1.6 (#116) --- project/build.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/build.properties b/project/build.properties index 05313438a..d6e35076c 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.1.2 +sbt.version=1.1.6 From a63b249c9e05ab203a6fd6e9f51270f6cdfd53c0 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Sat, 9 Jun 2018 00:12:03 -0400 Subject: [PATCH 60/73] Upgrade branch 2.6.x using TemplateControl (#117) * Updated with template-control on 2018-06-08T21:15:49.147Z **build.sbt: libraryDependencies += "net.codingwell" %% "scala-guice" % "4.2.1" **build.gradle: play "net.codingwell:scala-guice_$scalaVersion:4.2.1" * Add Java 10 and 11 to Travis build --- .travis.yml | 16 +++++++++++++++- build.gradle | 2 +- build.sbt | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- scripts/script-helper | 8 ++++---- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 74cc8fd60..a79ded0eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,12 @@ scala: # When updating Scala versions, also check the excludes # in build matrix below. - 2.11.12 -- 2.12.4 +- 2.12.6 jdk: - oraclejdk8 - oraclejdk9 +- oraclejdk10 +- oraclejdk11 env: matrix: - SCRIPT=scripts/test-sbt @@ -28,6 +30,18 @@ matrix: exclude: - scala: 2.11.12 jdk: oraclejdk9 + - scala: 2.11.12 + jdk: oraclejdk10 + - scala: 2.11.12 + jdk: oraclejdk11 + allow_failures: + # We should allow failures here since Java 11 removed some modules including + # java.xml.bind which we are adding when running with Java 9+. For more details + # see http://openjdk.java.net/jeps/320 + # + # Play already has a fix for that, but it needs to be backported and released + # for 2.6.x: https://github.com/playframework/playframework/pull/8382 + - jdk: oraclejdk11 # See https://blog.travis-ci.com/2014-03-13-slack-notifications/ # created with travis encrypt command line tool diff --git a/build.gradle b/build.gradle index 5becdf167..3be21de9c 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ dependencies { play "net.logstash.logback:logstash-logback-encoder:4.11" play "com.netaporter:scala-uri_$scalaVersion:0.4.16" - play "net.codingwell:scala-guice_$scalaVersion:4.1.1" + play "net.codingwell:scala-guice_$scalaVersion:4.2.1" playTest "org.scalatestplus.play:scalatestplus-play_$scalaVersion:3.1.2" playTest "io.gatling.highcharts:gatling-charts-highcharts:$gatlingVersion" diff --git a/build.sbt b/build.sbt index 91d074a25..9f42cb3f9 100644 --- a/build.sbt +++ b/build.sbt @@ -16,7 +16,7 @@ libraryDependencies += "org.joda" % "joda-convert" % "1.9.2" libraryDependencies += "net.logstash.logback" % "logstash-logback-encoder" % "4.11" libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.16" -libraryDependencies += "net.codingwell" %% "scala-guice" % "4.1.1" +libraryDependencies += "net.codingwell" %% "scala-guice" % "4.2.1" libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2" % Test libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % gatlingVersion(scalaBinaryVersion.value) % Test diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5ce78ed19..5a17e7455 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/scripts/script-helper b/scripts/script-helper index ac39d9a51..9a2faa643 100644 --- a/scripts/script-helper +++ b/scripts/script-helper @@ -5,9 +5,9 @@ set -o pipefail java_version=$(java -version 2>&1 | java -version 2>&1 | awk -F '"' '/version/ {print $2}') -if [[ $java_version = 9* ]] ; then - echo "The build is using Java 9 ($java_version). We need additional JVM parameters" - export _JAVA_OPTIONS="$_JAVA_OPTIONS --add-modules=java.xml.bind" +if [[ $java_version = 1.8* ]] ; then + echo "The build is using Java 8 ($java_version). No addional JVM params needed." else - echo "The build is NOT using Java 9 ($java_version). No addional JVM params needed." + echo "The build is using Java 9+ ($java_version). We need additional JVM parameters" + export _JAVA_OPTIONS="$_JAVA_OPTIONS --add-modules=java.xml.bind" fi From c64d528f1e69eafda1b57178e601fea68135567d Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Mon, 16 Jul 2018 15:57:26 -0400 Subject: [PATCH 61/73] Updated with template-control on 2018-07-16T18:38:45.676Z (#118) **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.16") **build.gradle: def playVersion = "2.6.16" --- build.gradle | 2 +- project/plugins.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 3be21de9c..8e834e7ef 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id "com.github.lkishalmi.gatling" version "0.7.1" } -def playVersion = "2.6.15" +def playVersion = "2.6.16" def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") def gatlingVersion = "2.3.0" diff --git a/project/plugins.sbt b/project/plugins.sbt index 0961393a9..31cdc1e64 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.15") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.16") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.3") From 9caf0fa300d72c65b4ecabb8d977716075799db8 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 19 Jul 2018 12:26:30 -0400 Subject: [PATCH 62/73] Updated with template-control on 2018-07-19T01:58:56.043Z (#119) **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.17") **build.gradle: def playVersion = "2.6.17" --- build.gradle | 2 +- project/plugins.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 8e834e7ef..a6437fb12 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id "com.github.lkishalmi.gatling" version "0.7.1" } -def playVersion = "2.6.16" +def playVersion = "2.6.17" def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") def gatlingVersion = "2.3.0" diff --git a/project/plugins.sbt b/project/plugins.sbt index 31cdc1e64..f6480b3c0 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.16") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.17") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.3") From 6f46422ac71fdc31dfdb88268c3f5ab3c58c21eb Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Mon, 20 Aug 2018 19:38:26 -0400 Subject: [PATCH 63/73] Updated with template-control on 2018-08-20T20:37:13.956Z (#121) **/build.properties: sbt.version=1.2.1 **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.18") **build.gradle: def playVersion = "2.6.18" **gradle/wrapper/gradle-wrapper.properties: distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- project/build.properties | 2 +- project/plugins.sbt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index a6437fb12..892440725 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id "com.github.lkishalmi.gatling" version "0.7.1" } -def playVersion = "2.6.17" +def playVersion = "2.6.18" def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") def gatlingVersion = "2.3.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5a17e7455..89dba2d9d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStorePath=wrapper/dists diff --git a/project/build.properties b/project/build.properties index d6e35076c..5620cc502 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.1.6 +sbt.version=1.2.1 diff --git a/project/plugins.sbt b/project/plugins.sbt index f6480b3c0..524d2ec10 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.17") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.18") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.3") From 1f51164bdf94b77e2d8eea2e3adde13279f87258 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 11 Sep 2018 18:09:32 -0400 Subject: [PATCH 64/73] Updated with template-control on 2018-09-11T20:14:50.615Z (#122) **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.19") **build.gradle: def playVersion = "2.6.19" --- build.gradle | 2 +- project/plugins.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 892440725..9a379b491 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id "com.github.lkishalmi.gatling" version "0.7.1" } -def playVersion = "2.6.18" +def playVersion = "2.6.19" def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") def gatlingVersion = "2.3.0" diff --git a/project/plugins.sbt b/project/plugins.sbt index 524d2ec10..a31dc0ba0 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.18") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.19") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.3") From 2a78f6a52d16c2793ff13ee27d59ec0b18158fcd Mon Sep 17 00:00:00 2001 From: Renato Cavalcanti Date: Mon, 8 Oct 2018 21:11:41 +0200 Subject: [PATCH 65/73] Updated with template-control on 2018-10-08T19:11:41.384Z **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.20") **build.gradle: def playVersion = "2.6.20" --- build.gradle | 2 +- project/plugins.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9a379b491..599e26c9d 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id "com.github.lkishalmi.gatling" version "0.7.1" } -def playVersion = "2.6.19" +def playVersion = "2.6.20" def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") def gatlingVersion = "2.3.0" diff --git a/project/plugins.sbt b/project/plugins.sbt index a31dc0ba0..a5d255551 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.19") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.20") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.3") From 2ba6a5d43ea8569aa14cb296b71ade4560185365 Mon Sep 17 00:00:00 2001 From: Renato Cavalcanti Date: Thu, 6 Dec 2018 16:46:06 +0100 Subject: [PATCH 66/73] Upgrade branch 2.6.x using TemplateControl (#127) * Updated with template-control on 2018-11-29T15:50:43.780Z /.mergify.yml: wrote /.mergify.yml * Remove JDK 9 & 10 --- .mergify.yml | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ .travis.yml | 9 ++------- 2 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 .mergify.yml diff --git a/.mergify.yml b/.mergify.yml new file mode 100644 index 000000000..4a37a16dd --- /dev/null +++ b/.mergify.yml @@ -0,0 +1,57 @@ +pull_request_rules: + - name: automatic merge on CI success require review + conditions: + - status-success=continuous-integration/travis-ci/pr + - "#approved-reviews-by>=1" + - "#changes-requested-reviews-by=0" + - label=merge-when-green + - label!=block-merge + actions: + merge: + method: squash + strict: smart + + - name: automatic merge on CI success for TemplateControl + conditions: + - status-success=continuous-integration/travis-ci/pr + - label=merge-when-green + - label=template-control + - label!=block-merge + actions: + merge: + method: squash + strict: smart + + # delete any branch when already merged + # doesn't matter if marked with labels or not + - name: delete branch after merge + conditions: + - merged + actions: + delete_head_branch: {} + + # delete 'merge-when-green' label if present and merged + - name: remove label after merge + conditions: + - merged + - label=merge-when-green + actions: + label: + remove: [merge-when-green] + + # delete 'template-control' label if present and merged + - name: remove label after merge + conditions: + - merged + - label=template-control + actions: + label: + remove: [template-control] + + - name: auto add wip + conditions: + # match a few flavours of wip + - title~=^(\[wip\]( |:) |\[WIP\]( |:) |wip( |:) |WIP( |:)).* + actions: + label: + add: ["block-merge"] \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index a79ded0eb..1f02c18d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,6 @@ scala: - 2.12.6 jdk: - oraclejdk8 -- oraclejdk9 -- oraclejdk10 - oraclejdk11 env: matrix: @@ -27,11 +25,8 @@ before_cache: # Exclude some combinations from build matrix. See: # https://docs.travis-ci.com/user/customizing-the-build/#Build-Matrix matrix: + fast_finish: true exclude: - - scala: 2.11.12 - jdk: oraclejdk9 - - scala: 2.11.12 - jdk: oraclejdk10 - scala: 2.11.12 jdk: oraclejdk11 allow_failures: @@ -47,4 +42,4 @@ matrix: # created with travis encrypt command line tool notifications: slack: - secure: PskU6+VapjwI01Ty8Ya+5imNpD34hzzG4sYx1tIMvkg6F2x7JfYKDihotYVCdJbNYiPTbmOr529Iv7q+31WvMzmzD9knH9hEhCz6Ojali5FziTdUYfAOuQcgeI+tKtmHk/T6ks6I2ksFzJrRmanlhjMo+ENblIyB92kgvZWzlkX/pERjmMmvL9HH9eU3KxA2BxIRvVE6YyrhqnZJYAk0CqwIDwtKFePIMQaBJQkDBWVlryyVBDggItp1fqGEt4Zxt92eE0rkWTko3ejx0kjsiOLOqluhi8TekrZpvQJFZbIfP/2RRlv6JkItzt6BZz2iyCaQKWv6BUnUDwdjTvXfy/zriH0SSvKbxQqla0KCd8W33XAYzj8L6YvGeImsiOaviOemjdgHJLxg8MaTvB/Jyzno1S/A8qeE+wXy8++dqviAxKwGAzVpQ9KDSaKu/HVhXlPJCOO/i9Ut7O+SHlA3vSGgRTnI7GgIZgCn6FAKaeQO5ExxUzgxTsB+N+WonHlDGPxU2Jr8VAbj2jcMAOJhWYUPLxFT4JRGZeycK6SF+WOukQGFDRFL7trgDzhSDzZaj/FIuUHiv5Ih+ZkxoDTWZb3gXopNgAc/5Hhtx8YyKLZJ5G1V8+FV0/OLdFlgqRyIdrmoSDxu/EG3F/0NBpY856YhKc6zFMe/feL4RQnFFWU= \ No newline at end of file + secure: PskU6+VapjwI01Ty8Ya+5imNpD34hzzG4sYx1tIMvkg6F2x7JfYKDihotYVCdJbNYiPTbmOr529Iv7q+31WvMzmzD9knH9hEhCz6Ojali5FziTdUYfAOuQcgeI+tKtmHk/T6ks6I2ksFzJrRmanlhjMo+ENblIyB92kgvZWzlkX/pERjmMmvL9HH9eU3KxA2BxIRvVE6YyrhqnZJYAk0CqwIDwtKFePIMQaBJQkDBWVlryyVBDggItp1fqGEt4Zxt92eE0rkWTko3ejx0kjsiOLOqluhi8TekrZpvQJFZbIfP/2RRlv6JkItzt6BZz2iyCaQKWv6BUnUDwdjTvXfy/zriH0SSvKbxQqla0KCd8W33XAYzj8L6YvGeImsiOaviOemjdgHJLxg8MaTvB/Jyzno1S/A8qeE+wXy8++dqviAxKwGAzVpQ9KDSaKu/HVhXlPJCOO/i9Ut7O+SHlA3vSGgRTnI7GgIZgCn6FAKaeQO5ExxUzgxTsB+N+WonHlDGPxU2Jr8VAbj2jcMAOJhWYUPLxFT4JRGZeycK6SF+WOukQGFDRFL7trgDzhSDzZaj/FIuUHiv5Ih+ZkxoDTWZb3gXopNgAc/5Hhtx8YyKLZJ5G1V8+FV0/OLdFlgqRyIdrmoSDxu/EG3F/0NBpY856YhKc6zFMe/feL4RQnFFWU= From 629be666fecc877c53e8b323b42fca616ed9f005 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Fri, 4 Jan 2019 13:18:14 -0500 Subject: [PATCH 67/73] Updated with template-control on 2019-01-04T17:13:24.057Z (#128) /.travis.yml: wrote /.travis.yml **/build.properties: sbt.version=1.2.8 --- .travis.yml | 73 +++++++++++++++++++++------------------- project/build.properties | 2 +- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1f02c18d9..1e8c0e7c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,45 +1,50 @@ language: scala scala: -# When updating Scala versions, also check the excludes -# in build matrix below. -- 2.11.12 -- 2.12.6 -jdk: -- oraclejdk8 -- oraclejdk11 + - 2.12.8 + +before_install: + - curl -sL https://github.com/shyiko/jabba/raw/master/install.sh | bash && . ~/.jabba/jabba.sh + env: + global: + - JABBA_HOME=$HOME/.jabba matrix: - - SCRIPT=scripts/test-sbt - - SCRIPT=scripts/test-gradle -script: -- $SCRIPT -cache: - directories: - - "$HOME/.ivy2/cache" - - "$HOME/.gradle/caches" -before_cache: -- rm -rf $HOME/.ivy2/cache/com.typesafe.play/* -- rm -rf $HOME/.ivy2/cache/scala_*/sbt_*/com.typesafe.play/* -- find $HOME/.ivy2/cache -name "ivydata-*.properties" -print0 | xargs -n10 -0 rm + # There is no concise way to specify multi-dimensional build matrix: + # https://github.com/travis-ci/travis-ci/issues/1519 + - SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.8.192-12 + - SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.11.0-1 + - SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.8.192-12 + - SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.11.0-1 # Exclude some combinations from build matrix. See: # https://docs.travis-ci.com/user/customizing-the-build/#Build-Matrix matrix: fast_finish: true - exclude: - - scala: 2.11.12 - jdk: oraclejdk11 allow_failures: - # We should allow failures here since Java 11 removed some modules including - # java.xml.bind which we are adding when running with Java 9+. For more details - # see http://openjdk.java.net/jeps/320 - # - # Play already has a fix for that, but it needs to be backported and released - # for 2.6.x: https://github.com/playframework/playframework/pull/8382 - - jdk: oraclejdk11 + # Current release of Gradle still does not supports Play 2.7.x releases + # As soon as there is a release of Gradle that fixes that, we can then + # remove this allowed failure. + - env: SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.8.192-12 + - env: SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.11.0-1 + # Java 11 is still not fully supported. It is good that we are already + # testing our sample applications to better discover possible problems + # but we can allow failures here too. + - env: SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.11.0-1 + +install: + - $JABBA_HOME/bin/jabba install $TRAVIS_JDK + - unset _JAVA_OPTIONS + - export JAVA_HOME="$JABBA_HOME/jdk/$TRAVIS_JDK" && export PATH="$JAVA_HOME/bin:$PATH" && java -Xmx32m -version -# See https://blog.travis-ci.com/2014-03-13-slack-notifications/ -# created with travis encrypt command line tool -notifications: - slack: - secure: PskU6+VapjwI01Ty8Ya+5imNpD34hzzG4sYx1tIMvkg6F2x7JfYKDihotYVCdJbNYiPTbmOr529Iv7q+31WvMzmzD9knH9hEhCz6Ojali5FziTdUYfAOuQcgeI+tKtmHk/T6ks6I2ksFzJrRmanlhjMo+ENblIyB92kgvZWzlkX/pERjmMmvL9HH9eU3KxA2BxIRvVE6YyrhqnZJYAk0CqwIDwtKFePIMQaBJQkDBWVlryyVBDggItp1fqGEt4Zxt92eE0rkWTko3ejx0kjsiOLOqluhi8TekrZpvQJFZbIfP/2RRlv6JkItzt6BZz2iyCaQKWv6BUnUDwdjTvXfy/zriH0SSvKbxQqla0KCd8W33XAYzj8L6YvGeImsiOaviOemjdgHJLxg8MaTvB/Jyzno1S/A8qeE+wXy8++dqviAxKwGAzVpQ9KDSaKu/HVhXlPJCOO/i9Ut7O+SHlA3vSGgRTnI7GgIZgCn6FAKaeQO5ExxUzgxTsB+N+WonHlDGPxU2Jr8VAbj2jcMAOJhWYUPLxFT4JRGZeycK6SF+WOukQGFDRFL7trgDzhSDzZaj/FIuUHiv5Ih+ZkxoDTWZb3gXopNgAc/5Hhtx8YyKLZJ5G1V8+FV0/OLdFlgqRyIdrmoSDxu/EG3F/0NBpY856YhKc6zFMe/feL4RQnFFWU= +script: + - $SCRIPT + +before_cache: + - find $HOME/.ivy2 -name "ivydata-*.properties" -delete + - find $HOME/.sbt -name "*.lock" -delete + +cache: + directories: + - "$HOME/.ivy2/cache" + - "$HOME/.gradle/caches" + - "$HOME/.jabba/jdk" diff --git a/project/build.properties b/project/build.properties index 5620cc502..c0bab0494 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.2.1 +sbt.version=1.2.8 From ddc515f43e5dad585cae7f42930aa1e14789249a Mon Sep 17 00:00:00 2001 From: Renato Cavalcanti Date: Thu, 10 Jan 2019 21:38:47 +0100 Subject: [PATCH 68/73] Upgrade branch 2.6.x using TemplateControl (#133) * Updated with template-control on 2019-01-08T14:44:42.202Z **/plugins.sbt: addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.21") **build.gradle: def playVersion = "2.6.21" **build.gradle: play "net.codingwell:scala-guice_\$scalaVersion:4.2.1" **build.gradle: playTest "org.scalatestplus.play:scalatestplus-play_\$scalaVersion:3.1.2" **build.gradle: playTest "io.gatling.highcharts:gatling-charts-highcharts:\$gatlingVersion" **build.gradle: playTest "io.gatling:gatling-test-framework:\$gatlingVersion" * fixes wrong escaping gradle file --- build.gradle | 2 +- project/plugins.sbt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 599e26c9d..4eebc734e 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { id "com.github.lkishalmi.gatling" version "0.7.1" } -def playVersion = "2.6.20" +def playVersion = "2.6.21" def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12") def gatlingVersion = "2.3.0" diff --git a/project/plugins.sbt b/project/plugins.sbt index a5d255551..8dccdf85d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.20") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.21") // sbt-paradox, used for documentation addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.3.3") From 901204e23254c6af0a17087b78c21dd476299507 Mon Sep 17 00:00:00 2001 From: Renato Cavalcanti Date: Wed, 16 Jan 2019 13:08:46 +0100 Subject: [PATCH 69/73] Updated with template-control on 2019-01-16T12:08:46.083Z /LICENSE: wrote /LICENSE /NOTICE: wrote /NOTICE /.mergify.yml: wrote /.mergify.yml --- .mergify.yml | 10 ++--- LICENSE | 119 ++++++++++++++++++++++++++++++++++++++++++++++++--- NOTICE | 8 ++++ 3 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 NOTICE diff --git a/.mergify.yml b/.mergify.yml index 4a37a16dd..3549efd41 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -22,16 +22,13 @@ pull_request_rules: method: squash strict: smart - # delete any branch when already merged - # doesn't matter if marked with labels or not - name: delete branch after merge conditions: - merged actions: delete_head_branch: {} - # delete 'merge-when-green' label if present and merged - - name: remove label after merge + - name: remove merge-when-green label after merge conditions: - merged - label=merge-when-green @@ -39,8 +36,7 @@ pull_request_rules: label: remove: [merge-when-green] - # delete 'template-control' label if present and merged - - name: remove label after merge + - name: remove template-control label after merge conditions: - merged - label=template-control @@ -54,4 +50,4 @@ pull_request_rules: - title~=^(\[wip\]( |:) |\[WIP\]( |:) |wip( |:) |WIP( |:)).* actions: label: - add: ["block-merge"] \ No newline at end of file + add: ["block-merge"] diff --git a/LICENSE b/LICENSE index b018ae2bc..670154e35 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,116 @@ -License -------- -Written in 2016 by Lightbend +CC0 1.0 Universal -To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. +Statement of Purpose -You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see . +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see + diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..6d6c034d3 --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ +Written by Lightbend + +To the extent possible under law, the author(s) have dedicated all copyright and +related and neighboring rights to this software to the public domain worldwide. +This software is distributed without any warranty. + +You should have received a copy of the CC0 Public Domain Dedication along with +this software. If not, see . From 72838b5c107c99e08158ea0477bc091fa0eb46db Mon Sep 17 00:00:00 2001 From: Renato Cavalcanti Date: Thu, 17 Jan 2019 16:10:57 +0100 Subject: [PATCH 70/73] Updated with template-control on 2019-01-17T15:10:57.883Z /.mergify.yml: wrote /.mergify.yml --- .mergify.yml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index 3549efd41..b215a7709 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -4,7 +4,6 @@ pull_request_rules: - status-success=continuous-integration/travis-ci/pr - "#approved-reviews-by>=1" - "#changes-requested-reviews-by=0" - - label=merge-when-green - label!=block-merge actions: merge: @@ -15,7 +14,6 @@ pull_request_rules: conditions: - status-success=continuous-integration/travis-ci/pr - label=merge-when-green - - label=template-control - label!=block-merge actions: merge: @@ -35,19 +33,3 @@ pull_request_rules: actions: label: remove: [merge-when-green] - - - name: remove template-control label after merge - conditions: - - merged - - label=template-control - actions: - label: - remove: [template-control] - - - name: auto add wip - conditions: - # match a few flavours of wip - - title~=^(\[wip\]( |:) |\[WIP\]( |:) |wip( |:) |WIP( |:)).* - actions: - label: - add: ["block-merge"] From 9618169e487401f7e0993959f85b92e389496ccc Mon Sep 17 00:00:00 2001 From: Renato Cavalcanti Date: Fri, 1 Feb 2019 11:40:33 +0100 Subject: [PATCH 71/73] Updated with template-control on 2019-02-01T10:40:33.469Z /.mergify.yml: wrote /.mergify.yml --- .mergify.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index b215a7709..fbbe4380f 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -25,11 +25,3 @@ pull_request_rules: - merged actions: delete_head_branch: {} - - - name: remove merge-when-green label after merge - conditions: - - merged - - label=merge-when-green - actions: - label: - remove: [merge-when-green] From 3110147b54992157bd31dd134dba215dd7f9237c Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 14 Feb 2019 00:39:36 -0500 Subject: [PATCH 72/73] Updated with template-control on 2019-02-13T20:25:39.695Z (#140) /.mergify.yml: wrote /.mergify.yml --- .mergify.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index fbbe4380f..32f8689ae 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,7 +1,7 @@ pull_request_rules: - name: automatic merge on CI success require review conditions: - - status-success=continuous-integration/travis-ci/pr + - status-success=Travis CI - Pull Request - "#approved-reviews-by>=1" - "#changes-requested-reviews-by=0" - label!=block-merge @@ -12,7 +12,7 @@ pull_request_rules: - name: automatic merge on CI success for TemplateControl conditions: - - status-success=continuous-integration/travis-ci/pr + - status-success=Travis CI - Pull Request - label=merge-when-green - label!=block-merge actions: From e1b04603b7de123e7777e1157b5e4ad5328b441a Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 23 Apr 2019 15:49:58 +0100 Subject: [PATCH 73/73] Nest play-scala-rest-api-example --- .../.gitignore | 0 .../.mergify.yml | 0 .../.travis.yml | 0 LICENSE => play-scala-rest-api-example/LICENSE | 0 NOTICE => play-scala-rest-api-example/NOTICE | 0 README.md => play-scala-rest-api-example/README.md | 0 .../app}/ErrorHandler.scala | 0 .../app}/Module.scala | 0 .../app}/RequestHandler.scala | 0 .../app}/controllers/HomeController.scala | 0 .../app}/v1/post/PostActionBuilder.scala | 0 .../app}/v1/post/PostController.scala | 0 .../app}/v1/post/PostRepository.scala | 0 .../app}/v1/post/PostResourceHandler.scala | 0 .../app}/v1/post/PostRouter.scala | 0 .../app}/views/index.scala.html | 0 .../build.gradle | 0 build.sbt => play-scala-rest-api-example/build.sbt | 0 .../conf}/application.conf | 0 .../conf}/generated.keystore | Bin .../conf}/logback.xml | 0 {conf => play-scala-rest-api-example/conf}/routes | 0 .../conf}/secure.conf | 0 .../docs}/build.sbt | 0 .../docs}/src/main/paradox/appendix.md | 0 .../docs}/src/main/paradox/index.md | 0 .../docs}/src/main/paradox/part-1/index.md | 0 .../gatling}/simulation/GatlingSpec.scala | 0 .../gradle}/wrapper/gradle-wrapper.jar | Bin .../gradle}/wrapper/gradle-wrapper.properties | 0 gradlew => play-scala-rest-api-example/gradlew | 0 .../gradlew.bat | 0 .../project}/Common.scala | 0 .../project}/build.properties | 0 .../project}/plugins.sbt | 0 .../public}/stylesheets/.crunch | 0 .../public}/stylesheets/main.css | 0 .../public}/stylesheets/main.css.map | 0 .../public}/stylesheets/main.less | 0 .../scripts}/script-helper | 0 .../scripts}/test-gradle | 0 .../scripts}/test-sbt | 0 .../test}/controllers/HomeControllerSpec.scala | 0 43 files changed, 0 insertions(+), 0 deletions(-) rename .gitignore => play-scala-rest-api-example/.gitignore (100%) rename .mergify.yml => play-scala-rest-api-example/.mergify.yml (100%) rename .travis.yml => play-scala-rest-api-example/.travis.yml (100%) rename LICENSE => play-scala-rest-api-example/LICENSE (100%) rename NOTICE => play-scala-rest-api-example/NOTICE (100%) rename README.md => play-scala-rest-api-example/README.md (100%) rename {app => play-scala-rest-api-example/app}/ErrorHandler.scala (100%) rename {app => play-scala-rest-api-example/app}/Module.scala (100%) rename {app => play-scala-rest-api-example/app}/RequestHandler.scala (100%) rename {app => play-scala-rest-api-example/app}/controllers/HomeController.scala (100%) rename {app => play-scala-rest-api-example/app}/v1/post/PostActionBuilder.scala (100%) rename {app => play-scala-rest-api-example/app}/v1/post/PostController.scala (100%) rename {app => play-scala-rest-api-example/app}/v1/post/PostRepository.scala (100%) rename {app => play-scala-rest-api-example/app}/v1/post/PostResourceHandler.scala (100%) rename {app => play-scala-rest-api-example/app}/v1/post/PostRouter.scala (100%) rename {app => play-scala-rest-api-example/app}/views/index.scala.html (100%) rename build.gradle => play-scala-rest-api-example/build.gradle (100%) rename build.sbt => play-scala-rest-api-example/build.sbt (100%) rename {conf => play-scala-rest-api-example/conf}/application.conf (100%) rename {conf => play-scala-rest-api-example/conf}/generated.keystore (100%) rename {conf => play-scala-rest-api-example/conf}/logback.xml (100%) rename {conf => play-scala-rest-api-example/conf}/routes (100%) rename {conf => play-scala-rest-api-example/conf}/secure.conf (100%) rename {docs => play-scala-rest-api-example/docs}/build.sbt (100%) rename {docs => play-scala-rest-api-example/docs}/src/main/paradox/appendix.md (100%) rename {docs => play-scala-rest-api-example/docs}/src/main/paradox/index.md (100%) rename {docs => play-scala-rest-api-example/docs}/src/main/paradox/part-1/index.md (100%) rename {gatling => play-scala-rest-api-example/gatling}/simulation/GatlingSpec.scala (100%) rename {gradle => play-scala-rest-api-example/gradle}/wrapper/gradle-wrapper.jar (100%) rename {gradle => play-scala-rest-api-example/gradle}/wrapper/gradle-wrapper.properties (100%) rename gradlew => play-scala-rest-api-example/gradlew (100%) rename gradlew.bat => play-scala-rest-api-example/gradlew.bat (100%) rename {project => play-scala-rest-api-example/project}/Common.scala (100%) rename {project => play-scala-rest-api-example/project}/build.properties (100%) rename {project => play-scala-rest-api-example/project}/plugins.sbt (100%) rename {public => play-scala-rest-api-example/public}/stylesheets/.crunch (100%) rename {public => play-scala-rest-api-example/public}/stylesheets/main.css (100%) rename {public => play-scala-rest-api-example/public}/stylesheets/main.css.map (100%) rename {public => play-scala-rest-api-example/public}/stylesheets/main.less (100%) rename {scripts => play-scala-rest-api-example/scripts}/script-helper (100%) rename {scripts => play-scala-rest-api-example/scripts}/test-gradle (100%) rename {scripts => play-scala-rest-api-example/scripts}/test-sbt (100%) rename {test => play-scala-rest-api-example/test}/controllers/HomeControllerSpec.scala (100%) diff --git a/.gitignore b/play-scala-rest-api-example/.gitignore similarity index 100% rename from .gitignore rename to play-scala-rest-api-example/.gitignore diff --git a/.mergify.yml b/play-scala-rest-api-example/.mergify.yml similarity index 100% rename from .mergify.yml rename to play-scala-rest-api-example/.mergify.yml diff --git a/.travis.yml b/play-scala-rest-api-example/.travis.yml similarity index 100% rename from .travis.yml rename to play-scala-rest-api-example/.travis.yml diff --git a/LICENSE b/play-scala-rest-api-example/LICENSE similarity index 100% rename from LICENSE rename to play-scala-rest-api-example/LICENSE diff --git a/NOTICE b/play-scala-rest-api-example/NOTICE similarity index 100% rename from NOTICE rename to play-scala-rest-api-example/NOTICE diff --git a/README.md b/play-scala-rest-api-example/README.md similarity index 100% rename from README.md rename to play-scala-rest-api-example/README.md diff --git a/app/ErrorHandler.scala b/play-scala-rest-api-example/app/ErrorHandler.scala similarity index 100% rename from app/ErrorHandler.scala rename to play-scala-rest-api-example/app/ErrorHandler.scala diff --git a/app/Module.scala b/play-scala-rest-api-example/app/Module.scala similarity index 100% rename from app/Module.scala rename to play-scala-rest-api-example/app/Module.scala diff --git a/app/RequestHandler.scala b/play-scala-rest-api-example/app/RequestHandler.scala similarity index 100% rename from app/RequestHandler.scala rename to play-scala-rest-api-example/app/RequestHandler.scala diff --git a/app/controllers/HomeController.scala b/play-scala-rest-api-example/app/controllers/HomeController.scala similarity index 100% rename from app/controllers/HomeController.scala rename to play-scala-rest-api-example/app/controllers/HomeController.scala diff --git a/app/v1/post/PostActionBuilder.scala b/play-scala-rest-api-example/app/v1/post/PostActionBuilder.scala similarity index 100% rename from app/v1/post/PostActionBuilder.scala rename to play-scala-rest-api-example/app/v1/post/PostActionBuilder.scala diff --git a/app/v1/post/PostController.scala b/play-scala-rest-api-example/app/v1/post/PostController.scala similarity index 100% rename from app/v1/post/PostController.scala rename to play-scala-rest-api-example/app/v1/post/PostController.scala diff --git a/app/v1/post/PostRepository.scala b/play-scala-rest-api-example/app/v1/post/PostRepository.scala similarity index 100% rename from app/v1/post/PostRepository.scala rename to play-scala-rest-api-example/app/v1/post/PostRepository.scala diff --git a/app/v1/post/PostResourceHandler.scala b/play-scala-rest-api-example/app/v1/post/PostResourceHandler.scala similarity index 100% rename from app/v1/post/PostResourceHandler.scala rename to play-scala-rest-api-example/app/v1/post/PostResourceHandler.scala diff --git a/app/v1/post/PostRouter.scala b/play-scala-rest-api-example/app/v1/post/PostRouter.scala similarity index 100% rename from app/v1/post/PostRouter.scala rename to play-scala-rest-api-example/app/v1/post/PostRouter.scala diff --git a/app/views/index.scala.html b/play-scala-rest-api-example/app/views/index.scala.html similarity index 100% rename from app/views/index.scala.html rename to play-scala-rest-api-example/app/views/index.scala.html diff --git a/build.gradle b/play-scala-rest-api-example/build.gradle similarity index 100% rename from build.gradle rename to play-scala-rest-api-example/build.gradle diff --git a/build.sbt b/play-scala-rest-api-example/build.sbt similarity index 100% rename from build.sbt rename to play-scala-rest-api-example/build.sbt diff --git a/conf/application.conf b/play-scala-rest-api-example/conf/application.conf similarity index 100% rename from conf/application.conf rename to play-scala-rest-api-example/conf/application.conf diff --git a/conf/generated.keystore b/play-scala-rest-api-example/conf/generated.keystore similarity index 100% rename from conf/generated.keystore rename to play-scala-rest-api-example/conf/generated.keystore diff --git a/conf/logback.xml b/play-scala-rest-api-example/conf/logback.xml similarity index 100% rename from conf/logback.xml rename to play-scala-rest-api-example/conf/logback.xml diff --git a/conf/routes b/play-scala-rest-api-example/conf/routes similarity index 100% rename from conf/routes rename to play-scala-rest-api-example/conf/routes diff --git a/conf/secure.conf b/play-scala-rest-api-example/conf/secure.conf similarity index 100% rename from conf/secure.conf rename to play-scala-rest-api-example/conf/secure.conf diff --git a/docs/build.sbt b/play-scala-rest-api-example/docs/build.sbt similarity index 100% rename from docs/build.sbt rename to play-scala-rest-api-example/docs/build.sbt diff --git a/docs/src/main/paradox/appendix.md b/play-scala-rest-api-example/docs/src/main/paradox/appendix.md similarity index 100% rename from docs/src/main/paradox/appendix.md rename to play-scala-rest-api-example/docs/src/main/paradox/appendix.md diff --git a/docs/src/main/paradox/index.md b/play-scala-rest-api-example/docs/src/main/paradox/index.md similarity index 100% rename from docs/src/main/paradox/index.md rename to play-scala-rest-api-example/docs/src/main/paradox/index.md diff --git a/docs/src/main/paradox/part-1/index.md b/play-scala-rest-api-example/docs/src/main/paradox/part-1/index.md similarity index 100% rename from docs/src/main/paradox/part-1/index.md rename to play-scala-rest-api-example/docs/src/main/paradox/part-1/index.md diff --git a/gatling/simulation/GatlingSpec.scala b/play-scala-rest-api-example/gatling/simulation/GatlingSpec.scala similarity index 100% rename from gatling/simulation/GatlingSpec.scala rename to play-scala-rest-api-example/gatling/simulation/GatlingSpec.scala diff --git a/gradle/wrapper/gradle-wrapper.jar b/play-scala-rest-api-example/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from gradle/wrapper/gradle-wrapper.jar rename to play-scala-rest-api-example/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/play-scala-rest-api-example/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from gradle/wrapper/gradle-wrapper.properties rename to play-scala-rest-api-example/gradle/wrapper/gradle-wrapper.properties diff --git a/gradlew b/play-scala-rest-api-example/gradlew similarity index 100% rename from gradlew rename to play-scala-rest-api-example/gradlew diff --git a/gradlew.bat b/play-scala-rest-api-example/gradlew.bat similarity index 100% rename from gradlew.bat rename to play-scala-rest-api-example/gradlew.bat diff --git a/project/Common.scala b/play-scala-rest-api-example/project/Common.scala similarity index 100% rename from project/Common.scala rename to play-scala-rest-api-example/project/Common.scala diff --git a/project/build.properties b/play-scala-rest-api-example/project/build.properties similarity index 100% rename from project/build.properties rename to play-scala-rest-api-example/project/build.properties diff --git a/project/plugins.sbt b/play-scala-rest-api-example/project/plugins.sbt similarity index 100% rename from project/plugins.sbt rename to play-scala-rest-api-example/project/plugins.sbt diff --git a/public/stylesheets/.crunch b/play-scala-rest-api-example/public/stylesheets/.crunch similarity index 100% rename from public/stylesheets/.crunch rename to play-scala-rest-api-example/public/stylesheets/.crunch diff --git a/public/stylesheets/main.css b/play-scala-rest-api-example/public/stylesheets/main.css similarity index 100% rename from public/stylesheets/main.css rename to play-scala-rest-api-example/public/stylesheets/main.css diff --git a/public/stylesheets/main.css.map b/play-scala-rest-api-example/public/stylesheets/main.css.map similarity index 100% rename from public/stylesheets/main.css.map rename to play-scala-rest-api-example/public/stylesheets/main.css.map diff --git a/public/stylesheets/main.less b/play-scala-rest-api-example/public/stylesheets/main.less similarity index 100% rename from public/stylesheets/main.less rename to play-scala-rest-api-example/public/stylesheets/main.less diff --git a/scripts/script-helper b/play-scala-rest-api-example/scripts/script-helper similarity index 100% rename from scripts/script-helper rename to play-scala-rest-api-example/scripts/script-helper diff --git a/scripts/test-gradle b/play-scala-rest-api-example/scripts/test-gradle similarity index 100% rename from scripts/test-gradle rename to play-scala-rest-api-example/scripts/test-gradle diff --git a/scripts/test-sbt b/play-scala-rest-api-example/scripts/test-sbt similarity index 100% rename from scripts/test-sbt rename to play-scala-rest-api-example/scripts/test-sbt diff --git a/test/controllers/HomeControllerSpec.scala b/play-scala-rest-api-example/test/controllers/HomeControllerSpec.scala similarity index 100% rename from test/controllers/HomeControllerSpec.scala rename to play-scala-rest-api-example/test/controllers/HomeControllerSpec.scala