├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── core └── src │ └── main │ └── scala │ └── jp │ └── t2v │ └── lab │ └── play2 │ └── stackc │ └── StackableController.scala ├── project ├── Build.scala ├── build.properties └── plugins.sbt └── sample ├── app ├── controllers │ ├── Application.scala │ └── stack │ │ ├── DBSessionElement.scala │ │ └── LoggingElement.scala ├── models │ ├── Account.scala │ ├── Message.scala │ ├── Permission.scala │ └── package.scala └── views │ ├── index.scala.html │ ├── main.scala.html │ └── messages.scala.html ├── conf ├── application.conf └── routes ├── public ├── images │ └── favicon.png ├── javascripts │ └── jquery-1.9.0.min.js └── stylesheets │ └── main.css └── test ├── ApplicationSpec.scala └── IntegrationSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | project/target 4 | target 5 | tmp 6 | .history 7 | dist 8 | /.idea 9 | /*.iml 10 | /out 11 | /.idea_modules 12 | /.classpath 13 | /.project 14 | /RUNNING_PID 15 | /.settings 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | sudo: false 7 | 8 | scala: 9 | - 2.11.7 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Play2 Stackable Action Composition 2 | [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/t2v/stackable-controller?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 3 | 4 | This module offers Action Composition Utilities to Play2.1, 2.2, 2.3, 2.4, 2.5 applications 5 | 6 | 7 | ## Target 8 | 9 | 10 | This module targets the Scala version of Play2.x 11 | 12 | This module has been tested on Play2.5.0 13 | 14 | 15 | ## Motivation 16 | 17 | [Action Composition](http://www.playframework.com/documentation/2.4.2/ScalaActionsComposition) is somewhat limited in terms of composability. 18 | 19 | For example, imagine that we want automatic DB transaction management, auth, and pjax functionality. 20 | 21 | 22 | ```scala 23 | def TxAction(f: DBSession => Request[AnyContent] => Future[Result]): Action[AnyContent] = { 24 | Action.async { request => 25 | import TxBoundary.Future._ 26 | DB localTx { session => 27 | f(session)(request) 28 | } 29 | } 30 | } 31 | 32 | def AuthAction(authority: Authority)(f: User => DBSession => Request[AnyContent] => Future[Result]): Action[AnyContent] = { 33 | TxAction { session => request => 34 | val user: Either[Result, User] = authorized(authority)(request) 35 | user.right.map(u => f(u)(session)(request)).merge 36 | } 37 | } 38 | 39 | type Template = Html => Html 40 | def PjaxAction(authority: Authority)(f: Template => User => DBSession => Request[AnyContent] => Future[Result]): Action[AnyContent] = { 41 | AuthAction(authority) { user => session => request => 42 | val template = if (req.headers.keys("X-Pjax")) views.html.pjaxTemplate.apply else views.html.fullTemplate.apply 43 | f(template)(user)(session)(request) 44 | } 45 | } 46 | ``` 47 | 48 | ```scala 49 | def index = PjaxAction(NormalUser) { template => user => session => request => 50 | val messages = Message.findAll(session) 51 | Ok(views.hrml.index(messages)(template)) 52 | } 53 | ``` 54 | 55 | So far so good, but what if we need a new action that does both DB transaction management and pjax? 56 | 57 | We have to create another PjaxAction. 58 | 59 | ```scala 60 | def PjaxAction(f: Template => DBSession => Request[AnyContent] => Future[Result]): Action[AnyContent] = { 61 | TxAction { session => request => 62 | val template = if (req.headers.keys("X-Pjax")) html.pjaxTemplate.apply else views.html.fullTemplate.apply 63 | f(template)(session)(request) 64 | } 65 | } 66 | ``` 67 | 68 | What a mess! 69 | 70 | 71 | As an alternative, this module offers Composable Action composition using the power of traits. 72 | 73 | ## Example 74 | 75 | 1. First step, Create a sub trait of `StackableController` for every function. 76 | 77 | ```scala 78 | trait AuthElement extends StackableController with AuthConfigImpl { 79 | self: Controller with Auth => 80 | 81 | case object AuthKey extends RequestAttributeKey[User] 82 | case object AuthorityKey extends RequestAttributeKey[Authority] 83 | 84 | override def proceed[A](req: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Future[Result]): Future[Result] = { 85 | (for { 86 | authority <- req.get(AuthorityKey).toRight(authorizationFailed(req)).right 87 | user <- authorized(authority)(req).right 88 | } yield super.proceed(req.set(AuthKey, user))(f)).merge 89 | } 90 | 91 | implicit def loggedIn(implicit req: RequestWithAttributes[_]): User = req.get(AuthKey).get 92 | 93 | } 94 | ``` 95 | 96 | ```scala 97 | trait DBSessionElement extends StackableController { 98 | self: Controller => 99 | 100 | case object DBSessionKey extends RequestAttributeKey[DBSession] 101 | 102 | override def proceed[A](req: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Future[Result]): Future[Result] = { 103 | import TxBoundary.Future._ 104 | DB.localTx { session => 105 | super.proceed(req.set(DBSessionKey, session))(f) 106 | } 107 | } 108 | 109 | implicit def dbSession(implicit req: RequestWithAttributes[_]): DBSession = req.get(DBSessionKey).get 110 | 111 | } 112 | ``` 113 | 114 | ```scala 115 | trait PjaxElement extends StackableController with AuthConfigImpl { 116 | self: Controller with Auth => 117 | 118 | type Template = Html => Html 119 | 120 | case object TemplateKey extends RequestAttributeKey[Template] 121 | 122 | override def proceed[A](req: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Future[Result]): Future[Result] = { 123 | val template = if (req.headers.keys("X-Pjax")) views.html.pjaxTemplate else views.html.fullTemplate 124 | super.proceed(req.set(TemplateKey, template))(f) 125 | } 126 | 127 | implicit def template(implicit req: RequestWithAttributes[_]): Template = req.get(TemplateKey).get 128 | 129 | } 130 | ``` 131 | 132 | 2. mix your traits into your Controller 133 | 134 | ```scala 135 | object Application extends Controller with PjaxElement with AuthElement with DBSessionElement with Auth with AuthConfigImpl { 136 | 137 | def messages = StackAction(AuthorityKey -> NormalUser) { implicit req => 138 | val messages = Message.findAll 139 | Ok(html.messages(messages)(loggedIn)(template)) 140 | } 141 | 142 | def editMessage(id: MessageId) = StackAction(AuthorityKey -> Administrator) { implicit req => 143 | val messages = Message.findAll 144 | Ok(html.messages(messages)(loggedIn)(template)) 145 | } 146 | 147 | } 148 | ``` 149 | 150 | 3. Mixin different combinations of traits, depending on the functionality that you need. 151 | 152 | ```scala 153 | object NoAuthController extends Controller with PjaxElement with DBSessionElement { 154 | 155 | def messages = StackAction { implicit req => 156 | val messages = Message.findAll 157 | Ok(html.messages(messages)(GuestUser)(template)) 158 | } 159 | 160 | def editMessage(id: MessageId) = StackAction { implicit req => 161 | val messages = Message.findAll 162 | Ok(html.messages(messages)(GuestUser)(template)) 163 | } 164 | 165 | } 166 | ``` 167 | 168 | ## How to use 169 | 170 | Add a dependency declaration into your Build.scala or build.sbt file: 171 | 172 | ```scala 173 | libraryDependencies += "jp.t2v" %% "stackable-controller" % "0.6.0" 174 | ``` 175 | 176 | - for Play2.2.x, use 0.3.0 177 | - for Play2.3.x, use 0.4.1 178 | - for Play2.4.x, use 0.5.1 179 | 180 | ## ExecutionContext 181 | 182 | When you want to use `scala.concurrent.ExecutionContext`, you can use `StackActionExecutionContext` method. 183 | 184 | `StackActionExecutionContext` returns an `ExecutionContext` that is given when `StackAction` method calling. 185 | So, users of your StackElement can customize `ExecutionContext`. 186 | 187 | ```scala 188 | trait AsyncElement extends StackableController with AuthConfigImpl { 189 | self: Controller with Auth => 190 | 191 | private case object FooKey extends RequestAttributeKey[Foo] 192 | 193 | override def proceed[A](req: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Future[Result]): Future[Result] = { 194 | val ctx: ExecutionContext = StackActionExecutionContext(req) 195 | val future: Future[Foo] = getFooAsynchronously(ctx) 196 | future flatMap { 197 | foo => super.proceed(req.set(FooKey, foo))(f) 198 | } recoverWith { 199 | _ => super.proceed(req)(f) 200 | } 201 | } 202 | 203 | implicit def foo(implicit req: RequestWithAttributes[_]): Foo = req.get(FooKey).get 204 | 205 | } 206 | ``` 207 | 208 | 209 | ## License 210 | 211 | This library is released under the Apache Software License, version 2, which should be included with the source in a file named `LICENSE`. 212 | -------------------------------------------------------------------------------- /core/src/main/scala/jp/t2v/lab/play2/stackc/StackableController.scala: -------------------------------------------------------------------------------- 1 | package jp.t2v.lab.play2.stackc 2 | 3 | import play.api.mvc._ 4 | import scala.collection.concurrent.TrieMap 5 | import scala.concurrent.{Future, ExecutionContext} 6 | import scala.util.{Failure, Success} 7 | import scala.util.control.{NonFatal, ControlThrowable} 8 | 9 | trait StackableController { 10 | self: Controller => 11 | 12 | final class StackActionBuilder(params: Attribute[_]*) extends ActionBuilder[RequestWithAttributes] { 13 | def invokeBlock[A](req: Request[A], block: (RequestWithAttributes[A]) => Future[Result]): Future[Result] = { 14 | val request = new RequestWithAttributes(req, new TrieMap[RequestAttributeKey[_], Any] ++= params.map(_.toTuple)) 15 | try { 16 | cleanup(request, proceed(request)(block))(StackActionExecutionContext(request)) 17 | } catch { 18 | case e: ControlThrowable => cleanupOnSucceeded(request, None); throw e 19 | case NonFatal(e) => cleanupOnFailed(request, e); throw e 20 | } 21 | } 22 | } 23 | 24 | final def AsyncStack[A](p: BodyParser[A], params: Attribute[_]*)(f: RequestWithAttributes[A] => Future[Result]): Action[A] = new StackActionBuilder(params: _*).async(p)(f) 25 | final def AsyncStack(params: Attribute[_]*)(f: RequestWithAttributes[AnyContent] => Future[Result]): Action[AnyContent] = new StackActionBuilder(params: _*).async(f) 26 | final def AsyncStack(f: RequestWithAttributes[AnyContent] => Future[Result]): Action[AnyContent] = new StackActionBuilder().async(f) 27 | 28 | final def StackAction[A](p: BodyParser[A], params: Attribute[_]*)(f: RequestWithAttributes[A] => Result): Action[A] = new StackActionBuilder(params: _*).apply(p)(f) 29 | final def StackAction(params: Attribute[_]*)(f: RequestWithAttributes[AnyContent] => Result): Action[AnyContent] = new StackActionBuilder(params: _*).apply(f) 30 | final def StackAction(f: RequestWithAttributes[AnyContent] => Result): Action[AnyContent] = new StackActionBuilder().apply(f) 31 | 32 | def proceed[A](request: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Future[Result]): Future[Result] = f(request) 33 | 34 | def cleanupOnSucceeded[A](request: RequestWithAttributes[A], result: Option[Result]): Unit = cleanupOnSucceeded(request) 35 | 36 | def cleanupOnSucceeded[A](request: RequestWithAttributes[A]): Unit = () 37 | 38 | def cleanupOnFailed[A](request: RequestWithAttributes[A], e: Throwable): Unit = () 39 | 40 | private def cleanup[A](request: RequestWithAttributes[A], result: Future[Result])(implicit ctx: ExecutionContext): Future[Result] = result andThen { 41 | case Success(p) => cleanupOnSucceeded(request, Some(p)) 42 | case Failure(e) => cleanupOnFailed(request, e) 43 | } 44 | 45 | protected object ExecutionContextKey extends RequestAttributeKey[ExecutionContext] 46 | 47 | protected def StackActionExecutionContext(implicit req: RequestWithAttributes[_]): ExecutionContext = 48 | req.get(ExecutionContextKey).getOrElse(play.api.libs.concurrent.Execution.defaultContext) 49 | 50 | } 51 | 52 | 53 | trait RequestAttributeKey[A] { 54 | 55 | def ->(value: A): Attribute[A] = Attribute(this, value) 56 | 57 | def →(value: A): Attribute[A] = Attribute(this, value) 58 | 59 | } 60 | 61 | case class Attribute[A](key: RequestAttributeKey[A], value: A) { 62 | 63 | def toTuple: (RequestAttributeKey[A], A) = (key, value) 64 | 65 | } 66 | 67 | class RequestWithAttributes[A](underlying: Request[A], attributes: TrieMap[RequestAttributeKey[_], Any]) extends WrappedRequest[A](underlying) { 68 | 69 | def get[B](key: RequestAttributeKey[B]): Option[B] = attributes.get(key).asInstanceOf[Option[B]] 70 | 71 | /** side effect! */ 72 | def set[B](key: RequestAttributeKey[B], value: B): RequestWithAttributes[A] = { 73 | attributes.put(key, value) 74 | this 75 | } 76 | 77 | } 78 | 79 | -------------------------------------------------------------------------------- /project/Build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | import play.sbt.routes.RoutesKeys.routesGenerator 4 | import play.routes.compiler.StaticRoutesGenerator 5 | 6 | object StackableControllerProjects extends Build { 7 | 8 | lazy val _organization = "jp.t2v" 9 | 10 | lazy val _version = "0.6.0-SNAPSHOT" 11 | 12 | def _publishTo(v: String) = { 13 | val nexus = "https://oss.sonatype.org/" 14 | if (v.trim.endsWith("SNAPSHOT")) Some("snapshots" at nexus + "content/repositories/snapshots") 15 | else Some("releases" at nexus + "service/local/staging/deploy/maven2") 16 | } 17 | 18 | lazy val _resolvers = Seq( 19 | "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases", 20 | "sonatype releases" at "http://oss.sonatype.org/content/repositories/releases" 21 | ) 22 | 23 | lazy val _scalacOptions = Seq("-unchecked") 24 | 25 | lazy val _pomExtra = { 26 | https://github.com/t2v/stackable-controller 27 | 28 | 29 | Apache License, Version 2.0 30 | http://www.apache.org/licenses/LICENSE-2.0.html 31 | repo 32 | 33 | 34 | 35 | git@github.com:t2v/stackable-controller 36 | scm:git:git@github.com:t2v/stackable-controller 37 | 38 | 39 | 40 | gakuzzzz 41 | gakuzzzz 42 | https://github.com/gakuzzzz 43 | 44 | 45 | } 46 | 47 | val Scala211 = "2.11.7" 48 | 49 | lazy val core = Project( 50 | id = "core", 51 | base = file("core") 52 | ).settings( 53 | organization := _organization, 54 | name := "stackable-controller", 55 | version := _version, 56 | scalaVersion := Scala211, 57 | crossScalaVersions := Scala211 :: Nil, 58 | publishTo <<= version { (v: String) => _publishTo(v) }, 59 | publishMavenStyle := true, 60 | resolvers ++= _resolvers, 61 | libraryDependencies ++= Seq( 62 | "com.typesafe.play" %% "play" % play.core.PlayVersion.current % "provided" 63 | ), 64 | sbtPlugin := false, 65 | scalacOptions ++= _scalacOptions, 66 | publishMavenStyle := true, 67 | publishArtifact in Test := false, 68 | pomIncludeRepository := { x => false }, 69 | pomExtra := _pomExtra 70 | ) 71 | 72 | lazy val sample = Project("sample", file("sample")).enablePlugins(play.sbt.PlayScala).settings( 73 | version := _version, 74 | scalaVersion := Scala211, 75 | resolvers ++= _resolvers, 76 | routesGenerator := StaticRoutesGenerator, 77 | libraryDependencies ++= Seq( 78 | play.sbt.Play.autoImport.jdbc, 79 | play.sbt.Play.autoImport.specs2 % "test", 80 | "com.typesafe.play" %% "play" % play.core.PlayVersion.current, 81 | "org.scalikejdbc" %% "scalikejdbc" % "2.3.5", 82 | "org.scalikejdbc" %% "scalikejdbc-config" % "2.3.5", 83 | "org.scalikejdbc" %% "scalikejdbc-play-initializer" % "2.5.0", 84 | "org.slf4j" % "slf4j-simple" % "[1.7,)" 85 | ) 86 | ) dependsOn(core) 87 | 88 | lazy val root = Project(id = "root", base = file(".")).settings( 89 | scalaVersion := Scala211 90 | ).aggregate(core, sample) 91 | 92 | } 93 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.11 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.0") 2 | 3 | scalacOptions ++= Seq("-deprecation", "-language:_", "-unchecked") 4 | -------------------------------------------------------------------------------- /sample/app/controllers/Application.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api.mvc._ 4 | import models._ 5 | import views._ 6 | import controllers.stack._ 7 | import jp.t2v.lab.play2.stackc.RequestWithAttributes 8 | 9 | object Application extends Controller with DBSessionElement with LoggingElement { 10 | 11 | def index = Action { 12 | Ok(views.html.index("Your new application is ready.")) 13 | } 14 | 15 | def messages = StackAction { implicit req => 16 | val messages = Message.findAll 17 | Ok(views.html.messages(messages)) 18 | } 19 | 20 | def editMessage(id: MessageId) = StackAction { implicit req => 21 | val messages = Message.findAll 22 | Ok(views.html.messages(messages)) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /sample/app/controllers/stack/DBSessionElement.scala: -------------------------------------------------------------------------------- 1 | package controllers.stack 2 | 3 | import play.api.mvc.{Result, Controller} 4 | import scalikejdbc._ 5 | import jp.t2v.lab.play2.stackc.{RequestWithAttributes, RequestAttributeKey, StackableController} 6 | import scala.concurrent.Future 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | 9 | trait DBSessionElement extends StackableController { 10 | self: Controller => 11 | 12 | case object DBSessionKey extends RequestAttributeKey[DBSession] 13 | 14 | override def proceed[A](req: RequestWithAttributes[A])(f: RequestWithAttributes[A] => Future[Result]): Future[Result] = { 15 | import TxBoundary.Future._ 16 | DB.localTx { session => 17 | super.proceed(req.set(DBSessionKey, session))(f) 18 | } 19 | } 20 | 21 | implicit def dbSession[A](implicit req: RequestWithAttributes[A]): DBSession = req.get(DBSessionKey).get // throw 22 | 23 | } 24 | -------------------------------------------------------------------------------- /sample/app/controllers/stack/LoggingElement.scala: -------------------------------------------------------------------------------- 1 | package controllers.stack 2 | 3 | import play.api.mvc.{Result, Controller} 4 | import jp.t2v.lab.play2.stackc.{RequestWithAttributes, StackableController} 5 | import play.api.Logger 6 | 7 | trait LoggingElement extends StackableController { 8 | self: Controller => 9 | 10 | override def cleanupOnSucceeded[A](req: RequestWithAttributes[A], res: Option[Result]): Unit = { 11 | res.map { result => 12 | Logger.debug(Array(result.header.status, req.toString(), req.body).mkString("\t")) 13 | } 14 | super.cleanupOnSucceeded(req, res) 15 | } 16 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /sample/app/models/Account.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import scalikejdbc.DBSession 4 | 5 | case class Account(id: AccountId, name: String, hashedPassword: String) { 6 | 7 | } 8 | 9 | object Account { 10 | 11 | def findById(id: AccountId)(implicit session: DBSession): Option[Account] = Some(Account(1, "test", "test")) 12 | 13 | } 14 | -------------------------------------------------------------------------------- /sample/app/models/Message.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import scalikejdbc._ 4 | 5 | case class Message(id: MessageId, body: String) { 6 | 7 | } 8 | 9 | object Message { 10 | 11 | def findAll(implicit session: DBSession): Seq[Message] = Nil 12 | 13 | } 14 | -------------------------------------------------------------------------------- /sample/app/models/Permission.scala: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | sealed abstract class Permission { 4 | 5 | } 6 | 7 | case object Administrator extends Permission 8 | case object NormalUser extends Permission 9 | -------------------------------------------------------------------------------- /sample/app/models/package.scala: -------------------------------------------------------------------------------- 1 | package object models { 2 | 3 | type AccountId = Int 4 | type MessageId = Int 5 | 6 | } 7 | -------------------------------------------------------------------------------- /sample/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 | @main("Welcome to Play 2.1") { 4 | 5 | @play20.welcome(message) 6 | 7 | } 8 | -------------------------------------------------------------------------------- /sample/app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | @content 14 | 15 | 16 | -------------------------------------------------------------------------------- /sample/app/views/messages.scala.html: -------------------------------------------------------------------------------- 1 | @(messages: Seq[Message]) 2 | 3 | @main("Messages") { 4 | 5 | 10 | 11 | } 12 | -------------------------------------------------------------------------------- /sample/conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # If you deploy your application to several instances be sure to use the same key! 8 | application.secret="gC7RO7Rj@L_bpUx9l8i]OMNo/hyVp0<@_g[_3CuHt50&&t-1 in e)}function r(e){var t=Tt[e]={};return st.each(e.match(lt)||[],function(e,n){t[n]=!0}),t}function i(e,n,r,i){if(st.acceptData(e)){var o,a,s=st.expando,u="string"==typeof n,l=e.nodeType,c=l?st.cache:e,f=l?e[s]:e[s]&&s;if(f&&c[f]&&(i||c[f].data)||!u||r!==t)return f||(l?e[s]=f=K.pop()||st.guid++:f=s),c[f]||(c[f]={},l||(c[f].toJSON=st.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[f]=st.extend(c[f],n):c[f].data=st.extend(c[f].data,n)),o=c[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[st.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[st.camelCase(n)])):a=o,a}}function o(e,t,n){if(st.acceptData(e)){var r,i,o,a=e.nodeType,u=a?st.cache:e,l=a?e[st.expando]:st.expando;if(u[l]){if(t&&(r=n?u[l]:u[l].data)){st.isArray(t)?t=t.concat(st.map(t,st.camelCase)):t in r?t=[t]:(t=st.camelCase(t),t=t in r?[t]:t.split(" "));for(i=0,o=t.length;o>i;i++)delete r[t[i]];if(!(n?s:st.isEmptyObject)(r))return}(n||(delete u[l].data,s(u[l])))&&(a?st.cleanData([e],!0):st.support.deleteExpando||u!=u.window?delete u[l]:u[l]=null)}}}function a(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(Nt,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:wt.test(r)?st.parseJSON(r):r}catch(o){}st.data(e,n,r)}else r=t}return r}function s(e){var t;for(t in e)if(("data"!==t||!st.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function u(){return!0}function l(){return!1}function c(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function f(e,t,n){if(t=t||0,st.isFunction(t))return st.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return st.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=st.grep(e,function(e){return 1===e.nodeType});if(Wt.test(t))return st.filter(t,r,!n);t=st.filter(t,r)}return st.grep(e,function(e){return st.inArray(e,t)>=0===n})}function p(e){var t=zt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function d(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function h(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function g(e){var t=nn.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function m(e,t){for(var n,r=0;null!=(n=e[r]);r++)st._data(n,"globalEval",!t||st._data(t[r],"globalEval"))}function y(e,t){if(1===t.nodeType&&st.hasData(e)){var n,r,i,o=st._data(e),a=st._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)st.event.add(t,n,s[n][r])}a.data&&(a.data=st.extend({},a.data))}}function v(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!st.support.noCloneEvent&&t[st.expando]){r=st._data(t);for(i in r.events)st.removeEvent(t,i,r.handle);t.removeAttribute(st.expando)}"script"===n&&t.text!==e.text?(h(t).text=e.text,g(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),st.support.html5Clone&&e.innerHTML&&!st.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Zt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function b(e,n){var r,i,o=0,a=e.getElementsByTagName!==t?e.getElementsByTagName(n||"*"):e.querySelectorAll!==t?e.querySelectorAll(n||"*"):t;if(!a)for(a=[],r=e.childNodes||e;null!=(i=r[o]);o++)!n||st.nodeName(i,n)?a.push(i):st.merge(a,b(i,n));return n===t||n&&st.nodeName(e,n)?st.merge([e],a):a}function x(e){Zt.test(e.type)&&(e.defaultChecked=e.checked)}function T(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Nn.length;i--;)if(t=Nn[i]+n,t in e)return t;return r}function w(e,t){return e=t||e,"none"===st.css(e,"display")||!st.contains(e.ownerDocument,e)}function N(e,t){for(var n,r=[],i=0,o=e.length;o>i;i++)n=e[i],n.style&&(r[i]=st._data(n,"olddisplay"),t?(r[i]||"none"!==n.style.display||(n.style.display=""),""===n.style.display&&w(n)&&(r[i]=st._data(n,"olddisplay",S(n.nodeName)))):r[i]||w(n)||st._data(n,"olddisplay",st.css(n,"display")));for(i=0;o>i;i++)n=e[i],n.style&&(t&&"none"!==n.style.display&&""!==n.style.display||(n.style.display=t?r[i]||"":"none"));return e}function C(e,t,n){var r=mn.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function k(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=st.css(e,n+wn[o],!0,i)),r?("content"===n&&(a-=st.css(e,"padding"+wn[o],!0,i)),"margin"!==n&&(a-=st.css(e,"border"+wn[o]+"Width",!0,i))):(a+=st.css(e,"padding"+wn[o],!0,i),"padding"!==n&&(a+=st.css(e,"border"+wn[o]+"Width",!0,i)));return a}function E(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=ln(e),a=st.support.boxSizing&&"border-box"===st.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=un(e,t,o),(0>i||null==i)&&(i=e.style[t]),yn.test(i))return i;r=a&&(st.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+k(e,t,n||(a?"border":"content"),r,o)+"px"}function S(e){var t=V,n=bn[e];return n||(n=A(e,t),"none"!==n&&n||(cn=(cn||st("