├── .gitignore ├── LICENSE ├── README.md ├── play-mvc-library ├── build.sbt ├── project │ ├── build.properties │ └── plugins.sbt └── src │ ├── main │ └── scala │ │ └── play │ │ ├── Binders.scala │ │ ├── Controller.scala │ │ ├── Core.scala │ │ ├── Http.scala │ │ ├── Mvc.scala │ │ ├── PlayAppEngineServlet.scala │ │ ├── StandardValues.scala │ │ ├── UriEncoding.scala │ │ ├── core │ │ └── parsers │ │ │ └── FormUrlEncodedParser.scala │ │ └── utils │ │ └── OrderPreserving.scala │ └── test │ └── scala │ └── play │ └── core │ └── parsers │ └── FormUrlEncodedParserSpec.scala ├── samples ├── 2048 │ ├── build.sbt │ ├── project │ │ ├── build.properties │ │ └── plugins.sbt │ └── src │ │ └── main │ │ ├── conf │ │ └── routes │ │ ├── scala │ │ └── controllers │ │ │ ├── ApplicationController.scala │ │ │ └── package.scala │ │ ├── twirl │ │ └── index.scala.html │ │ └── webapp │ │ ├── WEB-INF │ │ ├── appengine-web.xml │ │ └── web.xml │ │ ├── favicon.ico │ │ ├── javascripts │ │ ├── animframe_polyfill.js │ │ ├── application.js │ │ ├── bind_polyfill.js │ │ ├── classlist_polyfill.js │ │ ├── game_manager.js │ │ ├── grid.js │ │ ├── html_actuator.js │ │ ├── keyboard_input_manager.js │ │ ├── local_storage_manager.js │ │ └── tile.js │ │ ├── meta │ │ ├── apple-touch-icon.png │ │ ├── apple-touch-startup-image-640x1096.png │ │ └── apple-touch-startup-image-640x920.png │ │ └── stylesheets │ │ ├── fonts │ │ ├── ClearSans-Bold-webfont.eot │ │ ├── ClearSans-Bold-webfont.svg │ │ ├── ClearSans-Bold-webfont.woff │ │ ├── ClearSans-Light-webfont.eot │ │ ├── ClearSans-Light-webfont.svg │ │ ├── ClearSans-Light-webfont.woff │ │ ├── ClearSans-Regular-webfont.eot │ │ ├── ClearSans-Regular-webfont.svg │ │ ├── ClearSans-Regular-webfont.woff │ │ └── clear-sans.css │ │ ├── helpers.scss │ │ ├── main.css │ │ └── main.scss ├── basic │ ├── build.sbt │ ├── project │ │ ├── build.properties │ │ └── plugins.sbt │ └── src │ │ └── main │ │ ├── conf │ │ └── routes │ │ ├── scala │ │ └── ApplicationController.scala │ │ └── webapp │ │ └── WEB-INF │ │ ├── appengine-web.xml │ │ └── web.xml └── realtime-clock │ ├── build.sbt │ ├── project │ ├── build.properties │ └── plugins.sbt │ └── src │ └── main │ ├── conf │ └── routes │ ├── scala │ └── controllers │ │ ├── ApplicationController.scala │ │ └── package.scala │ ├── twirl │ ├── index.scala.html │ └── main.scala.html │ └── webapp │ ├── WEB-INF │ ├── appengine-web.xml │ ├── application.xml │ ├── dispatch.xml │ └── web.xml │ ├── javascripts │ └── jquery-1.7.1.min.js │ └── stylesheets │ └── main.css └── sbt-routes-plugin ├── build.sbt └── src ├── main └── scala │ ├── PlayPlugin.scala │ └── PlayRouter.scala └── test ├── resources ├── duplicateHandlers.routes └── generating.routes └── scala └── play └── routes └── RoutesCompilerSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | dist/* 6 | target/ 7 | lib_managed/ 8 | src_managed/ 9 | project/boot/ 10 | project/plugins/project/ 11 | 12 | # Scala-IDE specific 13 | .scala_dependencies 14 | 15 | .idea 16 | .idea_* -------------------------------------------------------------------------------- /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 | playframework-appengine 2 | ======================= 3 | 4 | Adapting the Play Framework's Core/MVC/Routing to work on Google App Engine 5 | 6 | ## Samples 7 | 8 | ### 2048 9 | Simple multiplayer backend for the game 2048. 10 | 11 | - [Code](https://github.com/siderakis/playframework-appengine/tree/master/samples/2048) 12 | - [Live app](http://playframework-appengine-2048.ns-labs.appspot.com) 13 | 14 | Demonstrates: 15 | 16 | - App Engine Channel API 17 | - Play Templates 18 | 19 | ### Basic Routing 20 | 21 | Demonstrates: 22 | 23 | - App Engine URL Fetch API 24 | - Async Action 25 | 26 | 27 | 28 | ## Status 29 | 30 | 1. Route Plugin (done) 31 | 2. MVC, Action, Request, Response API ported to Servlets 2.5 (basics working) 32 | 3. Reverse Routing (working) 33 | 34 | Scala 2.10 35 | Sbt 0.13 36 | Play 2.3 37 | 38 | ## Getting started 39 | 40 | ### App Engine SDK 41 | 42 | Download and install the App Engine SDK, then add the following to your `bash.rc`: 43 | 44 | export APPENGINE_SDK_HOME=go~/Applications/appengine-java-sdk-1.9.0 45 | 46 | 47 | ### Adding dependencies 48 | 49 | In `build.sbt`: 50 | 51 | resolvers += "Scala AppEngine Sbt Repo" at "http://siderakis.github.com/maven" 52 | 53 | import play.PlayProject 54 | 55 | libraryDependencies ++= Seq( 56 | "com.siderakis" %% "futuraes" % "0.1-SNAPSHOT", 57 | "com.siderakis" %% "playframework-appengine-mvc" % "0.1-SNAPSHOT", 58 | "javax.servlet" % "servlet-api" % "2.5" % "provided", 59 | "org.mortbay.jetty" % "jetty" % "6.1.22" % "container" 60 | ) 61 | 62 | appengineSettings 63 | 64 | PlayProject.defaultPlaySettings 65 | 66 | 67 | In `project/plugins.sbt`: 68 | 69 | resolvers += "Scala AppEngine Sbt Repo" at "http://siderakis.github.com/maven" 70 | 71 | addSbtPlugin("com.siderakis" %% "playframework-appengine-routes" % "0.1-SNAPSHOT") 72 | 73 | addSbtPlugin("com.eed3si9n" % "sbt-appengine" % "0.6.0") 74 | 75 | 76 | ### Controller 77 | 78 | 79 | object SimpleController extends Controller { 80 | 81 | def index = Action { 82 | Ok("So Simple!") 83 | } 84 | 85 | def hello(name: String) = Action { 86 | Ok("Hello " + name) 87 | } 88 | 89 | } 90 | 91 | 92 | ### Write Routes file 93 | 94 | GET / controllers.SimpleController.index 95 | GET /hello/:name controllers.SimpleController.hello(name: String) 96 | 97 | 98 | ### Update web.xml 99 | 100 | 101 | Play 102 | play.PlayAppEngineServlet 103 | 104 | 105 | Play 106 | /* 107 | 108 | 109 | 110 | ## Implementation Details 111 | 112 | I decided not to include `BodyParsers` and `Action[A]` has been simplified to `Action` that operates on String content only. I may inlcude BodyParsers later depending on how many dependencies there are. 113 | 114 | This port should work in any Servlet 2.5 envoirment, not just App Engine. 115 | 116 | #### Also check out these related scala projects that work great on App Engine: 117 | 118 | [Twirl](https://github.com/spray/twirl) The Play framework Scala **template engine**, stand-alone and packaged as an SBT plugin 119 | 120 | [Play-Json-Standalone](https://github.com/mandubian/play-json-alone) Plays amazing JSON API, as a stand-alone library. 121 | 122 | 123 | -------------------------------------------------------------------------------- /play-mvc-library/build.sbt: -------------------------------------------------------------------------------- 1 | sbtPlugin := false 2 | 3 | organization := "com.siderakis" 4 | 5 | name := "playframework-appengine-mvc" 6 | 7 | version := "0.2-SNAPSHOT" 8 | 9 | scalaVersion := "2.10.2" 10 | 11 | resolvers += "Scala AppEngine Sbt Repo" at "http://siderakis.github.com/maven" 12 | 13 | libraryDependencies ++= Seq( 14 | "javax.servlet" % "servlet-api" % "2.5" % "provided", 15 | "org.specs2" %% "specs2" % "2.3.10" % "test" 16 | ) 17 | 18 | publishMavenStyle := true 19 | 20 | publishTo := Some(Resolver.file("Local", Path.userHome / "siderakis.github.com" / "maven" asFile)(Patterns(true, Resolver.mavenStyleBasePattern))) -------------------------------------------------------------------------------- /play-mvc-library/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.0-RC5 2 | -------------------------------------------------------------------------------- /play-mvc-library/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.10.3" -------------------------------------------------------------------------------- /play-mvc-library/src/main/scala/play/Binders.scala: -------------------------------------------------------------------------------- 1 | package play.api.mvc 2 | 3 | import scala.annotation._ 4 | 5 | import play.api.mvc._ 6 | 7 | import java.net.{ URLEncoder, URLDecoder } 8 | import java.util.UUID 9 | import scala.annotation._ 10 | 11 | import scala.collection.JavaConverters._ 12 | import reflect.ClassTag 13 | import scala.Some 14 | 15 | /** 16 | * Binder for query string parameters. 17 | * 18 | * You can provide an implementation of `QueryStringBindable[A]` for any type `A` you want to be able to 19 | * bind directly from the request query string. 20 | * 21 | * For example, if you have the following type to encode pagination: 22 | * 23 | * {{{ 24 | * /** 25 | * * @param index Current page index 26 | * * @param size Number of items in a page 27 | * */ 28 | * case class Pager(index: Int, size: Int) 29 | * }}} 30 | * 31 | * Play will create a `Pager(5, 42)` value from a query string looking like `/foo?p.index=5&p.size=42` if you define 32 | * an instance of `QueryStringBindable[Pager]` available in the implicit scope. 33 | * 34 | * For example: 35 | * 36 | * {{{ 37 | * object Pager { 38 | * implicit def queryStringBinder(implicit intBinder: QueryStringBindable[Int]) = new QueryStringBindable[Pager] { 39 | * override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, Pager]] = { 40 | * for { 41 | * index <- intBinder.bind(key + ".index", params) 42 | * size <- intBinder.bind(key + ".size", params) 43 | * } yield { 44 | * (index, size) match { 45 | * case (Right(index), Right(size)) => Right(Pager(index, size)) 46 | * case _ => Left("Unable to bind a Pager") 47 | * } 48 | * } 49 | * } 50 | * override def unbind(key: String, pager: Pager): String = { 51 | * intBinder.unbind(key + ".index", pager.index) + "&" + intBinder.unbind(key + ".size", pager.size) 52 | * } 53 | * } 54 | * } 55 | * }}} 56 | * 57 | * To use it in a route, just write a type annotation aside the parameter you want to bind: 58 | * 59 | * {{{ 60 | * GET /foo controllers.foo(p: Pager) 61 | * }}} 62 | */ 63 | @implicitNotFound( 64 | "No QueryString binder found for type ${A}. Try to implement an implicit QueryStringBindable for this type." 65 | ) 66 | trait QueryStringBindable[A] { 67 | self => 68 | 69 | /** 70 | * Bind a query string parameter. 71 | * 72 | * @param key Parameter key 73 | * @param params QueryString data 74 | * @return `None` if the parameter was not present in the query string data. Otherwise, returns `Some` of either 75 | * `Right` of the parameter value, or `Left` of an error message if the binding failed. 76 | */ 77 | def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, A]] 78 | 79 | /** 80 | * Unbind a query string parameter. 81 | * 82 | * @param key Parameter key 83 | * @param value Parameter value. 84 | * @return a query string fragment containing the key and its value. E.g. "foo=42" 85 | */ 86 | def unbind(key: String, value: A): String 87 | 88 | /** 89 | * Javascript function to unbind in the Javascript router. 90 | */ 91 | def javascriptUnbind: String = """function(k,v) {return encodeURIComponent(k)+'='+encodeURIComponent(v)}""" 92 | 93 | /** 94 | * Transform this QueryStringBindable[A] to QueryStringBindable[B] 95 | */ 96 | def transform[B](toB: A => B, toA: B => A) = new QueryStringBindable[B] { 97 | def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, B]] = { 98 | self.bind(key, params).map(_.right.map(toB)) 99 | } 100 | def unbind(key: String, value: B): String = self.unbind(key, toA(value)) 101 | } 102 | } 103 | 104 | /** 105 | * Binder for URL path parameters. 106 | * 107 | * You can provide an implementation of `PathBindable[A]` for any type `A` you want to be able to 108 | * bind directly from the request path. 109 | * 110 | * For example, given this class definition: 111 | * 112 | * {{{ 113 | * case class User(id: Int, name: String, age: Int) 114 | * }}} 115 | * 116 | * You can define a binder retrieving a `User` instance from its id, useable like the following: 117 | * 118 | * {{{ 119 | * // In your routes: 120 | * // GET /show/:user controllers.Application.show(user) 121 | * // For example: /show/42 122 | * 123 | * object Application extends Controller { 124 | * def show(user: User) = Action { 125 | * … 126 | * } 127 | * } 128 | * }}} 129 | * 130 | * The definition the binder can look like the following: 131 | * 132 | * {{{ 133 | * object User { 134 | * implicit def pathBinder(implicit intBinder: PathBindable[Int]) = new PathBindable[User] { 135 | * override def bind(key: String, value: String): Either[String, User] = { 136 | * for { 137 | * id <- intBinder.bind(key, value).right 138 | * user <- User.findById(id).toRight("User not found").right 139 | * } yield user 140 | * } 141 | * override def unbind(key: String, user: User): String = { 142 | * intBinder.unbind(user.id) 143 | * } 144 | * } 145 | * } 146 | * }}} 147 | */ 148 | @implicitNotFound( 149 | "No URL path binder found for type ${A}. Try to implement an implicit PathBindable for this type." 150 | ) 151 | trait PathBindable[A] { 152 | self => 153 | 154 | /** 155 | * Bind an URL path parameter. 156 | * 157 | * @param key Parameter key 158 | * @param value The value as String (extracted from the URL path) 159 | * @return `Right` of the value or `Left` of an error message if the binding failed 160 | */ 161 | def bind(key: String, value: String): Either[String, A] 162 | 163 | /** 164 | * Unbind a URL path parameter. 165 | * 166 | * @param key Parameter key 167 | * @param value Parameter value. 168 | */ 169 | def unbind(key: String, value: A): String 170 | 171 | /** 172 | * Javascript function to unbind in the Javascript router. 173 | */ 174 | def javascriptUnbind: String = """function(k,v) {return v}""" 175 | 176 | /** 177 | * Transform this PathBinding[A] to PathBinding[B] 178 | */ 179 | def transform[B](toB: A => B, toA: B => A) = new PathBindable[B] { 180 | def bind(key: String, value: String): Either[String, B] = self.bind(key, value).right.map(toB) 181 | def unbind(key: String, value: B): String = self.unbind(key, toA(value)) 182 | } 183 | } 184 | 185 | /** 186 | * Transform a value to a Javascript literal. 187 | */ 188 | @implicitNotFound( 189 | "No JavaScript litteral binder found for type ${A}. Try to implement an implicit JavascriptLitteral for this type." 190 | ) 191 | trait JavascriptLitteral[A] { 192 | 193 | /** 194 | * Convert a value of A to a JavaScript literal. 195 | */ 196 | def to(value: A): String 197 | 198 | } 199 | 200 | /** 201 | * Default JavaScript literals converters. 202 | */ 203 | object JavascriptLitteral { 204 | 205 | /** 206 | * Convert a Scala String to Javascript String 207 | */ 208 | implicit def litteralString: JavascriptLitteral[String] = new JavascriptLitteral[String] { 209 | def to(value: String) = "\"" + value + "\"" 210 | } 211 | 212 | /** 213 | * Convert a Scala Int to Javascript number 214 | */ 215 | implicit def litteralInt: JavascriptLitteral[Int] = new JavascriptLitteral[Int] { 216 | def to(value: Int) = value.toString 217 | } 218 | 219 | /** 220 | * Convert a Java Integer to Javascript number 221 | */ 222 | implicit def litteralJavaInteger: JavascriptLitteral[java.lang.Integer] = new JavascriptLitteral[java.lang.Integer] { 223 | def to(value: java.lang.Integer) = value.toString 224 | } 225 | 226 | /** 227 | * Convert a Scala Long to Javascript Long 228 | */ 229 | implicit def litteralLong: JavascriptLitteral[Long] = new JavascriptLitteral[Long] { 230 | def to(value: Long) = value.toString 231 | } 232 | 233 | /** 234 | * Convert a Scala Boolean to Javascript boolean 235 | */ 236 | implicit def litteralBoolean: JavascriptLitteral[Boolean] = new JavascriptLitteral[Boolean] { 237 | def to(value: Boolean) = value.toString 238 | } 239 | 240 | /** 241 | * Convert a Scala Option to Javascript literal (use null for None) 242 | */ 243 | implicit def litteralOption[T](implicit jsl: JavascriptLitteral[T]): JavascriptLitteral[Option[T]] = new JavascriptLitteral[Option[T]] { 244 | def to(value: Option[T]) = value.map(jsl.to(_)).getOrElse("null") 245 | } 246 | 247 | } 248 | 249 | /** 250 | * Default binders for Query String 251 | */ 252 | object QueryStringBindable { 253 | 254 | class Parsing[A](parse: String => A, serialize: A => String, error: (String, Exception) => String) 255 | extends QueryStringBindable[A] { 256 | 257 | def bind(key: String, params: Map[String, Seq[String]]) = params.get(key).flatMap(_.headOption).map { p => 258 | try { 259 | Right(parse(p)) 260 | } catch { 261 | case e: Exception => Left(error(key, e)) 262 | } 263 | } 264 | def unbind(key: String, value: A) = key + "=" + serialize(value) 265 | } 266 | 267 | /** 268 | * QueryString binder for String. 269 | */ 270 | implicit def bindableString = new QueryStringBindable[String] { 271 | def bind(key: String, params: Map[String, Seq[String]]) = params.get(key).flatMap(_.headOption).map(Right(_)) // No need to URL decode from query string since netty already does that 272 | def unbind(key: String, value: String) = key + "=" + (URLEncoder.encode(value, "utf-8")) 273 | } 274 | 275 | /** 276 | * QueryString binder for Int. 277 | */ 278 | implicit object bindableInt extends Parsing[Int]( 279 | _.toInt, _.toString, (key: String, e: Exception) => "Cannot parse parameter %s as Int: %s".format(key, e.getMessage) 280 | ) 281 | 282 | /** 283 | * QueryString binder for Integer. 284 | */ 285 | implicit def bindableJavaInteger: QueryStringBindable[java.lang.Integer] = 286 | bindableInt.transform(i => i, i => i) 287 | 288 | /** 289 | * QueryString binder for Long. 290 | */ 291 | implicit object bindableLong extends Parsing[Long]( 292 | _.toLong, _.toString, (key: String, e: Exception) => "Cannot parse parameter %s as Long: %s".format(key, e.getMessage) 293 | ) 294 | 295 | /** 296 | * QueryString binder for Java Long. 297 | */ 298 | implicit def bindableJavaLong: QueryStringBindable[java.lang.Long] = 299 | bindableLong.transform(l => l, l => l) 300 | 301 | /** 302 | * QueryString binder for Double. 303 | */ 304 | implicit object bindableDouble extends Parsing[Double]( 305 | _.toDouble, _.toString, (key: String, e: Exception) => "Cannot parse parameter %s as Double: %s".format(key, e.getMessage) 306 | ) 307 | 308 | /** 309 | * QueryString binder for Java Double. 310 | */ 311 | implicit def bindableJavaDouble: QueryStringBindable[java.lang.Double] = 312 | bindableDouble.transform(d => d, d => d) 313 | 314 | /** 315 | * QueryString binder for Float. 316 | */ 317 | implicit object bindableFloat extends Parsing[Float]( 318 | _.toFloat, _.toString, (key: String, e: Exception) => "Cannot parse parameter %s as Float: %s".format(key, e.getMessage) 319 | ) 320 | 321 | /** 322 | * QueryString binder for Java Float. 323 | */ 324 | implicit def bindableJavaFloat: QueryStringBindable[java.lang.Float] = 325 | bindableFloat.transform(f => f, f => f) 326 | 327 | /** 328 | * QueryString binder for Boolean. 329 | */ 330 | implicit object bindableBoolean extends Parsing[Boolean]( 331 | _.trim match { 332 | case "true" => true 333 | case "false" => false 334 | case b => b.toInt match { 335 | case 1 => true 336 | case 0 => false 337 | } 338 | }, _.toString, 339 | (key: String, e: Exception) => "Cannot parse parameter %s as Boolean: should be true, false, 0 or 1".format(key) 340 | ) { 341 | override def javascriptUnbind = """function(k,v){return k+'='+(!!v)}""" 342 | } 343 | 344 | /** 345 | * QueryString binder for Java Boolean. 346 | */ 347 | implicit def bindableJavaBoolean: QueryStringBindable[java.lang.Boolean] = 348 | bindableBoolean.transform(b => b, b => b) 349 | 350 | /** 351 | * Path binder for java.util.UUID. 352 | */ 353 | implicit object bindableUUID extends Parsing[UUID]( 354 | UUID.fromString(_), _.toString, (key: String, e: Exception) => "Cannot parse parameter %s as UUID: %s".format(key, e.getMessage) 355 | ) 356 | 357 | /** 358 | * QueryString binder for Option. 359 | */ 360 | implicit def bindableOption[T: QueryStringBindable] = new QueryStringBindable[Option[T]] { 361 | def bind(key: String, params: Map[String, Seq[String]]) = { 362 | Some( 363 | implicitly[QueryStringBindable[T]].bind(key, params) 364 | .map(_.right.map(Some(_))) 365 | .getOrElse(Right(None))) 366 | } 367 | def unbind(key: String, value: Option[T]) = value.map(implicitly[QueryStringBindable[T]].unbind(key, _)).getOrElse("") 368 | override def javascriptUnbind = javascriptUnbindOption(implicitly[QueryStringBindable[T]].javascriptUnbind) 369 | } 370 | 371 | 372 | 373 | private def javascriptUnbindOption(jsUnbindT: String) = "function(k,v){return v!=null?(" + jsUnbindT + ")(k,v):''}" 374 | 375 | /** 376 | * QueryString binder for List 377 | */ 378 | implicit def bindableList[T: QueryStringBindable]: QueryStringBindable[List[T]] = new QueryStringBindable[List[T]] { 379 | def bind(key: String, params: Map[String, Seq[String]]) = Some(Right(bindList[T](key, params))) 380 | def unbind(key: String, values: List[T]) = unbindList(key, values) 381 | override def javascriptUnbind = javascriptUnbindList(implicitly[QueryStringBindable[T]].javascriptUnbind) 382 | } 383 | 384 | private def bindList[T: QueryStringBindable](key: String, params: Map[String, Seq[String]]): List[T] = { 385 | for { 386 | values <- params.get(key).toList 387 | rawValue <- values 388 | bound <- implicitly[QueryStringBindable[T]].bind(key, Map(key -> Seq(rawValue))) 389 | value <- bound.right.toOption 390 | } yield value 391 | } 392 | 393 | private def unbindList[T: QueryStringBindable](key: String, values: Iterable[T]): String = { 394 | (for (value <- values) yield { 395 | implicitly[QueryStringBindable[T]].unbind(key, value) 396 | }).mkString("&") 397 | } 398 | 399 | private def javascriptUnbindList(jsUnbindT: String) = "function(k,vs){var l=vs&&vs.length,r=[],i=0;for(;i A, serialize: A => String, error: (String, Exception) => String) 410 | extends PathBindable[A] { 411 | 412 | def bind(key: String, value: String): Either[String, A] = { 413 | try { 414 | Right(parse(value)) 415 | } catch { 416 | case e: Exception => Left(error(key, e)) 417 | } 418 | } 419 | def unbind(key: String, value: A): String = serialize(value) 420 | } 421 | 422 | /** 423 | * Path binder for String. 424 | */ 425 | implicit object bindableString extends Parsing[String]( 426 | (s: String) => s, (s: String) => s, (key: String, e: Exception) => "Cannot parse parameter %s as String: %s".format(key, e.getMessage) 427 | ) 428 | 429 | /** 430 | * Path binder for Int. 431 | */ 432 | implicit object bindableInt extends Parsing[Int]( 433 | _.toInt, _.toString, (key: String, e: Exception) => "Cannot parse parameter %s as Int: %s".format(key, e.getMessage) 434 | ) 435 | 436 | /** 437 | * Path binder for Java Integer. 438 | */ 439 | implicit def bindableJavaInteger: PathBindable[java.lang.Integer] = 440 | bindableInt.transform(i => i, i => i) 441 | 442 | /** 443 | * Path binder for Long. 444 | */ 445 | implicit object bindableLong extends Parsing[Long]( 446 | _.toLong, _.toString, (key: String, e: Exception) => "Cannot parse parameter %s as Long: %s".format(key, e.getMessage) 447 | ) 448 | 449 | /** 450 | * Path binder for Java Long. 451 | */ 452 | implicit def bindableJavaLong: PathBindable[java.lang.Long] = 453 | bindableLong.transform(l => l, l => l) 454 | 455 | /** 456 | * Path binder for Double. 457 | */ 458 | implicit object bindableDouble extends Parsing[Double]( 459 | _.toDouble, _.toString, (key: String, e: Exception) => "Cannot parse parameter %s as Double: %s".format(key, e.getMessage) 460 | ) 461 | 462 | /** 463 | * Path binder for Java Double. 464 | */ 465 | implicit def bindableJavaDouble: PathBindable[java.lang.Double] = 466 | bindableDouble.transform(d => d, d => d) 467 | 468 | /** 469 | * Path binder for Float. 470 | */ 471 | implicit object bindableFloat extends Parsing[Float]( 472 | _.toFloat, _.toString, (key: String, e: Exception) => "Cannot parse parameter %s as Float: %s".format(key, e.getMessage) 473 | ) 474 | 475 | /** 476 | * Path binder for Java Float. 477 | */ 478 | implicit def bindableJavaFloat: PathBindable[java.lang.Float] = 479 | bindableFloat.transform(f => f, f => f) 480 | 481 | /** 482 | * Path binder for Boolean. 483 | */ 484 | implicit object bindableBoolean extends Parsing[Boolean]( 485 | _.trim match { 486 | case "true" => true 487 | case "false" => false 488 | case b => b.toInt match { 489 | case 1 => true 490 | case 0 => false 491 | } 492 | }, _.toString, 493 | (key: String, e: Exception) => "Cannot parse parameter %s as Boolean: should be true, false, 0 or 1".format(key) 494 | ) { 495 | override def javascriptUnbind = """function(k,v){return !!v}""" 496 | } 497 | 498 | /** 499 | * Path binder for Java Boolean. 500 | */ 501 | implicit def bindableJavaBoolean: PathBindable[java.lang.Boolean] = 502 | bindableBoolean.transform(b => b, b => b) 503 | 504 | /** 505 | * Path binder for java.util.UUID. 506 | */ 507 | implicit object bindableUUID extends Parsing[UUID]( 508 | UUID.fromString(_), _.toString, (key: String, e: Exception) => "Cannot parse parameter %s as UUID: %s".format(key, e.getMessage) 509 | ) 510 | 511 | } 512 | -------------------------------------------------------------------------------- /play-mvc-library/src/main/scala/play/Controller.scala: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | 4 | import play.api.http.{HeaderNames, Status} 5 | import play.api.http.HeaderNames._ 6 | 7 | import play.api.mvc.DiscardingCookie 8 | import play.api.mvc.Cookie 9 | import scala.collection.mutable 10 | 11 | 12 | /** 13 | * Defines utility methods to generate `Action` and `Results` types. 14 | * 15 | * For example: 16 | * {{{ 17 | * object Application extends Controller { 18 | * 19 | * def hello(name:String) = Action { request => 20 | * Ok("Hello " + name) 21 | * } 22 | * 23 | * } 24 | * }}} 25 | */ 26 | trait Controller extends Results with Status with HeaderNames { 27 | 28 | 29 | } 30 | 31 | object Controller extends Controller 32 | 33 | /** Helper utilities to generate results. */ 34 | object Results extends Results { 35 | 36 | /** Empty result, i.e. nothing to send. */ 37 | case class EmptyContent() 38 | 39 | } 40 | 41 | trait Response { 42 | 43 | /** 44 | * Handles a result. 45 | * 46 | * Depending on the result type, it will be sent synchronously or asynchronously. 47 | */ 48 | def handle(result: Result): Unit 49 | 50 | } 51 | 52 | /** Helper utilities to generate results. */ 53 | trait Results { 54 | 55 | import play.api.http.Status._ 56 | import play.api.http.HeaderNames._ 57 | 58 | 59 | /** 60 | * Generates default `SimpleResult` from a content type, headers and content. 61 | * 62 | * @param status the HTTP response status, e.g ‘200 OK’ 63 | */ 64 | class Status(override val status: Int) extends SimpleResult(header = ResponseHeader(status), body = "") { 65 | 66 | 67 | /** 68 | * Set the result's content. 69 | * 70 | * @param content content to send 71 | */ 72 | def apply(content: String): Result = SimpleResult(ResponseHeader(status), content) 73 | 74 | 75 | } 76 | 77 | // def Async(promise: Future[Result]) = AsyncResult(promise) 78 | 79 | /** Generates a ‘200 OK’ result. */ 80 | val Ok = new Status(OK) 81 | 82 | /** Generates a ‘201 CREATED’ result. */ 83 | val Created = new Status(CREATED) 84 | 85 | /** Generates a ‘202 ACCEPTED’ result. */ 86 | val Accepted = new Status(ACCEPTED) 87 | 88 | /** Generates a ‘203 NON_AUTHORITATIVE_INFORMATION’ result. */ 89 | val NonAuthoritativeInformation = new Status(NON_AUTHORITATIVE_INFORMATION) 90 | 91 | /** Generates a ‘204 NO_CONTENT’ result. */ 92 | val NoContent = SimpleResult(header = ResponseHeader(NO_CONTENT), body = "") 93 | 94 | /** Generates a ‘205 RESET_CONTENT’ result. */ 95 | val ResetContent = SimpleResult(header = ResponseHeader(RESET_CONTENT), body = "") 96 | 97 | /** Generates a ‘206 PARTIAL_CONTENT’ result. */ 98 | val PartialContent = new Status(PARTIAL_CONTENT) 99 | 100 | /** Generates a ‘207 MULTI_STATUS’ result. */ 101 | val MultiStatus = new Status(MULTI_STATUS) 102 | 103 | /** 104 | * Generates a ‘301 MOVED_PERMANENTLY’ simple result. 105 | * 106 | * @param url the URL to redirect to 107 | */ 108 | def MovedPermanently(url: String): SimpleResult = Redirect(url, MOVED_PERMANENTLY) 109 | 110 | /** 111 | * Generates a ‘302 FOUND’ simple result. 112 | * 113 | * @param url the URL to redirect to 114 | */ 115 | def Found(url: String): SimpleResult = Redirect(url, FOUND) 116 | 117 | /** 118 | * Generates a ‘303 SEE_OTHER’ simple result. 119 | * 120 | * @param url the URL to redirect to 121 | */ 122 | def SeeOther(url: String): SimpleResult = Redirect(url, SEE_OTHER) 123 | 124 | /** Generates a ‘304 NOT_MODIFIED’ result. */ 125 | val NotModified = SimpleResult(header = ResponseHeader(NOT_MODIFIED), body = "") 126 | 127 | /** 128 | * Generates a ‘307 TEMPORARY_REDIRECT’ simple result. 129 | * 130 | * @param url the URL to redirect to 131 | */ 132 | def TemporaryRedirect(url: String): SimpleResult = Redirect(url, TEMPORARY_REDIRECT) 133 | 134 | /** Generates a ‘400 BAD_REQUEST’ result. */ 135 | val BadRequest = new Status(BAD_REQUEST) 136 | 137 | /** Generates a ‘401 UNAUTHORIZED’ result. */ 138 | val Unauthorized = new Status(UNAUTHORIZED) 139 | 140 | /** Generates a ‘403 FORBIDDEN’ result. */ 141 | val Forbidden = new Status(FORBIDDEN) 142 | 143 | /** Generates a ‘404 NOT_FOUND’ result. */ 144 | val NotFound = new Status(NOT_FOUND) 145 | 146 | /** Generates a ‘405 METHOD_NOT_ALLOWED’ result. */ 147 | val MethodNotAllowed = new Status(METHOD_NOT_ALLOWED) 148 | 149 | /** Generates a ‘406 NOT_ACCEPTABLE’ result. */ 150 | val NotAcceptable = new Status(NOT_ACCEPTABLE) 151 | 152 | /** Generates a ‘408 REQUEST_TIMEOUT’ result. */ 153 | val RequestTimeout = new Status(REQUEST_TIMEOUT) 154 | 155 | /** Generates a ‘409 CONFLICT’ result. */ 156 | val Conflict = new Status(CONFLICT) 157 | 158 | /** Generates a ‘410 GONE’ result. */ 159 | val Gone = new Status(GONE) 160 | 161 | /** Generates a ‘412 PRECONDITION_FAILED’ result. */ 162 | val PreconditionFailed = new Status(PRECONDITION_FAILED) 163 | 164 | /** Generates a ‘413 REQUEST_ENTITY_TOO_LARGE’ result. */ 165 | val EntityTooLarge = new Status(REQUEST_ENTITY_TOO_LARGE) 166 | 167 | /** Generates a ‘414 REQUEST_URI_TOO_LONG’ result. */ 168 | val UriTooLong = new Status(REQUEST_URI_TOO_LONG) 169 | 170 | /** Generates a ‘415 UNSUPPORTED_MEDIA_TYPE’ result. */ 171 | val UnsupportedMediaType = new Status(UNSUPPORTED_MEDIA_TYPE) 172 | 173 | /** Generates a ‘417 EXPECTATION_FAILED’ result. */ 174 | val ExpectationFailed = new Status(EXPECTATION_FAILED) 175 | 176 | /** Generates a ‘422 UNPROCESSABLE_ENTITY’ result. */ 177 | val UnprocessableEntity = new Status(UNPROCESSABLE_ENTITY) 178 | 179 | /** Generates a ‘423 LOCKED’ result. */ 180 | val Locked = new Status(LOCKED) 181 | 182 | /** Generates a ‘424 FAILED_DEPENDENCY’ result. */ 183 | val FailedDependency = new Status(FAILED_DEPENDENCY) 184 | 185 | /** Generates a ‘429 TOO_MANY_REQUEST’ result. */ 186 | val TooManyRequest = new Status(TOO_MANY_REQUEST) 187 | 188 | /** Generates a ‘500 INTERNAL_SERVER_ERROR’ result. */ 189 | val InternalServerError = new Status(INTERNAL_SERVER_ERROR) 190 | 191 | /** Generates a ‘501 NOT_IMPLEMENTED’ result. */ 192 | val NotImplemented = new Status(NOT_IMPLEMENTED) 193 | 194 | /** Generates a ‘502 BAD_GATEWAY’ result. */ 195 | val BadGateway = new Status(BAD_GATEWAY) 196 | 197 | /** Generates a ‘503 SERVICE_UNAVAILABLE’ result. */ 198 | val ServiceUnavailable = new Status(SERVICE_UNAVAILABLE) 199 | 200 | /** Generates a ‘504 GATEWAY_TIMEOUT’ result. */ 201 | val GatewayTimeout = new Status(GATEWAY_TIMEOUT) 202 | 203 | /** Generates a ‘505 HTTP_VERSION_NOT_SUPPORTED’ result. */ 204 | val HttpVersionNotSupported = new Status(HTTP_VERSION_NOT_SUPPORTED) 205 | 206 | /** Generates a ‘507 INSUFFICIENT_STORAGE’ result. */ 207 | val InsufficientStorage = new Status(INSUFFICIENT_STORAGE) 208 | 209 | /** 210 | * Generates a simple result. 211 | * 212 | * @param code the status code 213 | */ 214 | def Status(code: Int) = new Status(code) 215 | 216 | /** 217 | * Generates a redirect simple result. 218 | * 219 | * @param url the URL to redirect to 220 | * @param status HTTP status 221 | */ 222 | def Redirect(url: String, status: Int): SimpleResult = Redirect(url, Map.empty, status) 223 | 224 | /** 225 | * Generates a redirect simple result. 226 | * 227 | * @param url the URL to redirect to 228 | * @param queryString queryString parameters to add to the queryString 229 | * @param status HTTP status 230 | */ 231 | def Redirect(url: String, queryString: Map[String, Seq[String]] = Map.empty, status: Int = SEE_OTHER) = { 232 | import java.net.URLEncoder 233 | val fullUrl = url + Option(queryString).filterNot(_.isEmpty).map { 234 | params => 235 | (if (url.contains("?")) "&" else "?") + params.toSeq.flatMap { 236 | pair => 237 | pair._2.map(value => (pair._1 + "=" + URLEncoder.encode(value, "utf-8"))) 238 | }.mkString("&") 239 | }.getOrElse("") 240 | Status(status).withHeaders(LOCATION -> fullUrl) 241 | } 242 | 243 | // /** 244 | // * Generates a redirect simple result. 245 | // * 246 | // * @param call Call defining the URL to redirect to, which typically comes from the reverse router 247 | // */ 248 | // def Redirect(call: Call): SimpleResult = Redirect(call.url) 249 | 250 | } 251 | 252 | /** 253 | * A simple HTTP response header, used for standard responses. 254 | * 255 | * @param status the response status, e.g. ‘200 OK’ 256 | * @param headers the HTTP headers 257 | */ 258 | case class ResponseHeader(status: Int, headers: Map[String, String] = Map.empty) { 259 | 260 | override def toString = { 261 | status + ", " + headers 262 | } 263 | 264 | } 265 | 266 | /** 267 | * A simple result, which defines the response header and a body ready to send to the client. 268 | * 269 | * @param header the response header, which contains status code and HTTP headers 270 | * @param body the response body 271 | */ 272 | case class SimpleResult(header: ResponseHeader, body: String, cookies: mutable.Set[Cookie] = mutable.Set[Cookie]()) extends PlainResult { 273 | 274 | val status = header.status 275 | 276 | /** 277 | * Adds headers to this result. 278 | * 279 | * For example: 280 | * {{{ 281 | * Ok("Hello world").withHeaders(ETAG -> "0") 282 | * }}} 283 | * 284 | * @param headers the headers to add to this result. 285 | * @return the new result 286 | */ 287 | def withHeaders(headers: (String, String)*) = { 288 | copy(header = header.copy(headers = header.headers ++ headers)) 289 | } 290 | 291 | override def toString = { 292 | "SimpleResult(" + header + ")" 293 | } 294 | 295 | } 296 | 297 | sealed trait WithHeaders[+A <: Result] { 298 | /** 299 | * Adds HTTP headers to this result. 300 | * 301 | * For example: 302 | * {{{ 303 | * Ok("Hello world").withHeaders(ETAG -> "0") 304 | * }}} 305 | * 306 | * @param headers the headers to add to this result. 307 | * @return the new result 308 | */ 309 | def withHeaders(headers: (String, String)*): A 310 | 311 | /** 312 | * Adds cookies to this result. 313 | * 314 | * For example: 315 | * {{{ 316 | * Ok("Hello world").withCookies(Cookie("theme", "blue")) 317 | * }}} 318 | * 319 | * @param cookies the cookies to add to this result 320 | * @return the new result 321 | */ 322 | def withCookies(cookies: Cookie*): A 323 | 324 | /** 325 | * Discards cookies along this result. 326 | * 327 | * For example: 328 | * {{{ 329 | * Ok("Hello world").discardingCookies(DiscardingCookie("theme")) 330 | * }}} 331 | * 332 | * @param cookies the cookies to discard along to this result 333 | * @return the new result 334 | */ 335 | def discardingCookies(cookies: DiscardingCookie*): A 336 | 337 | 338 | /** 339 | * Changes the result content type. 340 | * 341 | * For example: 342 | * {{{ 343 | * Ok("Hello world").as("text/xml") 344 | * }}} 345 | * 346 | * @param contentType the new content type. 347 | * @return the new result 348 | */ 349 | def as(contentType: String): A 350 | } 351 | 352 | sealed trait Result extends NotNull with WithHeaders[Result] { 353 | def body: String 354 | 355 | val status: Int 356 | 357 | val cookies: mutable.Set[Cookie] 358 | 359 | val header: ResponseHeader 360 | } 361 | 362 | /** 363 | * A plain HTTP result. 364 | */ 365 | trait PlainResult extends Result with WithHeaders[PlainResult] { 366 | 367 | /** 368 | * The response header 369 | */ 370 | val header: ResponseHeader 371 | 372 | /** 373 | * Adds cookies to this result. 374 | * 375 | * For example: 376 | * {{{ 377 | * Ok("Hello world").withCookies(Cookie("theme", "blue")) 378 | * }}} 379 | * 380 | * @param cookies the cookies to add to this result 381 | * @return the new result 382 | */ 383 | def withCookies(cookies: Cookie*): PlainResult = { 384 | this.cookies ++= cookies 385 | this 386 | } 387 | 388 | /** 389 | * Discards cookies along this result. 390 | * 391 | * For example: 392 | * {{{ 393 | * Ok("Hello world").discardingCookies("theme") 394 | * }}} 395 | * 396 | * @param cookies the cookies to discard along to this result 397 | * @return the new result 398 | */ 399 | def discardingCookies(cookies: DiscardingCookie*): PlainResult = { 400 | val names = cookies.map(_.name) 401 | this.cookies.retain(c => !names.contains(c.name)) 402 | this 403 | } 404 | 405 | 406 | /** 407 | * Changes the result content type. 408 | * 409 | * For example: 410 | * {{{ 411 | * Ok("Hello world").as("text/xml") 412 | * }}} 413 | * 414 | * @param contentType the new content type. 415 | * @return the new result 416 | */ 417 | def as(contentType: String): PlainResult = withHeaders(CONTENT_TYPE -> contentType) 418 | } -------------------------------------------------------------------------------- /play-mvc-library/src/main/scala/play/Http.scala: -------------------------------------------------------------------------------- 1 | package play.api.mvc 2 | 3 | import javax.servlet.http.{HttpServletResponse, HttpServletRequest} 4 | import play.Result 5 | import play.api.http.HeaderNames 6 | import play.core.parsers.FormUrlEncodedParser 7 | import scala.io.Source 8 | 9 | 10 | trait Request extends RequestHeader { 11 | 12 | } 13 | 14 | case class HttpRequest(req: HttpServletRequest, resp: HttpServletResponse) extends RequestHeader { 15 | 16 | def path: String = req.getServletPath + Option(req.getPathInfo).getOrElse("") 17 | 18 | def method: String = req.getMethod 19 | 20 | def cookies = Option(req.getCookies).map(_.map(c => c.getName -> c.getValue).toMap) getOrElse Map() 21 | 22 | lazy val queryString: Map[String, Seq[String]] = { 23 | Option(req.getQueryString).map { 24 | _.split("&").foldLeft(Map[String, Seq[String]]().withDefaultValue(Seq.empty)) { 25 | (map, pair) => 26 | val (k, v) = pair.span(_ != '=') 27 | map.updated(k, map(k) ++ Seq(v.drop(1))) 28 | } 29 | }.getOrElse(Map()) 30 | } 31 | 32 | lazy val bodyAsFormUrlEncoded = FormUrlEncodedParser.parse(body) 33 | 34 | lazy val body = Source.fromInputStream(req.getInputStream).mkString 35 | 36 | /** 37 | * The HTTP host (domain, optionally port) 38 | */ 39 | def host: String = Option(req.getHeader(HeaderNames.HOST)).getOrElse("") 40 | 41 | /** 42 | * The HTTP domain 43 | */ 44 | lazy val domain: String = host.split(':').head 45 | 46 | } 47 | 48 | trait RequestHeader { 49 | /** 50 | * The URI path. 51 | */ 52 | def path: String 53 | 54 | /** 55 | * The HTTP method. 56 | */ 57 | def method: String 58 | 59 | def host: String 60 | 61 | /** 62 | * The parsed query string. 63 | */ 64 | def queryString: Map[String, Seq[String]] 65 | 66 | def req: HttpServletRequest 67 | 68 | def resp: HttpServletResponse 69 | 70 | } 71 | 72 | /** 73 | * Defines a `Call`, which describes an HTTP request and can be used to create links or fill redirect data. 74 | * 75 | * These values are usually generated by the reverse router. 76 | * 77 | * @param method the request HTTP method 78 | * @param url the request URL 79 | */ 80 | case class Call(method: String, url: String) { 81 | 82 | /** 83 | * Transform this call to an absolute URL. 84 | */ 85 | def absoluteURL(secure: Boolean = false)(implicit request: RequestHeader) = { 86 | "http" + (if (secure) "s" else "") + "://" + request.host + this.url 87 | } 88 | 89 | 90 | val rand = new java.util.Random() 91 | 92 | /** 93 | * Append a unique identifier to the URL. 94 | */ 95 | def unique: Call = new Call(method, if (this.url.contains('?')) this.url + "?" + rand.nextLong() else this.url + "&" + rand.nextLong()) 96 | 97 | 98 | override def toString = url 99 | 100 | } 101 | 102 | /** 103 | * An Handler handles a request. 104 | */ 105 | trait Handler { 106 | def apply(ctx: RequestHeader): Result 107 | } 108 | 109 | /** 110 | * Reference to an Handler. 111 | */ 112 | class HandlerRef(callValue: => Action, handlerDef: play.core.Router.HandlerDef)(implicit handlerInvoker: play.core.Router.HandlerInvoker) { 113 | //} extends play.mvc.HandlerRef { 114 | 115 | 116 | /** 117 | * Retrieve a real handler behind this ref. 118 | */ 119 | def handler: play.api.mvc.Handler = { 120 | handlerInvoker.call(callValue, handlerDef) 121 | } 122 | 123 | /** 124 | * String representation of this Handler. 125 | */ 126 | lazy val sym = { 127 | handlerDef.controller + "." + handlerDef.method + "(" + handlerDef.parameterTypes.map(_.getName).mkString(", ") + ")" 128 | } 129 | 130 | override def toString = { 131 | "HandlerRef[" + sym + ")]" 132 | } 133 | 134 | 135 | } 136 | 137 | 138 | /** 139 | * An HTTP cookie. 140 | * 141 | * @param name the cookie name 142 | * @param value the cookie value 143 | * @param maxAge the cookie expiration date in seconds, `None` for a transient cookie, or a value less than 0 to expire a cookie now 144 | * @param path the cookie path, defaulting to the root path `/` 145 | * @param domain the cookie domain 146 | * @param secure whether this cookie is secured, sent only for HTTPS requests 147 | * @param httpOnly whether this cookie is HTTP only, i.e. not accessible from client-side JavaScipt code 148 | */ 149 | case class Cookie(name: String, value: String, maxAge: Option[Int] = None, path: String = "/", domain: Option[String] = None, secure: Boolean = false, httpOnly: Boolean = true) 150 | 151 | /** 152 | * A cookie to be discarded. This contains only the data necessary for discarding a cookie. 153 | * 154 | * @param name the name of the cookie to discard 155 | * @param path the path of the cookie, defaults to the root path 156 | * @param domain the cookie domain 157 | * @param secure whether this cookie is secured 158 | */ 159 | case class DiscardingCookie(name: String, path: String = "/", domain: Option[String] = None, secure: Boolean = false) { 160 | def toCookie = Cookie(name, "", Some(-1), path, domain, secure) 161 | } 162 | 163 | /** 164 | * The HTTP cookies set. 165 | */ 166 | trait Cookies { 167 | 168 | /** 169 | * Optionally returns the cookie associated with a key. 170 | */ 171 | def get(name: String): Option[Cookie] 172 | 173 | /** 174 | * Retrieves the cookie that is associated with the given key. 175 | */ 176 | def apply(name: String): Cookie = get(name).getOrElse(scala.sys.error("Cookie doesn't exist")) 177 | 178 | } -------------------------------------------------------------------------------- /play-mvc-library/src/main/scala/play/Mvc.scala: -------------------------------------------------------------------------------- 1 | package play.api.mvc 2 | 3 | import play.Result 4 | import scala.concurrent.{Await, Future} 5 | import scala.concurrent.duration._ 6 | 7 | 8 | /** 9 | * Provides helpers for creating `Action` values. 10 | */ 11 | trait ActionBuilder { 12 | 13 | /** 14 | * Constructs an `Action`. 15 | * 16 | * For example: 17 | * {{{ 18 | * val echo = Action(parse.anyContent) { request => 19 | * Ok("Got request [" + request + "]") 20 | * } 21 | * }}} 22 | * 23 | * @param block the action code 24 | * @return an action 25 | */ 26 | def apply(block: RequestHeader => Result): Action = new Action { 27 | 28 | 29 | def apply(ctx: RequestHeader) = try { 30 | block(ctx) 31 | 32 | } catch { 33 | // NotImplementedError is not caught by NonFatal, wrap it 34 | case e: NotImplementedError => throw new RuntimeException(e) 35 | // LinkageError is similarly harmless in Play Framework, since automatic reloading could easily trigger it 36 | case e: LinkageError => throw new RuntimeException(e) 37 | } 38 | } 39 | 40 | /** 41 | * Constructs an `Action` with default content, and no request parameter. 42 | * 43 | * For example: 44 | * {{{ 45 | * val hello = Action { 46 | * Ok("Hello!") 47 | * } 48 | * }}} 49 | * 50 | * @param block the action code 51 | * @return an action 52 | */ 53 | def apply(block: => Result): Action = apply(_ => block) 54 | 55 | 56 | def async(block: RequestHeader => Future[Result]): Action = new Action { 57 | 58 | def apply(ctx: RequestHeader) = try { 59 | Await.result(block(ctx), 1 hour) 60 | } catch { 61 | // NotImplementedError is not caught by NonFatal, wrap it 62 | case e: NotImplementedError => throw new RuntimeException(e) 63 | // LinkageError is similarly harmless in Play Framework, since automatic reloading could easily trigger it 64 | case e: LinkageError => throw new RuntimeException(e) 65 | } 66 | } 67 | 68 | def async(block: => Future[Result]): Action = async(_ => block) 69 | 70 | 71 | } 72 | 73 | /** 74 | * Helper object to create `Action` values. 75 | */ 76 | object Action extends ActionBuilder 77 | 78 | 79 | trait Action extends Handler 80 | 81 | 82 | -------------------------------------------------------------------------------- /play-mvc-library/src/main/scala/play/PlayAppEngineServlet.scala: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import javax.servlet.http.{HttpServletResponse, HttpServletRequest, HttpServlet, Cookie} 4 | 5 | import play.api.mvc._ 6 | import play.core.Router.Routes 7 | 8 | class PlayAppEngineServlet extends HttpServlet with Results { 9 | 10 | val NotFoundPage = PartialFunction[RequestHeader, Handler] { 11 | _ => new Handler { 12 | def apply(ctx: RequestHeader): Result = NotFound 13 | } 14 | } 15 | 16 | private[this] val generatedRoute: PartialFunction[RequestHeader, Handler] = { 17 | //TODO: add error handling 18 | val name = "play.Routes" 19 | val router = Class.forName(name + "$").getField("MODULE$").get(null).asInstanceOf[Routes] 20 | router.routes 21 | } 22 | 23 | override def doGet(req: HttpServletRequest, resp: HttpServletResponse) = { 24 | val request = HttpRequest(req, resp) 25 | val handler = (generatedRoute orElse NotFoundPage)(request) 26 | val result = handler(request) 27 | resp.setCharacterEncoding("utf-8") 28 | resp.setStatus(result.status) 29 | result.cookies.map(c => new Cookie(c.name, c.value)).foreach(resp.addCookie) 30 | result.header.headers.foreach(t => resp.addHeader(t._1, t._2)) 31 | resp.getWriter.print(result.body) 32 | } 33 | 34 | override def doPost(req: HttpServletRequest, resp: HttpServletResponse) = doGet(req, resp) 35 | 36 | } -------------------------------------------------------------------------------- /play-mvc-library/src/main/scala/play/StandardValues.scala: -------------------------------------------------------------------------------- 1 | package play.api.http 2 | 3 | 4 | /** Common HTTP MIME types */ 5 | object MimeTypes extends MimeTypes 6 | 7 | /** Common HTTP MIME types */ 8 | trait MimeTypes { 9 | 10 | /** 11 | * Content-Type of text. 12 | */ 13 | val TEXT = "text/plain" 14 | 15 | /** 16 | * Content-Type of html. 17 | */ 18 | val HTML = "text/html" 19 | 20 | /** 21 | * Content-Type of json. 22 | */ 23 | val JSON = "application/json" 24 | 25 | /** 26 | * Content-Type of xml. 27 | */ 28 | val XML = "text/xml" 29 | 30 | /** 31 | * Content-Type of css. 32 | */ 33 | val CSS = "text/css" 34 | 35 | /** 36 | * Content-Type of javascript. 37 | */ 38 | val JAVASCRIPT = "text/javascript" 39 | 40 | /** 41 | * Content-Type of form-urlencoded. 42 | */ 43 | val FORM = "application/x-www-form-urlencoded" 44 | 45 | /** 46 | * Content-Type of server sent events. 47 | */ 48 | val EVENT_STREAM = "text/event-stream" 49 | 50 | /** 51 | * Content-Type of binary data. 52 | */ 53 | val BINARY = "application/octet-stream" 54 | 55 | } 56 | 57 | /** 58 | * Defines all standard HTTP Status. 59 | */ 60 | object Status extends Status 61 | 62 | /** 63 | * Defines all standard HTTP status codes. 64 | */ 65 | trait Status { 66 | 67 | val CONTINUE = 100 68 | val SWITCHING_PROTOCOLS = 101 69 | 70 | val OK = 200 71 | val CREATED = 201 72 | val ACCEPTED = 202 73 | val NON_AUTHORITATIVE_INFORMATION = 203 74 | val NO_CONTENT = 204 75 | val RESET_CONTENT = 205 76 | val PARTIAL_CONTENT = 206 77 | val MULTI_STATUS = 207 78 | 79 | val MULTIPLE_CHOICES = 300 80 | val MOVED_PERMANENTLY = 301 81 | val FOUND = 302 82 | val SEE_OTHER = 303 83 | val NOT_MODIFIED = 304 84 | val USE_PROXY = 305 85 | val TEMPORARY_REDIRECT = 307 86 | 87 | val BAD_REQUEST = 400 88 | val UNAUTHORIZED = 401 89 | val PAYMENT_REQUIRED = 402 90 | val FORBIDDEN = 403 91 | val NOT_FOUND = 404 92 | val METHOD_NOT_ALLOWED = 405 93 | val NOT_ACCEPTABLE = 406 94 | val PROXY_AUTHENTICATION_REQUIRED = 407 95 | val REQUEST_TIMEOUT = 408 96 | val CONFLICT = 409 97 | val GONE = 410 98 | val LENGTH_REQUIRED = 411 99 | val PRECONDITION_FAILED = 412 100 | val REQUEST_ENTITY_TOO_LARGE = 413 101 | val REQUEST_URI_TOO_LONG = 414 102 | val UNSUPPORTED_MEDIA_TYPE = 415 103 | val REQUESTED_RANGE_NOT_SATISFIABLE = 416 104 | val EXPECTATION_FAILED = 417 105 | val UNPROCESSABLE_ENTITY = 422 106 | val LOCKED = 423 107 | val FAILED_DEPENDENCY = 424 108 | val TOO_MANY_REQUEST = 429 109 | 110 | val INTERNAL_SERVER_ERROR = 500 111 | val NOT_IMPLEMENTED = 501 112 | val BAD_GATEWAY = 502 113 | val SERVICE_UNAVAILABLE = 503 114 | val GATEWAY_TIMEOUT = 504 115 | val HTTP_VERSION_NOT_SUPPORTED = 505 116 | val INSUFFICIENT_STORAGE = 507 117 | } 118 | 119 | /** Defines all standard HTTP headers. */ 120 | object HeaderNames extends HeaderNames 121 | 122 | /** Defines all standard HTTP headers. */ 123 | trait HeaderNames { 124 | 125 | val ACCEPT = "Accept" 126 | val ACCEPT_CHARSET = "Accept-Charset" 127 | val ACCEPT_ENCODING = "Accept-Encoding" 128 | val ACCEPT_LANGUAGE = "Accept-Language" 129 | val ACCEPT_RANGES = "Accept-Ranges" 130 | val AGE = "Age" 131 | val ALLOW = "Allow" 132 | val AUTHORIZATION = "Authorization" 133 | 134 | val CACHE_CONTROL = "Cache-Control" 135 | val CONNECTION = "Connection" 136 | val CONTENT_DISPOSITION = "Content-Disposition" 137 | val CONTENT_ENCODING = "Content-Encoding" 138 | val CONTENT_LANGUAGE = "Content-Language" 139 | val CONTENT_LENGTH = "Content-Length" 140 | val CONTENT_LOCATION = "Content-Location" 141 | val CONTENT_MD5 = "Content-MD5" 142 | val CONTENT_RANGE = "Content-Range" 143 | val CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding" 144 | val CONTENT_TYPE = "Content-Type" 145 | val COOKIE = "Cookie" 146 | 147 | val DATE = "Date" 148 | 149 | val ETAG = "Etag" 150 | val EXPECT = "Expect" 151 | val EXPIRES = "Expires" 152 | 153 | val FROM = "From" 154 | 155 | val HOST = "Host" 156 | 157 | val IF_MATCH = "If-Match" 158 | val IF_MODIFIED_SINCE = "If-Modified-Since" 159 | val IF_NONE_MATCH = "If-None-Match" 160 | val IF_RANGE = "If-Range" 161 | val IF_UNMODIFIED_SINCE = "If-Unmodified-Since" 162 | 163 | val LAST_MODIFIED = "Last-Modified" 164 | val LOCATION = "Location" 165 | 166 | val MAX_FORWARDS = "Max-Forwards" 167 | 168 | val PRAGMA = "Pragma" 169 | val PROXY_AUTHENTICATE = "Proxy-Authenticate" 170 | val PROXY_AUTHORIZATION = "Proxy-Authorization" 171 | 172 | val RANGE = "Range" 173 | val REFERER = "Referer" 174 | val RETRY_AFTER = "Retry-After" 175 | 176 | val SERVER = "Server" 177 | 178 | val SET_COOKIE = "Set-Cookie" 179 | val SET_COOKIE2 = "Set-Cookie2" 180 | 181 | val TE = "Te" 182 | val TRAILER = "Trailer" 183 | val TRANSFER_ENCODING = "Transfer-Encoding" 184 | 185 | val UPGRADE = "Upgrade" 186 | val USER_AGENT = "User-Agent" 187 | 188 | val VARY = "Vary" 189 | val VIA = "Via" 190 | 191 | val WARNING = "Warning" 192 | val WWW_AUTHENTICATE = "WWW-Authenticate" 193 | 194 | val X_FORWARDED_FOR = "X-Forwarded-For" 195 | val X_FORWARDED_HOST = "X-Forwarded-Host" 196 | val X_FORWARDED_PORT = "X-Forwarded-Port" 197 | val X_FORWARDED_PROTO = "X-Forwarded-Proto" 198 | 199 | val ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin" 200 | val ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers" 201 | val ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age" 202 | val ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials" 203 | val ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods" 204 | val ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers" 205 | 206 | val ORIGIN = "Origin" 207 | val ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method" 208 | val ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers" 209 | } -------------------------------------------------------------------------------- /play-mvc-library/src/main/scala/play/UriEncoding.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009-2013 Typesafe Inc. 3 | */ 4 | package play.utils 5 | 6 | import java.util.BitSet 7 | import java.io.ByteArrayOutputStream 8 | 9 | /** 10 | * Provides support for correctly encoding pieces of URIs. 11 | * 12 | * @see http://www.ietf.org/rfc/rfc3986.txt 13 | */ 14 | object UriEncoding { 15 | 16 | /** 17 | * Encode a string so that it can be used safely in the "path segment" 18 | * part of a URI. A path segment is defined in RFC 3986. In a URI such 19 | * as `http://www.example.com/abc/def?a=1&b=2` both `abc` and `def` 20 | * are path segments. 21 | * 22 | * Path segment encoding differs from encoding for other parts of a URI. 23 | * For example, the "&" character is permitted in a path segment, but 24 | * has special meaning in query parameters. On the other hand, the "/" 25 | * character cannot appear in a path segment, as it is the path delimiter, 26 | * so it must be encoded as "%2F". These are just two examples of the 27 | * differences between path segment and query string encoding; there are 28 | * other differences too. 29 | * 30 | * When encoding path segments the `encodePathSegment` method should always 31 | * be used in preference to the [[java.net.URLEncoder.encode(String,String)]] 32 | * method. `URLEncoder.encode`, despite its name, actually provides encoding 33 | * in the `application/x-www-form-urlencoded` MIME format which is the encoding 34 | * used for form data in HTTP GET and POST requests. This encoding is suitable 35 | * for inclusion in the query part of a URI. But `URLEncoder.encode` should not 36 | * be used for path segment encoding. (Also note that `URLEncoder.encode` is 37 | * not quite spec compliant. For example, it percent-encodes the `~` character when 38 | * really it should leave it as unencoded.) 39 | * 40 | * @param s The string to encode. 41 | * @param inputCharset The name of the encoding that the string `s` is encoded with. 42 | * The string `s` will be converted to octets (bytes) using this character encoding. 43 | * @return An encoded string in the US-ASCII character set. 44 | */ 45 | def encodePathSegment(s: String, inputCharset: String): String = { 46 | val in = s.getBytes(inputCharset) 47 | val out = new ByteArrayOutputStream() 48 | for (b <- in) { 49 | val allowed = segmentChars.get(b & 0xFF) 50 | if (allowed) { 51 | out.write(b) 52 | } else { 53 | out.write('%') 54 | out.write(upperHex((b >> 4) & 0xF)) 55 | out.write(upperHex(b & 0xF)) 56 | } 57 | } 58 | out.toString("US-ASCII") 59 | } 60 | 61 | /** 62 | * Decode a string according to the rules for the "path segment" 63 | * part of a URI. A path segment is defined in RFC 3986. In a URI such 64 | * as `http://www.example.com/abc/def?a=1&b=2` both `abc` and `def` 65 | * are path segments. 66 | * 67 | * Path segment encoding differs from encoding for other parts of a URI. 68 | * For example, the "&" character is permitted in a path segment, but 69 | * has special meaning in query parameters. On the other hand, the "/" 70 | * character cannot appear in a path segment, as it is the path delimiter, 71 | * so it must be encoded as "%2F". These are just two examples of the 72 | * differences between path segment and query string encoding; there are 73 | * other differences too. 74 | * 75 | * When decoding path segments the `decodePathSegment` method should always 76 | * be used in preference to the [[java.net.URLDecoder.decode(String,String)]] 77 | * method. `URLDecoder.decode`, despite its name, actually decodes 78 | * the `application/x-www-form-urlencoded` MIME format which is the encoding 79 | * used for form data in HTTP GET and POST requests. This format is suitable 80 | * for inclusion in the query part of a URI. But `URLDecoder.decoder` should not 81 | * be used for path segment encoding or decoding. 82 | * 83 | * @param s The string to decode. Must use the US-ASCII character set. 84 | * @param outputCharset The name of the encoding that the output should be encoded with. 85 | * The output string will be converted from octets (bytes) using this character encoding. 86 | * @throws InvalidEncodingException If the input is not a valid encoded path segment. 87 | * @return A decoded string in the `outputCharset` character set. 88 | */ 89 | def decodePathSegment(s: String, outputCharset: String): String = { 90 | val in = s.getBytes("US-ASCII") 91 | val out = new ByteArrayOutputStream() 92 | var inPos = 0 93 | def next(): Int = { 94 | val b = in(inPos) & 0xFF 95 | inPos += 1 96 | b 97 | } 98 | while (inPos < in.length) { 99 | val b = next() 100 | if (b == '%') { 101 | // Read high digit 102 | if (inPos >= in.length) throw new InvalidUriEncodingException(s"Cannot decode $s: % at end of string") 103 | val high = fromHex(next()) 104 | if (high == -1) throw new InvalidUriEncodingException(s"Cannot decode $s: expected hex digit at position $inPos.") 105 | // Read low digit 106 | if (inPos >= in.length) throw new InvalidUriEncodingException(s"Cannot decode $s: incomplete percent encoding at end of string") 107 | val low = fromHex(next()) 108 | if (low == -1) throw new InvalidUriEncodingException(s"Cannot decode $s: expected hex digit at position $inPos.") 109 | // Write decoded byte 110 | out.write((high << 4) + low) 111 | } else if (segmentChars.get(b)) { 112 | // This character is allowed 113 | out.write(b) 114 | } else { 115 | throw new InvalidUriEncodingException(s"Cannot decode $s: illegal character at position $inPos.") 116 | } 117 | } 118 | out.toString(outputCharset) 119 | } 120 | 121 | /** 122 | * Decode the path path of a URI. Each path segment will be decoded 123 | * using the same rules as ``decodePathSegment``. No normalization is performed: 124 | * leading, trailing and duplicated slashes, if present are left as they are and 125 | * if absent remain absent; dot-segments (".." and ".") are ignored. 126 | * 127 | * Encoded slash characters are will appear as slashes in the output, thus "a/b" 128 | * will be indistinguishable from "a%2Fb". 129 | * 130 | * @param s The string to decode. Must use the US-ASCII character set. 131 | * @param outputCharset The name of the encoding that the output should be encoded with. 132 | * The output string will be converted from octets (bytes) using this character encoding. 133 | * @throws InvalidEncodingException If the input is not a valid encoded path. 134 | * @return A decoded string in the `outputCharset` character set. 135 | */ 136 | def decodePath(s: String, outputCharset: String): String = { 137 | // Note: Could easily expose a method to return the decoded path as a Seq[String]. 138 | // This would allow better handling of paths segments with encoded slashes in them. 139 | // However, there is no need for this yet, so the method hasn't been added yet. 140 | splitString(s, '/').map(decodePathSegment(_, outputCharset)).mkString("/") 141 | } 142 | 143 | // RFC 3986, 3.3. Path 144 | // segment = *pchar 145 | // segment-nz = 1*pchar 146 | // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) 147 | // ; non-zero-length segment without any colon ":" 148 | /** The set of ASCII character codes that are allowed in a URI path segment. */ 149 | private val segmentChars: BitSet = membershipTable(pchar) 150 | 151 | /** The characters allowed in a path segment; defined in RFC 3986 */ 152 | private def pchar: Seq[Char] = { 153 | // RFC 3986, 2.3. Unreserved Characters 154 | // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 155 | val alphaDigit = for ((min, max) <- Seq(('a', 'z'), ('A', 'Z'), ('0', '9')); c <- min to max) yield c 156 | val unreserved = alphaDigit ++ Seq('-', '.', '_', '~') 157 | 158 | // RFC 3986, 2.2. Reserved Characters 159 | // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 160 | // / "*" / "+" / "," / ";" / "=" 161 | val subDelims = Seq('!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=') 162 | 163 | // RFC 3986, 3.3. Path 164 | // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 165 | unreserved ++ subDelims ++ Seq(':', '@') 166 | } 167 | 168 | /** Create a BitSet to act as a membership lookup table for the given characters. */ 169 | private def membershipTable(chars: Seq[Char]): BitSet = { 170 | val bits = new BitSet(256) 171 | for (c <- chars) { bits.set(c.toInt) } 172 | bits 173 | } 174 | 175 | /** 176 | * Given a number from 0 to 16, return the ASCII character code corresponding 177 | * to its uppercase hexadecimal representation. 178 | */ 179 | private def upperHex(x: Int): Int = { 180 | // Assume 0 <= x < 16 181 | if (x < 10) (x + '0') else (x - 10 + 'A') 182 | } 183 | 184 | /** 185 | * Given the ASCII value of a character, return its value as a hex digit. 186 | * If the character isn't a valid hex digit, return -1 instead. 187 | */ 188 | private def fromHex(b: Int): Int = { 189 | if (b >= '0' && b <= '9') { 190 | b - '0' 191 | } else if (b >= 'A' && b <= 'Z') { 192 | 10 + b - 'A' 193 | } else if (b >= 'a' && b <= 'z') { 194 | 10 + b - 'a' 195 | } else { 196 | -1 197 | } 198 | } 199 | 200 | /** 201 | * Split a string on a character. Similar to `String.split` except, for this method, 202 | * the invariant {{{splitString(s, '/').mkString("/") == s}}} holds. 203 | * 204 | * For example: 205 | * {{{ 206 | * splitString("//a//", '/') == Seq("", "", "a", "", "") 207 | * String.split("//a//", '/') == Seq("", "", "a") 208 | * }}} 209 | */ 210 | private[utils] def splitString(s: String, c: Char): Seq[String] = { 211 | val result = scala.collection.mutable.ListBuffer.empty[String] 212 | import scala.annotation.tailrec 213 | @tailrec 214 | def splitLoop(start: Int): Unit = if (start < s.length) { 215 | var end = s.indexOf(c, start) 216 | if (end == -1) { 217 | result += s.substring(start) 218 | } else { 219 | result += s.substring(start, end) 220 | splitLoop(end + 1) 221 | } 222 | } else if (start == s.length) { 223 | result += "" 224 | } 225 | splitLoop(0) 226 | result 227 | } 228 | 229 | } 230 | 231 | /** 232 | * An error caused by processing a value that isn't encoded correctly. 233 | */ 234 | class InvalidUriEncodingException(msg: String) extends RuntimeException(msg) -------------------------------------------------------------------------------- /play-mvc-library/src/main/scala/play/core/parsers/FormUrlEncodedParser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009-2013 Typesafe Inc. 3 | */ 4 | package play.core.parsers 5 | 6 | import scala.collection.immutable.ListMap 7 | 8 | /** An object for parsing application/x-www-form-urlencoded data */ 9 | object FormUrlEncodedParser { 10 | 11 | /** 12 | * Parse the content type "application/x-www-form-urlencoded" which consists of a bunch of & separated key=value 13 | * pairs, both of which are URL encoded. 14 | * @param data The body content of the request, or whatever needs to be so parsed 15 | * @param encoding The character encoding of data 16 | * @return A ListMap of keys to the sequence of values for that key 17 | */ 18 | def parseNotPreservingOrder(data: String, encoding: String = "utf-8"): Map[String, Seq[String]] = { 19 | // Generate the pairs of values from the string. 20 | parseToPairs(data, encoding).groupBy(_._1).map(param => param._1 -> param._2.map(_._2)).toMap 21 | } 22 | 23 | /** 24 | * Parse the content type "application/x-www-form-urlencoded" which consists of a bunch of & separated key=value 25 | * pairs, both of which are URL encoded. We are careful in this parser to maintain the original order of the 26 | * keys by using OrderPreserving.groupBy as some applications depend on the original browser ordering. 27 | * @param data The body content of the request, or whatever needs to be so parsed 28 | * @param encoding The character encoding of data 29 | * @return A ListMap of keys to the sequence of values for that key 30 | */ 31 | def parse(data: String, encoding: String = "utf-8"): Map[String, Seq[String]] = { 32 | 33 | // Generate the pairs of values from the string. 34 | val pairs: Seq[(String, String)] = parseToPairs(data, encoding) 35 | 36 | // Group the pairs by the key (first item of the pair) being sure to preserve insertion order 37 | play.utils.OrderPreserving.groupBy(pairs)(_._1) 38 | } 39 | 40 | /** 41 | * Do the basic parsing into a sequence of key/value pairs 42 | * @param data The data to parse 43 | * @param encoding The encoding to use for interpreting the data 44 | * @return The sequence of key/value pairs 45 | */ 46 | private def parseToPairs(data: String, encoding: String): Seq[(String, String)] = { 47 | 48 | import java.net._ 49 | 50 | // Generate all the pairs, with potentially redundant key values, by parsing the body content. 51 | data.split('&').flatMap { param => 52 | if (param.contains("=") && !param.startsWith("=")) { 53 | val parts = param.split("=") 54 | val key = URLDecoder.decode(parts.head, encoding) 55 | val value = URLDecoder.decode(parts.tail.headOption.getOrElse(""), encoding) 56 | Seq(key -> value) 57 | } else { 58 | Nil 59 | } 60 | }.toSeq 61 | } 62 | } -------------------------------------------------------------------------------- /play-mvc-library/src/main/scala/play/utils/OrderPreserving.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009-2013 Typesafe Inc. 3 | */ 4 | package play.utils 5 | 6 | import scala.collection.immutable.ListMap 7 | import scala.collection.mutable 8 | 9 | object OrderPreserving { 10 | 11 | def groupBy[K, V](seq: Seq[(K, V)])(f: ((K, V)) => K): Map[K, Seq[V]] = { 12 | // This mutable map will not retain insertion order for the seq, but it is fast for retrieval. The value is 13 | // a builder for the desired Seq[String] in the final result. 14 | val m = mutable.Map.empty[K, mutable.Builder[V, Seq[V]]] 15 | 16 | // Run through the seq and create builders for each unique key, effectively doing the grouping 17 | for ((key, value) <- seq) m.getOrElseUpdate(key, mutable.Seq.newBuilder[V]) += value 18 | 19 | // Create a builder for the resulting ListMap. Note that this one is immutable and will retain insertion order 20 | val b = ListMap.newBuilder[K, Seq[V]] 21 | 22 | // Note that we are NOT going through m (didn't retain order) but we are iterating over the original seq 23 | // just to get the keys so we can look up the values in m with them. This is how order is maintained. 24 | for ((k, v) <- seq.iterator) b += k -> m.getOrElse(k, mutable.Seq.newBuilder[V]).result 25 | 26 | // Get the builder to produce the final result 27 | b.result 28 | } 29 | } -------------------------------------------------------------------------------- /play-mvc-library/src/test/scala/play/core/parsers/FormUrlEncodedParserSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009-2013 Typesafe Inc. 3 | */ 4 | package play.core.parsers 5 | 6 | import org.specs2.mutable.Specification 7 | 8 | object FormUrlEncodedParserSpec extends Specification { 9 | "FormUrlEncodedParser" should { 10 | "decode forms" in { 11 | FormUrlEncodedParser.parse("foo1=bar1&foo2=bar2") must_== Map("foo1" -> List("bar1"), "foo2" -> List("bar2")) 12 | } 13 | "decode form elements with multiple values" in { 14 | FormUrlEncodedParser.parse("foo=bar1&foo=bar2") must_== Map("foo" -> List("bar1", "bar2")) 15 | } 16 | "decode fields with empty names" in { 17 | FormUrlEncodedParser.parse("foo=bar&=") must_== Map("foo" -> List("bar")) 18 | } 19 | "ensure field order is retained, when requested" in { 20 | val url_encoded = "Zero=zero&One=one&Two=two&Three=three&Four=four&Five=five&Six=six&Seven=seven" 21 | val result: Map[String, Seq[String]] = FormUrlEncodedParser.parse(url_encoded) 22 | val strings = ( for ( k <- result.keysIterator ) yield "&" + k + "=" + result(k).head ).mkString 23 | val reconstructed = strings.substring(1) 24 | reconstructed must equalTo(url_encoded) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /samples/2048/build.sbt: -------------------------------------------------------------------------------- 1 | import sbtappengine.Plugin.{AppengineKeys => gae} 2 | 3 | import play.PlayProject 4 | 5 | name := "2048" 6 | 7 | scalaVersion := "2.10.2" 8 | 9 | resolvers += "Scala AppEngine Sbt Repo" at "http://siderakis.github.com/maven" 10 | 11 | resolvers += "Typesafe repository releases" at "http://repo.typesafe.com/typesafe/releases/" 12 | 13 | libraryDependencies ++= Seq( 14 | "com.siderakis" %% "futuraes" % "0.1-SNAPSHOT", 15 | "com.siderakis" %% "playframework-appengine-mvc" % "0.2-SNAPSHOT", 16 | "javax.servlet" % "servlet-api" % "2.5" % "provided", 17 | "org.mortbay.jetty" % "jetty" % "6.1.22" % "container", 18 | "play" %% "play-iteratees" % "2.1.5", 19 | "com.siderakis" %% "futuraes" % "0.1-SNAPSHOT" 20 | ) 21 | 22 | appengineSettings 23 | 24 | PlayProject.defaultPlaySettings 25 | 26 | Twirl.settings 27 | 28 | -------------------------------------------------------------------------------- /samples/2048/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.0-RC5 2 | -------------------------------------------------------------------------------- /samples/2048/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.10.3" 2 | 3 | resolvers += "Scala AppEngine Sbt Repo" at "http://siderakis.github.com/maven" 4 | 5 | addSbtPlugin("com.siderakis" %% "playframework-appengine-routes" % "0.2-SNAPSHOT") 6 | 7 | addSbtPlugin("com.eed3si9n" % "sbt-appengine" % "0.6.2") 8 | 9 | // needed because sbt-twirl depends on twirl-compiler which is only available 10 | // at repo.spray.io 11 | resolvers += "spray repo" at "http://repo.spray.io" 12 | 13 | addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0") 14 | -------------------------------------------------------------------------------- /samples/2048/src/main/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.Application.index 7 | 8 | # Receives a new game state and broadcasts the message to all connected users. 9 | POST /game controllers.Application.move 10 | 11 | # Messages sent from the App Engine Channel API 12 | # https://developers.google.com/appengine/docs/java/channel/ 13 | POST /_ah/channel/connected/ controllers.Application.connected 14 | POST /_ah/channel/disconnected/ controllers.Application.connected 15 | 16 | -------------------------------------------------------------------------------- /samples/2048/src/main/scala/controllers/ApplicationController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import com.google.appengine.api.channel.{ChannelMessage, ChannelServiceFactory} 4 | import scala.util.Random 5 | import scala.collection.mutable 6 | import scala.io.Source 7 | 8 | import play.api.mvc._ 9 | import play.Controller 10 | 11 | 12 | //https://developers.google.com/appengine/docs/java/modules/#Java_Background_threads 13 | //https://developers.google.com/appengine/docs/java/channel/ 14 | //https://www.playframework.com/documentation/2.2.x/Enumerators 15 | //https://github.com/playframework/playframework/blob/321af079941f64cdd2cf32b407d4026f7e49dfec/framework/src/play/src/main/scala/play/api/mvc/Results.scala 16 | 17 | object Application extends Controller { 18 | 19 | 20 | val connectedSet = mutable.Set[String]() 21 | 22 | def connected() = Action { 23 | req => 24 | val who = ChannelServiceFactory.getChannelService.parsePresence(req.req) 25 | val did = if (who.isConnected) "connected" else "disconnected" 26 | println(s"${who.clientId} just $did") 27 | if (who.isConnected) { 28 | connectedSet.add(who.clientId()) 29 | } else { 30 | connectedSet.remove(who.clientId()) 31 | } 32 | Ok("") 33 | } 34 | 35 | def move() = Action { 36 | 37 | req => 38 | val body = Source.fromInputStream(req.req.getInputStream).mkString 39 | 40 | val channelService = ChannelServiceFactory.getChannelService 41 | 42 | connectedSet. 43 | map(new ChannelMessage(_, body)). 44 | foreach(channelService.sendMessage) 45 | 46 | Ok("") 47 | 48 | 49 | } 50 | 51 | def index() = Action { 52 | val token = ChannelServiceFactory.getChannelService.createChannel(Random.alphanumeric.take(10).mkString) 53 | 54 | Ok(html.index.render(token).toString) 55 | } 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /samples/2048/src/main/scala/controllers/package.scala: -------------------------------------------------------------------------------- 1 | package play.api.libs { 2 | 3 | /** 4 | * The Iteratee monad provides strict, safe, and functional I/O. 5 | */ 6 | package object iteratee { 7 | 8 | type K[E, A] = Input[E] => Iteratee[E, A] 9 | 10 | } 11 | 12 | } 13 | 14 | package play.api.libs.iteratee { 15 | 16 | import com.google.appengine.api.ThreadManager 17 | 18 | private[iteratee] object internal { 19 | import scala.concurrent.ExecutionContext 20 | import java.util.concurrent.Executors 21 | 22 | implicit lazy val defaultExecutionContext: scala.concurrent.ExecutionContext = { 23 | val numberOfThreads = 10 24 | val threadFactory = ThreadManager.backgroundThreadFactory() 25 | 26 | 27 | ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(numberOfThreads, threadFactory)) 28 | } 29 | } 30 | } 31 | 32 | 33 | 34 | 35 | /* __ *\ 36 | ** ________ ___ / / ___ Scala API ** 37 | ** / __/ __// _ | / / / _ | (c) 2003-2013, LAMP/EPFL ** 38 | ** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** 39 | ** /____/\___/_/ |_/____/_/ | | ** 40 | ** |/ ** 41 | \* */ 42 | 43 | package scala.concurrent.impl{ 44 | 45 | //import scala.concurrent.impl.Promise 46 | import scala.concurrent.{ExecutionContext, CanAwait, OnCompleteRunnable, TimeoutException, ExecutionException} 47 | import scala.concurrent.duration.{Duration, Deadline, FiniteDuration} 48 | import scala.annotation.tailrec 49 | import scala.util.control.NonFatal 50 | import scala.util.{Try, Success, Failure} 51 | 52 | private[concurrent] trait Promise[T] extends scala.concurrent.Promise[T] with scala.concurrent.Future[T] { 53 | def future: this.type = this 54 | } 55 | 56 | /* Precondition: `executor` is prepared, i.e., `executor` has been returned from invocation of `prepare` on some other `ExecutionContext`. 57 | */ 58 | private class CallbackRunnable[T](val executor: ExecutionContext, val onComplete: Try[T] => Any) extends Runnable with OnCompleteRunnable { 59 | // must be filled in before running it 60 | var value: Try[T] = null 61 | 62 | override def run() = { 63 | require(value ne null) // must set value to non-null before running! 64 | try onComplete(value) catch { 65 | case NonFatal(e) => executor reportFailure e 66 | } 67 | } 68 | 69 | def executeWithValue(v: Try[T]): Unit = { 70 | require(value eq null) // can't complete it twice 71 | value = v 72 | // Note that we cannot prepare the ExecutionContext at this point, since we might 73 | // already be running on a different thread! 74 | try executor.execute(this) catch { 75 | case NonFatal(t) => executor reportFailure t 76 | } 77 | } 78 | } 79 | 80 | private[concurrent] object Promise { 81 | 82 | /** An already completed Future is given its result at creation. 83 | * 84 | * Useful in Future-composition when a value to contribute is already available. 85 | */ 86 | final class KeptPromise[T](suppliedValue: Try[T]) extends Promise[T] { 87 | 88 | val value = Some(resolveTry(suppliedValue)) 89 | 90 | override def isCompleted: Boolean = true 91 | 92 | def tryComplete(value: Try[T]): Boolean = false 93 | 94 | def onComplete[U](func: Try[T] => U)(implicit executor: ExecutionContext): Unit = { 95 | val completedAs = value.get 96 | val preparedEC = executor.prepare 97 | (new CallbackRunnable(preparedEC, func)).executeWithValue(completedAs) 98 | } 99 | 100 | def ready(atMost: Duration)(implicit permit: CanAwait): this.type = this 101 | 102 | def result(atMost: Duration)(implicit permit: CanAwait): T = value.get.get 103 | } 104 | 105 | private def resolveTry[T](source: Try[T]): Try[T] = source match { 106 | case Failure(t) => resolver(t) 107 | case _ => source 108 | } 109 | 110 | private def resolver[T](throwable: Throwable): Try[T] = throwable match { 111 | case t: scala.runtime.NonLocalReturnControl[_] => Success(t.value.asInstanceOf[T]) 112 | case t: scala.util.control.ControlThrowable => Failure(new ExecutionException("Boxed ControlThrowable", t)) 113 | case t: InterruptedException => Failure(new ExecutionException("Boxed InterruptedException", t)) 114 | case e: Error => Failure(new ExecutionException("Boxed Error", e)) 115 | case t => Failure(t) 116 | } 117 | 118 | /** 119 | * App Engine promise implementation. 120 | */ 121 | class DefaultPromise[T] extends Object with Promise[T] { 122 | self => 123 | 124 | /** 125 | * Atomically update variable to newState if it is currently 126 | * holding oldState. 127 | * @return true if successful 128 | */ 129 | def updateState(oldState: AnyRef, newState: AnyRef) = { 130 | // println(s"updateState: $oldState and $newState") 131 | obj.synchronized( 132 | if (obj == oldState) { 133 | obj = newState 134 | true 135 | } else { 136 | false 137 | } 138 | ) 139 | } 140 | 141 | @volatile var obj: AnyRef = Nil 142 | 143 | def getState() = { 144 | obj.synchronized(obj) 145 | } 146 | 147 | // Start at "No callbacks" 148 | updateState(null, Nil) 149 | 150 | 151 | protected final def tryAwait(atMost: Duration): Boolean = { 152 | @tailrec 153 | def awaitUnsafe(deadline: Deadline, nextWait: FiniteDuration): Boolean = { 154 | if (!isCompleted && nextWait > Duration.Zero) { 155 | val ms = nextWait.toMillis 156 | val ns = (nextWait.toNanos % 1000000l).toInt // as per object.wait spec 157 | 158 | synchronized { 159 | if (!isCompleted) wait(ms, ns) 160 | } 161 | 162 | awaitUnsafe(deadline, deadline.timeLeft) 163 | } else 164 | isCompleted 165 | } 166 | @tailrec 167 | def awaitUnbounded(): Boolean = { 168 | if (isCompleted) true 169 | else { 170 | synchronized { 171 | if (!isCompleted) wait() 172 | } 173 | awaitUnbounded() 174 | } 175 | } 176 | 177 | import Duration.Undefined 178 | atMost match { 179 | case u if u eq Undefined => throw new IllegalArgumentException("cannot wait for Undefined period") 180 | case Duration.Inf => awaitUnbounded 181 | case Duration.MinusInf => isCompleted 182 | case f: FiniteDuration => if (f > Duration.Zero) awaitUnsafe(f.fromNow, f) else isCompleted 183 | } 184 | } 185 | 186 | @throws(classOf[TimeoutException]) 187 | @throws(classOf[InterruptedException]) 188 | def ready(atMost: Duration)(implicit permit: CanAwait): this.type = 189 | if (isCompleted || tryAwait(atMost)) this 190 | else throw new TimeoutException("Futures timed out after [" + atMost + "]") 191 | 192 | @throws(classOf[Exception]) 193 | def result(atMost: Duration)(implicit permit: CanAwait): T = 194 | ready(atMost).value.get match { 195 | case Failure(e) => throw e 196 | case Success(r) => r 197 | } 198 | 199 | def value: Option[Try[T]] = getState match { 200 | case c: Try[_] => Some(c.asInstanceOf[Try[T]]) 201 | case _ => None 202 | } 203 | 204 | override def isCompleted: Boolean = getState match { 205 | // Cheaper than boxing result into Option due to "def value" 206 | case _: Try[_] => true 207 | case _ => false 208 | } 209 | 210 | def tryComplete(value: Try[T]): Boolean = { 211 | val resolved = resolveTry(value) 212 | (try { 213 | @tailrec 214 | def tryComplete(v: Try[T]): List[CallbackRunnable[T]] = { 215 | getState match { 216 | case raw: List[_] => 217 | val cur = raw.asInstanceOf[List[CallbackRunnable[T]]] 218 | if (updateState(cur, v)) cur else tryComplete(v) 219 | case _ => null 220 | } 221 | } 222 | tryComplete(resolved) 223 | } finally { 224 | synchronized { 225 | notifyAll() 226 | } //Notify any evil blockers 227 | }) match { 228 | case null => false 229 | case rs if rs.isEmpty => true 230 | case rs => rs.foreach(r => r.executeWithValue(resolved)); true 231 | } 232 | } 233 | 234 | def onComplete[U](func: Try[T] => U)(implicit executor: ExecutionContext): Unit = { 235 | val preparedEC = executor.prepare 236 | val runnable = new CallbackRunnable[T](preparedEC, func) 237 | 238 | @tailrec //Tries to add the callback, if already completed, it dispatches the callback to be executed 239 | def dispatchOrAddCallback(): Unit = 240 | getState match { 241 | case r: Try[_] => runnable.executeWithValue(r.asInstanceOf[Try[T]]) 242 | case listeners: List[_] => if (updateState(listeners, runnable :: listeners)) () else dispatchOrAddCallback() 243 | } 244 | dispatchOrAddCallback() 245 | } 246 | 247 | 248 | 249 | 250 | 251 | /** Link this promise to the root of another promise using `link()`. Should only be 252 | * be called by Future.flatMap. 253 | */ 254 | protected[concurrent] final def linkRootOf(target: DefaultPromise[T]): Unit = link(target.compressedRoot()) 255 | 256 | 257 | 258 | /** Get the promise at the root of the chain of linked promises. Used by `compressedRoot()`. 259 | * The `compressedRoot()` method should be called instead of this method, as it is important 260 | * to compress the link chain whenever possible. 261 | */ 262 | @tailrec 263 | private def root: DefaultPromise[T] = { 264 | getState match { 265 | case linked: DefaultPromise[_] => linked.asInstanceOf[DefaultPromise[T]].root 266 | case _ => this 267 | } 268 | } 269 | 270 | /** Get the root promise for this promise, compressing the link chain to that 271 | * promise if necessary. 272 | * 273 | * For promises that are not linked, the result of calling 274 | * `compressedRoot()` will the promise itself. However for linked promises, 275 | * this method will traverse each link until it locates the root promise at 276 | * the base of the link chain. 277 | * 278 | * As a side effect of calling this method, the link from this promise back 279 | * to the root promise will be updated ("compressed") to point directly to 280 | * the root promise. This allows intermediate promises in the link chain to 281 | * be garbage collected. Also, subsequent calls to this method should be 282 | * faster as the link chain will be shorter. 283 | */ 284 | @tailrec 285 | private def compressedRoot(): DefaultPromise[T] = { 286 | getState match { 287 | case linked: DefaultPromise[_] => 288 | val target = linked.asInstanceOf[DefaultPromise[T]].root 289 | if (linked eq target) target else if (updateState(linked, target)) target else compressedRoot() 290 | case _ => this 291 | } 292 | } 293 | 294 | /** Tries to add the callback, if already completed, it dispatches the callback to be executed. 295 | * Used by `onComplete()` to add callbacks to a promise and by `link()` to transfer callbacks 296 | * to the root promise when linking two promises togehter. 297 | */ 298 | @tailrec 299 | private def dispatchOrAddCallback(runnable: CallbackRunnable[T]): Unit = { 300 | getState match { 301 | case r: Try[_] => runnable.executeWithValue(r.asInstanceOf[Try[T]]) 302 | case _: DefaultPromise[_] => compressedRoot().dispatchOrAddCallback(runnable) 303 | case listeners: List[_] => if (updateState(listeners, runnable :: listeners)) () else dispatchOrAddCallback(runnable) 304 | } 305 | } 306 | 307 | /** Link this promise to another promise so that both promises share the same 308 | * externally-visible state. Depending on the current state of this promise, this 309 | * may involve different things. For example, any onComplete listeners will need 310 | * to be transferred. 311 | * 312 | * If this promise is already completed, then the same effect as linking - 313 | * sharing the same completed value - is achieved by simply sending this 314 | * promise's result to the target promise. 315 | */ 316 | @tailrec 317 | private def link(target: DefaultPromise[T]): Unit = if (this ne target) { 318 | getState match { 319 | case r: Try[_] => 320 | if (!target.tryComplete(r.asInstanceOf[Try[T]])) { 321 | // Currently linking is done from Future.flatMap, which should ensure only 322 | // one promise can be completed. Therefore this situation is unexpected. 323 | throw new IllegalStateException("Cannot link completed promises together") 324 | } 325 | case _: DefaultPromise[_] => 326 | compressedRoot().link(target) 327 | case listeners: List[_] => if (updateState(listeners, target)) { 328 | if (!listeners.isEmpty) listeners.asInstanceOf[List[CallbackRunnable[T]]].foreach(target.dispatchOrAddCallback(_)) 329 | } else link(target) 330 | } 331 | } 332 | } 333 | 334 | 335 | } 336 | 337 | 338 | } 339 | 340 | package scala.concurrent{ 341 | 342 | import scala.language.higherKinds 343 | 344 | import java.util.concurrent.{TimeUnit} 345 | import scala.concurrent.{Future, ExecutionContext, Promise => SPromise} 346 | import scala.util.{Failure, Success, Try} 347 | import scala.concurrent.duration.FiniteDuration 348 | 349 | 350 | /** Promise is an object which can be completed with a value or failed 351 | * with an exception. 352 | * 353 | * @define promiseCompletion 354 | * If the promise has already been fulfilled, failed or has timed out, 355 | * calling this method will throw an IllegalStateException. 356 | * 357 | * @define allowedThrowables 358 | * If the throwable used to fail this promise is an error, a control exception 359 | * or an interrupted exception, it will be wrapped as a cause within an 360 | * `ExecutionException` which will fail the promise. 361 | * 362 | * @define nonDeterministic 363 | * Note: Using this method may result in non-deterministic concurrent programs. 364 | */ 365 | trait Promise[T] { 366 | 367 | // used for internal callbacks defined in 368 | // the lexical scope of this trait; 369 | // _never_ for application callbacks. 370 | private implicit def internalExecutor: ExecutionContext = Future.InternalCallbackExecutor 371 | 372 | /** Future containing the value of this promise. 373 | */ 374 | def future: Future[T] 375 | 376 | /** Returns whether the promise has already been completed with 377 | * a value or an exception. 378 | * 379 | * $nonDeterministic 380 | * 381 | * @return `true` if the promise is already completed, `false` otherwise 382 | */ 383 | def isCompleted: Boolean 384 | 385 | /** Completes the promise with either an exception or a value. 386 | * 387 | * @param result Either the value or the exception to complete the promise with. 388 | * 389 | * $promiseCompletion 390 | */ 391 | def complete(result: Try[T]): this.type = 392 | if (tryComplete(result)) this else throw new IllegalStateException("Promise already completed.") 393 | 394 | /** Tries to complete the promise with either a value or the exception. 395 | * 396 | * $nonDeterministic 397 | * 398 | * @return If the promise has already been completed returns `false`, or `true` otherwise. 399 | */ 400 | def tryComplete(result: Try[T]): Boolean 401 | 402 | /** Completes this promise with the specified future, once that future is completed. 403 | * 404 | * @return This promise 405 | */ 406 | final def completeWith(other: Future[T]): this.type = { 407 | other onComplete { this complete _ } 408 | this 409 | } 410 | 411 | /** Attempts to complete this promise with the specified future, once that future is completed. 412 | * 413 | * @return This promise 414 | */ 415 | final def tryCompleteWith(other: Future[T]): this.type = { 416 | other onComplete { this tryComplete _ } 417 | this 418 | } 419 | 420 | /** Completes the promise with a value. 421 | * 422 | * @param v The value to complete the promise with. 423 | * 424 | * $promiseCompletion 425 | */ 426 | def success(v: T): this.type = complete(Success(v)) 427 | 428 | /** Tries to complete the promise with a value. 429 | * 430 | * $nonDeterministic 431 | * 432 | * @return If the promise has already been completed returns `false`, or `true` otherwise. 433 | */ 434 | def trySuccess(value: T): Boolean = tryComplete(Success(value)) 435 | 436 | /** Completes the promise with an exception. 437 | * 438 | * @param t The throwable to complete the promise with. 439 | * 440 | * $allowedThrowables 441 | * 442 | * $promiseCompletion 443 | */ 444 | def failure(t: Throwable): this.type = complete(Failure(t)) 445 | 446 | /** Tries to complete the promise with an exception. 447 | * 448 | * $nonDeterministic 449 | * 450 | * @return If the promise has already been completed returns `false`, or `true` otherwise. 451 | */ 452 | def tryFailure(t: Throwable): Boolean = tryComplete(Failure(t)) 453 | } 454 | 455 | /** 456 | * useful helper methods to create and compose Promises 457 | */ 458 | object Promise { 459 | 460 | /** Creates a promise object which can be completed with a value. 461 | * 462 | * @tparam T the type of the value in the promise 463 | * @return the newly created `Promise` object 464 | */ 465 | def apply[T](): Promise[T] = new impl.Promise.DefaultPromise[T]() 466 | 467 | /** Creates an already completed Promise with the specified exception. 468 | * 469 | * @tparam T the type of the value in the promise 470 | * @return the newly created `Promise` object 471 | */ 472 | def failed[T](exception: Throwable): Promise[T] = fromTry(Failure(exception)) 473 | 474 | /** Creates an already completed Promise with the specified result. 475 | * 476 | * @tparam T the type of the value in the promise 477 | * @return the newly created `Promise` object 478 | */ 479 | def successful[T](result: T): Promise[T] = fromTry(Success(result)) 480 | 481 | /** Creates an already completed Promise with the specified result or exception. 482 | * 483 | * @tparam T the type of the value in the promise 484 | * @return the newly created `Promise` object 485 | */ 486 | def fromTry[T](result: Try[T]): Promise[T] = new impl.Promise.KeptPromise[T](result) 487 | 488 | /** 489 | * Constructs a Future which will contain value "message" after the given duration elapses. 490 | * This is useful only when used in conjunction with other Promises 491 | * @param message message to be displayed 492 | * @param duration duration for the scheduled promise 493 | * @return a scheduled promise 494 | */ 495 | def timeout[A](message: => A, duration: scala.concurrent.duration.Duration)(implicit ec: ExecutionContext): Future[A] = { 496 | timeout(message, duration.toMillis) 497 | } 498 | 499 | /** 500 | * Constructs a Future which will contain value "message" after the given duration elapses. 501 | * This is useful only when used in conjunction with other Promises 502 | * @param message message to be displayed 503 | * @param duration duration for the scheduled promise 504 | * @return a scheduled promise 505 | */ 506 | def timeout[A](message: => A, duration: Long, unit: TimeUnit = TimeUnit.MILLISECONDS)(implicit ec: ExecutionContext): Future[A] = { 507 | val p = SPromise[A]() 508 | //import play.api.Play.current 509 | //Akka.system.scheduler.scheduleOnce(FiniteDuration(duration, unit)) { 510 | Thread.sleep(FiniteDuration(duration, unit).toMillis) 511 | p.complete(Try(message)) 512 | //} 513 | p.future 514 | } 515 | 516 | } 517 | } -------------------------------------------------------------------------------- /samples/2048/src/main/twirl/index.scala.html: -------------------------------------------------------------------------------- 1 | @(token:String) 2 | 3 | 4 | 5 | 6 | 2048 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 |

2048

24 |
25 |
0
26 |
0
27 |
28 |
29 | 30 |
31 |

Join the numbers and get to the 2048 tile!

32 | New Game 33 |
34 | 35 |
36 |
37 |

38 |
39 | Keep going 40 | Try again 41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 71 |
72 | 73 |
74 |
75 | 76 |

77 | How to play: Use your arrow keys to move the tiles. When two tiles with the same number touch, they merge into one! 78 |

79 |
80 |
81 |

82 | Multiplayer backend created by Nick Siderakis. 83 |

84 |

85 | Frontend created by Gabriele Cirulli. 86 |

87 |

88 | Based on 1024 by Veewo Studio and conceptually similar to Threes by Asher Vollmer. 89 |

90 |
91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/WEB-INF/appengine-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ns-labs 4 | default 5 | playframework-appengine-2048 6 | true 7 | 8 | channel_presence 9 | 10 | 11 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | PlayAppEngineServlet 7 | play.PlayAppEngineServlet 8 | 9 | 10 | PlayAppEngineServlet 11 | /* 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siderakis/playframework-appengine/b09d47c580677803d3458c8d91b69ebf02dff393/samples/2048/src/main/webapp/favicon.ico -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/javascripts/animframe_polyfill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var lastTime = 0; 3 | var vendors = ['webkit', 'moz']; 4 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 5 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; 6 | window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || 7 | window[vendors[x] + 'CancelRequestAnimationFrame']; 8 | } 9 | 10 | if (!window.requestAnimationFrame) { 11 | window.requestAnimationFrame = function (callback) { 12 | var currTime = new Date().getTime(); 13 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 14 | var id = window.setTimeout(function () { 15 | callback(currTime + timeToCall); 16 | }, 17 | timeToCall); 18 | lastTime = currTime + timeToCall; 19 | return id; 20 | }; 21 | } 22 | 23 | if (!window.cancelAnimationFrame) { 24 | window.cancelAnimationFrame = function (id) { 25 | clearTimeout(id); 26 | }; 27 | } 28 | }()); 29 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // Wait till the browser is ready to render the game (avoids glitches) 2 | window.requestAnimationFrame(function () { 3 | window.GAME = new GameManager(4, KeyboardInputManager, HTMLActuator, LocalStorageManager); 4 | }); 5 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/javascripts/bind_polyfill.js: -------------------------------------------------------------------------------- 1 | Function.prototype.bind = Function.prototype.bind || function (target) { 2 | var self = this; 3 | return function (args) { 4 | if (!(args instanceof Array)) { 5 | args = [args]; 6 | } 7 | self.apply(target, args); 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/javascripts/classlist_polyfill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | if (typeof window.Element === "undefined" || 3 | "classList" in document.documentElement) { 4 | return; 5 | } 6 | 7 | var prototype = Array.prototype, 8 | push = prototype.push, 9 | splice = prototype.splice, 10 | join = prototype.join; 11 | 12 | function DOMTokenList(el) { 13 | this.el = el; 14 | // The className needs to be trimmed and split on whitespace 15 | // to retrieve a list of classes. 16 | var classes = el.className.replace(/^\s+|\s+$/g, '').split(/\s+/); 17 | for (var i = 0; i < classes.length; i++) { 18 | push.call(this, classes[i]); 19 | } 20 | } 21 | 22 | DOMTokenList.prototype = { 23 | add: function (token) { 24 | if (this.contains(token)) return; 25 | push.call(this, token); 26 | this.el.className = this.toString(); 27 | }, 28 | contains: function (token) { 29 | return this.el.className.indexOf(token) != -1; 30 | }, 31 | item: function (index) { 32 | return this[index] || null; 33 | }, 34 | remove: function (token) { 35 | if (!this.contains(token)) return; 36 | for (var i = 0; i < this.length; i++) { 37 | if (this[i] == token) break; 38 | } 39 | splice.call(this, i, 1); 40 | this.el.className = this.toString(); 41 | }, 42 | toString: function () { 43 | return join.call(this, ' '); 44 | }, 45 | toggle: function (token) { 46 | if (!this.contains(token)) { 47 | this.add(token); 48 | } else { 49 | this.remove(token); 50 | } 51 | 52 | return this.contains(token); 53 | } 54 | }; 55 | 56 | window.DOMTokenList = DOMTokenList; 57 | 58 | function defineElementGetter(obj, prop, getter) { 59 | if (Object.defineProperty) { 60 | Object.defineProperty(obj, prop, { 61 | get: getter 62 | }); 63 | } else { 64 | obj.__defineGetter__(prop, getter); 65 | } 66 | } 67 | 68 | defineElementGetter(HTMLElement.prototype, 'classList', function () { 69 | return new DOMTokenList(this); 70 | }); 71 | })(); 72 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/javascripts/game_manager.js: -------------------------------------------------------------------------------- 1 | function GameManager(size, InputManager, Actuator, StorageManager) { 2 | this.size = size; // Size of the grid 3 | this.inputManager = new InputManager; 4 | this.storageManager = new StorageManager; 5 | this.actuator = new Actuator; 6 | 7 | this.startTiles = 2; 8 | 9 | this.inputManager.on("move", this.move.bind(this)); 10 | this.inputManager.on("restart", this.restart.bind(this)); 11 | this.inputManager.on("keepPlaying", this.keepPlaying.bind(this)); 12 | 13 | this.setup(); 14 | } 15 | 16 | // Restart the game 17 | GameManager.prototype.restart = function () { 18 | this.storageManager.clearGameState(); 19 | this.actuator.continueGame(); // Clear the game won/lost message 20 | this.setup(); 21 | }; 22 | 23 | // Keep playing after winning (allows going over 2048) 24 | GameManager.prototype.keepPlaying = function () { 25 | this.keepPlaying = true; 26 | this.actuator.continueGame(); // Clear the game won/lost message 27 | }; 28 | 29 | // Return true if the game is lost, or has won and the user hasn't kept playing 30 | GameManager.prototype.isGameTerminated = function () { 31 | return this.over || (this.won && !this.keepPlaying); 32 | }; 33 | 34 | // Set up the game 35 | GameManager.prototype.setup = function () { 36 | var previousState = this.storageManager.getGameState(); 37 | 38 | // Reload the game from a previous game if present 39 | if (previousState) { 40 | this.grid = new Grid(previousState.grid.size, 41 | previousState.grid.cells); // Reload grid 42 | this.score = previousState.score; 43 | this.over = previousState.over; 44 | this.won = previousState.won; 45 | this.keepPlaying = previousState.keepPlaying; 46 | } else { 47 | this.grid = new Grid(this.size); 48 | this.score = 0; 49 | this.over = false; 50 | this.won = false; 51 | this.keepPlaying = false; 52 | 53 | // Add the initial tiles 54 | this.addStartTiles(); 55 | } 56 | 57 | // Update the actuator 58 | this.actuate(); 59 | }; 60 | 61 | 62 | // Set up the game 63 | GameManager.prototype.deserialize = function (json) { 64 | 65 | var last = JSON.stringify(this.storageManager.getGameState()); 66 | 67 | if (last != json) { 68 | var previousState = JSON.parse(json); 69 | 70 | // Reload the game from a previous game if present 71 | if (previousState) { 72 | this.grid = new Grid(previousState.grid.size, 73 | previousState.grid.cells); // Reload grid 74 | 75 | this.score = previousState.score; 76 | this.over = previousState.over; 77 | this.won = previousState.won; 78 | this.keepPlaying = previousState.keepPlaying; 79 | } 80 | 81 | // Update the actuator 82 | this.actuate(false); 83 | } 84 | }; 85 | 86 | // Set up the initial tiles to start the game with 87 | GameManager.prototype.addStartTiles = function () { 88 | for (var i = 0; i < this.startTiles; i++) { 89 | this.addRandomTile(); 90 | } 91 | }; 92 | 93 | // Adds a tile in a random position 94 | GameManager.prototype.addRandomTile = function () { 95 | if (this.grid.cellsAvailable()) { 96 | var value = Math.random() < 0.9 ? 2 : 4; 97 | var tile = new Tile(this.grid.randomAvailableCell(), value); 98 | 99 | this.grid.insertTile(tile); 100 | } 101 | }; 102 | 103 | // Sends the updated grid to the actuator 104 | GameManager.prototype.actuate = function (send) { 105 | if (this.storageManager.getBestScore() < this.score) { 106 | this.storageManager.setBestScore(this.score); 107 | } 108 | 109 | // Clear the state when the game is over (game over only, not win) 110 | if (this.over) { 111 | this.storageManager.clearGameState(); 112 | } else { 113 | this.storageManager.setGameState(this.serialize()); 114 | } 115 | 116 | if (send) { 117 | var xmlhttp = new XMLHttpRequest(); 118 | xmlhttp.open("POST","/game",true); 119 | xmlhttp.setRequestHeader("Content-type","text/json"); 120 | xmlhttp.send(JSON.stringify(this.serialize())); 121 | } 122 | 123 | this.actuator.actuate(this.grid, { 124 | score: this.score, 125 | over: this.over, 126 | won: this.won, 127 | bestScore: this.storageManager.getBestScore(), 128 | terminated: this.isGameTerminated() 129 | }); 130 | 131 | }; 132 | 133 | // Represent the current game as an object 134 | GameManager.prototype.serialize = function () { 135 | return { 136 | grid: this.grid.serialize(), 137 | score: this.score, 138 | over: this.over, 139 | won: this.won, 140 | keepPlaying: this.keepPlaying 141 | }; 142 | }; 143 | 144 | // Save all tile positions and remove merger info 145 | GameManager.prototype.prepareTiles = function () { 146 | this.grid.eachCell(function (x, y, tile) { 147 | if (tile) { 148 | tile.mergedFrom = null; 149 | tile.savePosition(); 150 | } 151 | }); 152 | }; 153 | 154 | // Move a tile and its representation 155 | GameManager.prototype.moveTile = function (tile, cell) { 156 | this.grid.cells[tile.x][tile.y] = null; 157 | this.grid.cells[cell.x][cell.y] = tile; 158 | tile.updatePosition(cell); 159 | }; 160 | 161 | // Move tiles on the grid in the specified direction 162 | GameManager.prototype.move = function (direction) { 163 | // 0: up, 1: right, 2: down, 3: left 164 | var self = this; 165 | 166 | if (this.isGameTerminated()) return; // Don't do anything if the game's over 167 | 168 | var cell, tile; 169 | 170 | var vector = this.getVector(direction); 171 | var traversals = this.buildTraversals(vector); 172 | var moved = false; 173 | 174 | // Save the current tile positions and remove merger information 175 | this.prepareTiles(); 176 | 177 | // Traverse the grid in the right direction and move tiles 178 | traversals.x.forEach(function (x) { 179 | traversals.y.forEach(function (y) { 180 | cell = { x: x, y: y }; 181 | tile = self.grid.cellContent(cell); 182 | 183 | if (tile) { 184 | var positions = self.findFarthestPosition(cell, vector); 185 | var next = self.grid.cellContent(positions.next); 186 | 187 | // Only one merger per row traversal? 188 | if (next && next.value === tile.value && !next.mergedFrom) { 189 | var merged = new Tile(positions.next, tile.value * 2); 190 | merged.mergedFrom = [tile, next]; 191 | 192 | self.grid.insertTile(merged); 193 | self.grid.removeTile(tile); 194 | 195 | // Converge the two tiles' positions 196 | tile.updatePosition(positions.next); 197 | 198 | // Update the score 199 | self.score += merged.value; 200 | 201 | // The mighty 2048 tile 202 | if (merged.value === 2048) self.won = true; 203 | } else { 204 | self.moveTile(tile, positions.farthest); 205 | } 206 | 207 | if (!self.positionsEqual(cell, tile)) { 208 | moved = true; // The tile moved from its original cell! 209 | } 210 | } 211 | }); 212 | }); 213 | 214 | if (moved) { 215 | this.addRandomTile(); 216 | 217 | if (!this.movesAvailable()) { 218 | this.over = true; // Game over! 219 | } 220 | 221 | this.actuate(true); 222 | } 223 | }; 224 | 225 | // Get the vector representing the chosen direction 226 | GameManager.prototype.getVector = function (direction) { 227 | // Vectors representing tile movement 228 | var map = { 229 | 0: { x: 0, y: -1 }, // Up 230 | 1: { x: 1, y: 0 }, // Right 231 | 2: { x: 0, y: 1 }, // Down 232 | 3: { x: -1, y: 0 } // Left 233 | }; 234 | 235 | return map[direction]; 236 | }; 237 | 238 | // Build a list of positions to traverse in the right order 239 | GameManager.prototype.buildTraversals = function (vector) { 240 | var traversals = { x: [], y: [] }; 241 | 242 | for (var pos = 0; pos < this.size; pos++) { 243 | traversals.x.push(pos); 244 | traversals.y.push(pos); 245 | } 246 | 247 | // Always traverse from the farthest cell in the chosen direction 248 | if (vector.x === 1) traversals.x = traversals.x.reverse(); 249 | if (vector.y === 1) traversals.y = traversals.y.reverse(); 250 | 251 | return traversals; 252 | }; 253 | 254 | GameManager.prototype.findFarthestPosition = function (cell, vector) { 255 | var previous; 256 | 257 | // Progress towards the vector direction until an obstacle is found 258 | do { 259 | previous = cell; 260 | cell = { x: previous.x + vector.x, y: previous.y + vector.y }; 261 | } while (this.grid.withinBounds(cell) && 262 | this.grid.cellAvailable(cell)); 263 | 264 | return { 265 | farthest: previous, 266 | next: cell // Used to check if a merge is required 267 | }; 268 | }; 269 | 270 | GameManager.prototype.movesAvailable = function () { 271 | return this.grid.cellsAvailable() || this.tileMatchesAvailable(); 272 | }; 273 | 274 | // Check for available matches between tiles (more expensive check) 275 | GameManager.prototype.tileMatchesAvailable = function () { 276 | var self = this; 277 | 278 | var tile; 279 | 280 | for (var x = 0; x < this.size; x++) { 281 | for (var y = 0; y < this.size; y++) { 282 | tile = this.grid.cellContent({ x: x, y: y }); 283 | 284 | if (tile) { 285 | for (var direction = 0; direction < 4; direction++) { 286 | var vector = self.getVector(direction); 287 | var cell = { x: x + vector.x, y: y + vector.y }; 288 | 289 | var other = self.grid.cellContent(cell); 290 | 291 | if (other && other.value === tile.value) { 292 | return true; // These two tiles can be merged 293 | } 294 | } 295 | } 296 | } 297 | } 298 | 299 | return false; 300 | }; 301 | 302 | GameManager.prototype.positionsEqual = function (first, second) { 303 | return first.x === second.x && first.y === second.y; 304 | }; 305 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/javascripts/grid.js: -------------------------------------------------------------------------------- 1 | function Grid(size, previousState) { 2 | this.size = size; 3 | this.cells = previousState ? this.fromState(previousState) : this.empty(); 4 | } 5 | 6 | // Build a grid of the specified size 7 | Grid.prototype.empty = function () { 8 | var cells = []; 9 | 10 | for (var x = 0; x < this.size; x++) { 11 | var row = cells[x] = []; 12 | 13 | for (var y = 0; y < this.size; y++) { 14 | row.push(null); 15 | } 16 | } 17 | 18 | return cells; 19 | }; 20 | 21 | Grid.prototype.fromState = function (state) { 22 | var cells = []; 23 | 24 | for (var x = 0; x < this.size; x++) { 25 | var row = cells[x] = []; 26 | 27 | for (var y = 0; y < this.size; y++) { 28 | var tile = state[x][y]; 29 | row.push(tile ? new Tile(tile.position, tile.value, tile.previousPosition, tile.mergedFrom) : null); 30 | } 31 | } 32 | 33 | return cells; 34 | }; 35 | 36 | // Find the first available random position 37 | Grid.prototype.randomAvailableCell = function () { 38 | var cells = this.availableCells(); 39 | 40 | if (cells.length) { 41 | return cells[Math.floor(Math.random() * cells.length)]; 42 | } 43 | }; 44 | 45 | Grid.prototype.availableCells = function () { 46 | var cells = []; 47 | 48 | this.eachCell(function (x, y, tile) { 49 | if (!tile) { 50 | cells.push({ x: x, y: y }); 51 | } 52 | }); 53 | 54 | return cells; 55 | }; 56 | 57 | // Call callback for every cell 58 | Grid.prototype.eachCell = function (callback) { 59 | for (var x = 0; x < this.size; x++) { 60 | for (var y = 0; y < this.size; y++) { 61 | callback(x, y, this.cells[x][y]); 62 | } 63 | } 64 | }; 65 | 66 | // Check if there are any cells available 67 | Grid.prototype.cellsAvailable = function () { 68 | return !!this.availableCells().length; 69 | }; 70 | 71 | // Check if the specified cell is taken 72 | Grid.prototype.cellAvailable = function (cell) { 73 | return !this.cellOccupied(cell); 74 | }; 75 | 76 | Grid.prototype.cellOccupied = function (cell) { 77 | return !!this.cellContent(cell); 78 | }; 79 | 80 | Grid.prototype.cellContent = function (cell) { 81 | if (this.withinBounds(cell)) { 82 | return this.cells[cell.x][cell.y]; 83 | } else { 84 | return null; 85 | } 86 | }; 87 | 88 | // Inserts a tile at its position 89 | Grid.prototype.insertTile = function (tile) { 90 | this.cells[tile.x][tile.y] = tile; 91 | }; 92 | 93 | Grid.prototype.removeTile = function (tile) { 94 | this.cells[tile.x][tile.y] = null; 95 | }; 96 | 97 | Grid.prototype.withinBounds = function (position) { 98 | return position.x >= 0 && position.x < this.size && 99 | position.y >= 0 && position.y < this.size; 100 | }; 101 | 102 | Grid.prototype.serialize = function () { 103 | var cellState = []; 104 | 105 | for (var x = 0; x < this.size; x++) { 106 | var row = cellState[x] = []; 107 | 108 | for (var y = 0; y < this.size; y++) { 109 | row.push(this.cells[x][y] ? this.cells[x][y].serialize() : null); 110 | } 111 | } 112 | 113 | return { 114 | size: this.size, 115 | cells: cellState 116 | }; 117 | }; 118 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/javascripts/html_actuator.js: -------------------------------------------------------------------------------- 1 | function HTMLActuator() { 2 | this.tileContainer = document.querySelector(".tile-container"); 3 | this.scoreContainer = document.querySelector(".score-container"); 4 | this.bestContainer = document.querySelector(".best-container"); 5 | this.messageContainer = document.querySelector(".game-message"); 6 | 7 | this.score = 0; 8 | } 9 | 10 | HTMLActuator.prototype.actuate = function (grid, metadata) { 11 | var self = this; 12 | 13 | window.requestAnimationFrame(function () { 14 | self.clearContainer(self.tileContainer); 15 | 16 | grid.cells.forEach(function (column) { 17 | column.forEach(function (cell) { 18 | if (cell) { 19 | self.addTile(cell); 20 | } 21 | }); 22 | }); 23 | 24 | self.updateScore(metadata.score); 25 | self.updateBestScore(metadata.bestScore); 26 | 27 | if (metadata.terminated) { 28 | if (metadata.over) { 29 | self.message(false); // You lose 30 | } else if (metadata.won) { 31 | self.message(true); // You win! 32 | } 33 | } 34 | 35 | }); 36 | }; 37 | 38 | // Continues the game (both restart and keep playing) 39 | HTMLActuator.prototype.continueGame = function () { 40 | this.clearMessage(); 41 | }; 42 | 43 | HTMLActuator.prototype.clearContainer = function (container) { 44 | while (container.firstChild) { 45 | container.removeChild(container.firstChild); 46 | } 47 | }; 48 | 49 | HTMLActuator.prototype.addTile = function (tile) { 50 | var self = this; 51 | 52 | var wrapper = document.createElement("div"); 53 | var inner = document.createElement("div"); 54 | var position = tile.previousPosition || { x: tile.x, y: tile.y }; 55 | var positionClass = this.positionClass(position); 56 | 57 | // We can't use classlist because it somehow glitches when replacing classes 58 | var classes = ["tile", "tile-" + tile.value, positionClass]; 59 | 60 | if (tile.value > 2048) classes.push("tile-super"); 61 | 62 | this.applyClasses(wrapper, classes); 63 | 64 | inner.classList.add("tile-inner"); 65 | inner.textContent = tile.value; 66 | 67 | if (tile.previousPosition) { 68 | // Make sure that the tile gets rendered in the previous position first 69 | window.requestAnimationFrame(function () { 70 | classes[2] = self.positionClass({ x: tile.x, y: tile.y }); 71 | self.applyClasses(wrapper, classes); // Update the position 72 | }); 73 | } else if (tile.mergedFrom) { 74 | classes.push("tile-merged"); 75 | this.applyClasses(wrapper, classes); 76 | 77 | // Render the tiles that merged 78 | tile.mergedFrom.forEach(function (merged) { 79 | self.addTile(merged); 80 | }); 81 | } else { 82 | classes.push("tile-new"); 83 | this.applyClasses(wrapper, classes); 84 | } 85 | 86 | // Add the inner part of the tile to the wrapper 87 | wrapper.appendChild(inner); 88 | 89 | // Put the tile on the board 90 | this.tileContainer.appendChild(wrapper); 91 | }; 92 | 93 | HTMLActuator.prototype.applyClasses = function (element, classes) { 94 | element.setAttribute("class", classes.join(" ")); 95 | }; 96 | 97 | HTMLActuator.prototype.normalizePosition = function (position) { 98 | return { x: position.x + 1, y: position.y + 1 }; 99 | }; 100 | 101 | HTMLActuator.prototype.positionClass = function (position) { 102 | position = this.normalizePosition(position); 103 | return "tile-position-" + position.x + "-" + position.y; 104 | }; 105 | 106 | HTMLActuator.prototype.updateScore = function (score) { 107 | this.clearContainer(this.scoreContainer); 108 | 109 | var difference = score - this.score; 110 | this.score = score; 111 | 112 | this.scoreContainer.textContent = this.score; 113 | 114 | if (difference > 0) { 115 | var addition = document.createElement("div"); 116 | addition.classList.add("score-addition"); 117 | addition.textContent = "+" + difference; 118 | 119 | this.scoreContainer.appendChild(addition); 120 | } 121 | }; 122 | 123 | HTMLActuator.prototype.updateBestScore = function (bestScore) { 124 | this.bestContainer.textContent = bestScore; 125 | }; 126 | 127 | HTMLActuator.prototype.message = function (won) { 128 | var type = won ? "game-won" : "game-over"; 129 | var message = won ? "You win!" : "Game over!"; 130 | 131 | this.messageContainer.classList.add(type); 132 | this.messageContainer.getElementsByTagName("p")[0].textContent = message; 133 | }; 134 | 135 | HTMLActuator.prototype.clearMessage = function () { 136 | // IE only takes one value to remove at a time. 137 | this.messageContainer.classList.remove("game-won"); 138 | this.messageContainer.classList.remove("game-over"); 139 | }; 140 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/javascripts/keyboard_input_manager.js: -------------------------------------------------------------------------------- 1 | function KeyboardInputManager() { 2 | this.events = {}; 3 | 4 | if (window.navigator.msPointerEnabled) { 5 | //Internet Explorer 10 style 6 | this.eventTouchstart = "MSPointerDown"; 7 | this.eventTouchmove = "MSPointerMove"; 8 | this.eventTouchend = "MSPointerUp"; 9 | } else { 10 | this.eventTouchstart = "touchstart"; 11 | this.eventTouchmove = "touchmove"; 12 | this.eventTouchend = "touchend"; 13 | } 14 | 15 | this.listen(); 16 | } 17 | 18 | KeyboardInputManager.prototype.on = function (event, callback) { 19 | if (!this.events[event]) { 20 | this.events[event] = []; 21 | } 22 | this.events[event].push(callback); 23 | }; 24 | 25 | KeyboardInputManager.prototype.emit = function (event, data) { 26 | var callbacks = this.events[event]; 27 | if (callbacks) { 28 | callbacks.forEach(function (callback) { 29 | callback(data); 30 | }); 31 | } 32 | }; 33 | 34 | KeyboardInputManager.prototype.listen = function () { 35 | var self = this; 36 | 37 | var map = { 38 | 38: 0, // Up 39 | 39: 1, // Right 40 | 40: 2, // Down 41 | 37: 3, // Left 42 | 75: 0, // Vim up 43 | 76: 1, // Vim right 44 | 74: 2, // Vim down 45 | 72: 3, // Vim left 46 | 87: 0, // W 47 | 68: 1, // D 48 | 83: 2, // S 49 | 65: 3 // A 50 | }; 51 | 52 | // Respond to direction keys 53 | document.addEventListener("keydown", function (event) { 54 | var modifiers = event.altKey || event.ctrlKey || event.metaKey || 55 | event.shiftKey; 56 | var mapped = map[event.which]; 57 | 58 | if (!modifiers) { 59 | if (mapped !== undefined) { 60 | event.preventDefault(); 61 | self.emit("move", mapped); 62 | } 63 | } 64 | 65 | // R key restarts the game 66 | if (!modifiers && event.which === 82) { 67 | self.restart.call(self, event); 68 | } 69 | }); 70 | 71 | // Respond to button presses 72 | this.bindButtonPress(".retry-button", this.restart); 73 | this.bindButtonPress(".restart-button", this.restart); 74 | this.bindButtonPress(".keep-playing-button", this.keepPlaying); 75 | 76 | // Respond to swipe events 77 | var touchStartClientX, touchStartClientY; 78 | var gameContainer = document.getElementsByClassName("game-container")[0]; 79 | 80 | gameContainer.addEventListener(this.eventTouchstart, function (event) { 81 | if ((!window.navigator.msPointerEnabled && event.touches.length > 1) || 82 | event.targetTouches > 1) { 83 | return; // Ignore if touching with more than 1 finger 84 | } 85 | 86 | if (window.navigator.msPointerEnabled) { 87 | touchStartClientX = event.pageX; 88 | touchStartClientY = event.pageY; 89 | } else { 90 | touchStartClientX = event.touches[0].clientX; 91 | touchStartClientY = event.touches[0].clientY; 92 | } 93 | 94 | event.preventDefault(); 95 | }); 96 | 97 | gameContainer.addEventListener(this.eventTouchmove, function (event) { 98 | event.preventDefault(); 99 | }); 100 | 101 | gameContainer.addEventListener(this.eventTouchend, function (event) { 102 | if ((!window.navigator.msPointerEnabled && event.touches.length > 0) || 103 | event.targetTouches > 0) { 104 | return; // Ignore if still touching with one or more fingers 105 | } 106 | 107 | var touchEndClientX, touchEndClientY; 108 | 109 | if (window.navigator.msPointerEnabled) { 110 | touchEndClientX = event.pageX; 111 | touchEndClientY = event.pageY; 112 | } else { 113 | touchEndClientX = event.changedTouches[0].clientX; 114 | touchEndClientY = event.changedTouches[0].clientY; 115 | } 116 | 117 | var dx = touchEndClientX - touchStartClientX; 118 | var absDx = Math.abs(dx); 119 | 120 | var dy = touchEndClientY - touchStartClientY; 121 | var absDy = Math.abs(dy); 122 | 123 | if (Math.max(absDx, absDy) > 10) { 124 | // (right : left) : (down : up) 125 | self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0)); 126 | } 127 | }); 128 | }; 129 | 130 | KeyboardInputManager.prototype.restart = function (event) { 131 | event.preventDefault(); 132 | this.emit("restart"); 133 | }; 134 | 135 | KeyboardInputManager.prototype.keepPlaying = function (event) { 136 | event.preventDefault(); 137 | this.emit("keepPlaying"); 138 | }; 139 | 140 | KeyboardInputManager.prototype.bindButtonPress = function (selector, fn) { 141 | var button = document.querySelector(selector); 142 | button.addEventListener("click", fn.bind(this)); 143 | button.addEventListener(this.eventTouchend, fn.bind(this)); 144 | }; 145 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/javascripts/local_storage_manager.js: -------------------------------------------------------------------------------- 1 | window.fakeStorage = { 2 | _data: {}, 3 | 4 | setItem: function (id, val) { 5 | return this._data[id] = String(val); 6 | }, 7 | 8 | getItem: function (id) { 9 | return this._data.hasOwnProperty(id) ? this._data[id] : undefined; 10 | }, 11 | 12 | removeItem: function (id) { 13 | return delete this._data[id]; 14 | }, 15 | 16 | clear: function () { 17 | return this._data = {}; 18 | } 19 | }; 20 | 21 | function LocalStorageManager() { 22 | this.bestScoreKey = "bestScore"; 23 | this.gameStateKey = "gameState"; 24 | 25 | var supported = this.localStorageSupported(); 26 | this.storage = supported ? window.localStorage : window.fakeStorage; 27 | } 28 | 29 | LocalStorageManager.prototype.localStorageSupported = function () { 30 | var testKey = "test"; 31 | var storage = window.localStorage; 32 | 33 | try { 34 | storage.setItem(testKey, "1"); 35 | storage.removeItem(testKey); 36 | return true; 37 | } catch (error) { 38 | return false; 39 | } 40 | }; 41 | 42 | // Best score getters/setters 43 | LocalStorageManager.prototype.getBestScore = function () { 44 | return this.storage.getItem(this.bestScoreKey) || 0; 45 | }; 46 | 47 | LocalStorageManager.prototype.setBestScore = function (score) { 48 | this.storage.setItem(this.bestScoreKey, score); 49 | }; 50 | 51 | // Game state getters/setters and clearing 52 | LocalStorageManager.prototype.getGameState = function () { 53 | var stateJSON = this.storage.getItem(this.gameStateKey); 54 | return stateJSON ? JSON.parse(stateJSON) : null; 55 | }; 56 | 57 | LocalStorageManager.prototype.setGameState = function (gameState) { 58 | this.storage.setItem(this.gameStateKey, JSON.stringify(gameState)); 59 | }; 60 | 61 | LocalStorageManager.prototype.clearGameState = function () { 62 | this.storage.removeItem(this.gameStateKey); 63 | }; 64 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/javascripts/tile.js: -------------------------------------------------------------------------------- 1 | function Tile(position, value, previousPosition, mergedFrom) { 2 | this.x = position.x; 3 | this.y = position.y; 4 | this.value = value || 2; 5 | 6 | this.previousPosition = previousPosition; 7 | this.mergedFrom = mergedFrom; // Tracks tiles that merged together 8 | } 9 | 10 | Tile.prototype.savePosition = function () { 11 | this.previousPosition = { x: this.x, y: this.y }; 12 | }; 13 | 14 | Tile.prototype.updatePosition = function (position) { 15 | this.x = position.x; 16 | this.y = position.y; 17 | }; 18 | 19 | Tile.prototype.serialize = function () { 20 | return { 21 | position: { 22 | x: this.x, 23 | y: this.y 24 | }, 25 | value: this.value, 26 | previousPosition: this.previousPosition, 27 | mergedFrom: this.mergedFrom 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/meta/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siderakis/playframework-appengine/b09d47c580677803d3458c8d91b69ebf02dff393/samples/2048/src/main/webapp/meta/apple-touch-icon.png -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/meta/apple-touch-startup-image-640x1096.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siderakis/playframework-appengine/b09d47c580677803d3458c8d91b69ebf02dff393/samples/2048/src/main/webapp/meta/apple-touch-startup-image-640x1096.png -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/meta/apple-touch-startup-image-640x920.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siderakis/playframework-appengine/b09d47c580677803d3458c8d91b69ebf02dff393/samples/2048/src/main/webapp/meta/apple-touch-startup-image-640x920.png -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/stylesheets/fonts/ClearSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siderakis/playframework-appengine/b09d47c580677803d3458c8d91b69ebf02dff393/samples/2048/src/main/webapp/stylesheets/fonts/ClearSans-Bold-webfont.eot -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/stylesheets/fonts/ClearSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siderakis/playframework-appengine/b09d47c580677803d3458c8d91b69ebf02dff393/samples/2048/src/main/webapp/stylesheets/fonts/ClearSans-Bold-webfont.woff -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/stylesheets/fonts/ClearSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siderakis/playframework-appengine/b09d47c580677803d3458c8d91b69ebf02dff393/samples/2048/src/main/webapp/stylesheets/fonts/ClearSans-Light-webfont.eot -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/stylesheets/fonts/ClearSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siderakis/playframework-appengine/b09d47c580677803d3458c8d91b69ebf02dff393/samples/2048/src/main/webapp/stylesheets/fonts/ClearSans-Light-webfont.woff -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/stylesheets/fonts/ClearSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siderakis/playframework-appengine/b09d47c580677803d3458c8d91b69ebf02dff393/samples/2048/src/main/webapp/stylesheets/fonts/ClearSans-Regular-webfont.eot -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/stylesheets/fonts/ClearSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siderakis/playframework-appengine/b09d47c580677803d3458c8d91b69ebf02dff393/samples/2048/src/main/webapp/stylesheets/fonts/ClearSans-Regular-webfont.woff -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/stylesheets/fonts/clear-sans.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Clear Sans"; 3 | src: url("ClearSans-Light-webfont.eot"); 4 | src: url("ClearSans-Light-webfont.eot?#iefix") format("embedded-opentype"), 5 | url("ClearSans-Light-webfont.svg#clear_sans_lightregular") format("svg"), 6 | url("ClearSans-Light-webfont.woff") format("woff"); 7 | font-weight: 200; 8 | font-style: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: "Clear Sans"; 13 | src: url("ClearSans-Regular-webfont.eot"); 14 | src: url("ClearSans-Regular-webfont.eot?#iefix") format("embedded-opentype"), 15 | url("ClearSans-Regular-webfont.svg#clear_sansregular") format("svg"), 16 | url("ClearSans-Regular-webfont.woff") format("woff"); 17 | font-weight: normal; 18 | font-style: normal; 19 | } 20 | 21 | @font-face { 22 | font-family: "Clear Sans"; 23 | src: url("ClearSans-Bold-webfont.eot"); 24 | src: url("ClearSans-Bold-webfont.eot?#iefix") format("embedded-opentype"), 25 | url("ClearSans-Bold-webfont.svg#clear_sansbold") format("svg"), 26 | url("ClearSans-Bold-webfont.woff") format("woff"); 27 | font-weight: 700; 28 | font-style: normal; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/stylesheets/helpers.scss: -------------------------------------------------------------------------------- 1 | // Exponent 2 | // From: https://github.com/Team-Sass/Sassy-math/blob/master/sass/math.scss#L36 3 | 4 | @function exponent($base, $exponent) { 5 | // reset value 6 | $value: $base; 7 | // positive intergers get multiplied 8 | @if $exponent > 1 { 9 | @for $i from 2 through $exponent { 10 | $value: $value * $base; } } 11 | // negitive intergers get divided. A number divided by itself is 1 12 | @if $exponent < 1 { 13 | @for $i from 0 through -$exponent { 14 | $value: $value / $base; } } 15 | // return the last value written 16 | @return $value; 17 | } 18 | 19 | @function pow($base, $exponent) { 20 | @return exponent($base, $exponent); 21 | } 22 | 23 | // Transition mixins 24 | @mixin transition($args...) { 25 | -webkit-transition: $args; 26 | -moz-transition: $args; 27 | transition: $args; 28 | } 29 | 30 | @mixin transition-property($args...) { 31 | -webkit-transition-property: $args; 32 | -moz-transition-property: $args; 33 | transition-property: $args; 34 | } 35 | 36 | @mixin animation($args...) { 37 | -webkit-animation: $args; 38 | -moz-animation: $args; 39 | animation: $args; 40 | } 41 | 42 | @mixin animation-fill-mode($args...) { 43 | -webkit-animation-fill-mode: $args; 44 | -moz-animation-fill-mode: $args; 45 | animation-fill-mode: $args; 46 | } 47 | 48 | @mixin transform($args...) { 49 | -webkit-transform: $args; 50 | -moz-transform: $args; 51 | transform: $args; 52 | } 53 | 54 | // Keyframe animations 55 | @mixin keyframes($animation-name) { 56 | @-webkit-keyframes $animation-name { 57 | @content; 58 | } 59 | @-moz-keyframes $animation-name { 60 | @content; 61 | } 62 | @keyframes $animation-name { 63 | @content; 64 | } 65 | } 66 | 67 | // Media queries 68 | @mixin smaller($width) { 69 | @media screen and (max-width: $width) { 70 | @content; 71 | } 72 | } 73 | 74 | // Clearfix 75 | @mixin clearfix { 76 | &:after { 77 | content: ""; 78 | display: block; 79 | clear: both; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /samples/2048/src/main/webapp/stylesheets/main.scss: -------------------------------------------------------------------------------- 1 | @import "helpers"; 2 | @import "fonts/clear-sans.css"; 3 | 4 | $field-width: 500px; 5 | $grid-spacing: 15px; 6 | $grid-row-cells: 4; 7 | $tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells; 8 | $tile-border-radius: 3px; 9 | 10 | $mobile-threshold: $field-width + 20px; 11 | 12 | $text-color: #776E65; 13 | $bright-text-color: #f9f6f2; 14 | 15 | $tile-color: #eee4da; 16 | $tile-gold-color: #edc22e; 17 | $tile-gold-glow-color: lighten($tile-gold-color, 15%); 18 | 19 | $game-container-margin-top: 40px; 20 | $game-container-background: #bbada0; 21 | 22 | $transition-speed: 100ms; 23 | 24 | html, body { 25 | margin: 0; 26 | padding: 0; 27 | 28 | background: #faf8ef; 29 | color: $text-color; 30 | font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif; 31 | font-size: 18px; 32 | } 33 | 34 | body { 35 | margin: 80px 0; 36 | } 37 | 38 | .heading { 39 | @include clearfix; 40 | } 41 | 42 | h1.title { 43 | font-size: 80px; 44 | font-weight: bold; 45 | margin: 0; 46 | display: block; 47 | float: left; 48 | } 49 | 50 | @include keyframes(move-up) { 51 | 0% { 52 | top: 25px; 53 | opacity: 1; 54 | } 55 | 56 | 100% { 57 | top: -50px; 58 | opacity: 0; 59 | } 60 | } 61 | 62 | .scores-container { 63 | float: right; 64 | text-align: right; 65 | } 66 | 67 | .score-container, .best-container { 68 | $height: 25px; 69 | 70 | position: relative; 71 | display: inline-block; 72 | background: $game-container-background; 73 | padding: 15px 25px; 74 | font-size: $height; 75 | height: $height; 76 | line-height: $height + 22px; 77 | font-weight: bold; 78 | border-radius: 3px; 79 | color: white; 80 | margin-top: 8px; 81 | text-align: center; 82 | 83 | &:after { 84 | position: absolute; 85 | width: 100%; 86 | top: 10px; 87 | left: 0; 88 | text-transform: uppercase; 89 | font-size: 13px; 90 | line-height: 13px; 91 | text-align: center; 92 | color: $tile-color; 93 | } 94 | 95 | .score-addition { 96 | position: absolute; 97 | right: 30px; 98 | color: red; 99 | font-size: $height; 100 | line-height: $height; 101 | font-weight: bold; 102 | color: rgba($text-color, .9); 103 | z-index: 100; 104 | @include animation(move-up 600ms ease-in); 105 | @include animation-fill-mode(both); 106 | } 107 | } 108 | 109 | .score-container:after { 110 | content: "Score"; 111 | } 112 | 113 | .best-container:after { 114 | content: "Best" 115 | } 116 | 117 | p { 118 | margin-top: 0; 119 | margin-bottom: 10px; 120 | line-height: 1.65; 121 | } 122 | 123 | a { 124 | color: $text-color; 125 | font-weight: bold; 126 | text-decoration: underline; 127 | cursor: pointer; 128 | } 129 | 130 | strong { 131 | &.important { 132 | text-transform: uppercase; 133 | } 134 | } 135 | 136 | hr { 137 | border: none; 138 | border-bottom: 1px solid lighten($text-color, 40%); 139 | margin-top: 20px; 140 | margin-bottom: 30px; 141 | } 142 | 143 | .container { 144 | width: $field-width; 145 | margin: 0 auto; 146 | } 147 | 148 | @include keyframes(fade-in) { 149 | 0% { 150 | opacity: 0; 151 | } 152 | 153 | 100% { 154 | opacity: 1; 155 | } 156 | } 157 | 158 | // Styles for buttons 159 | @mixin button { 160 | display: inline-block; 161 | background: darken($game-container-background, 20%); 162 | border-radius: 3px; 163 | padding: 0 20px; 164 | text-decoration: none; 165 | color: $bright-text-color; 166 | height: 40px; 167 | line-height: 42px; 168 | } 169 | 170 | // Game field mixin used to render CSS at different width 171 | @mixin game-field { 172 | .game-container { 173 | margin-top: $game-container-margin-top; 174 | position: relative; 175 | padding: $grid-spacing; 176 | 177 | cursor: default; 178 | -webkit-touch-callout: none; 179 | -ms-touch-callout: none; 180 | 181 | -webkit-user-select: none; 182 | -moz-user-select: none; 183 | -ms-user-select: none; 184 | 185 | -ms-touch-action: none; 186 | touch-action: none; 187 | 188 | background: $game-container-background; 189 | border-radius: $tile-border-radius * 2; 190 | width: $field-width; 191 | height: $field-width; 192 | -webkit-box-sizing: border-box; 193 | -moz-box-sizing: border-box; 194 | box-sizing: border-box; 195 | 196 | .game-message { 197 | display: none; 198 | 199 | position: absolute; 200 | top: 0; 201 | right: 0; 202 | bottom: 0; 203 | left: 0; 204 | background: rgba($tile-color, .5); 205 | z-index: 100; 206 | 207 | text-align: center; 208 | 209 | p { 210 | font-size: 60px; 211 | font-weight: bold; 212 | height: 60px; 213 | line-height: 60px; 214 | margin-top: 222px; 215 | // height: $field-width; 216 | // line-height: $field-width; 217 | } 218 | 219 | .lower { 220 | display: block; 221 | margin-top: 59px; 222 | } 223 | 224 | a { 225 | @include button; 226 | margin-left: 9px; 227 | // margin-top: 59px; 228 | 229 | &.keep-playing-button { 230 | display: none; 231 | } 232 | } 233 | 234 | @include animation(fade-in 800ms ease $transition-speed * 12); 235 | @include animation-fill-mode(both); 236 | 237 | &.game-won { 238 | background: rgba($tile-gold-color, .5); 239 | color: $bright-text-color; 240 | 241 | a.keep-playing-button { 242 | display: inline-block; 243 | } 244 | } 245 | 246 | &.game-won, &.game-over { 247 | display: block; 248 | } 249 | } 250 | } 251 | 252 | .grid-container { 253 | position: absolute; 254 | z-index: 1; 255 | } 256 | 257 | .grid-row { 258 | margin-bottom: $grid-spacing; 259 | 260 | &:last-child { 261 | margin-bottom: 0; 262 | } 263 | 264 | &:after { 265 | content: ""; 266 | display: block; 267 | clear: both; 268 | } 269 | } 270 | 271 | .grid-cell { 272 | width: $tile-size; 273 | height: $tile-size; 274 | margin-right: $grid-spacing; 275 | float: left; 276 | 277 | border-radius: $tile-border-radius; 278 | 279 | background: rgba($tile-color, .35); 280 | 281 | &:last-child { 282 | margin-right: 0; 283 | } 284 | } 285 | 286 | .tile-container { 287 | position: absolute; 288 | z-index: 2; 289 | } 290 | 291 | .tile { 292 | &, .tile-inner { 293 | width: ceil($tile-size); 294 | height: ceil($tile-size); 295 | line-height: $tile-size + 10px; 296 | } 297 | 298 | // Build position classes 299 | @for $x from 1 through $grid-row-cells { 300 | @for $y from 1 through $grid-row-cells { 301 | &.tile-position-#{$x}-#{$y} { 302 | $xPos: floor(($tile-size + $grid-spacing) * ($x - 1)); 303 | $yPos: floor(($tile-size + $grid-spacing) * ($y - 1)); 304 | @include transform(translate($xPos, $yPos)); 305 | } 306 | } 307 | } 308 | } 309 | } 310 | 311 | // End of game-field mixin 312 | @include game-field; 313 | 314 | .tile { 315 | position: absolute; // Makes transforms relative to the top-left corner 316 | 317 | .tile-inner { 318 | border-radius: $tile-border-radius; 319 | 320 | background: $tile-color; 321 | text-align: center; 322 | font-weight: bold; 323 | z-index: 10; 324 | 325 | font-size: 55px; 326 | } 327 | 328 | // Movement transition 329 | @include transition($transition-speed ease-in-out); 330 | -webkit-transition-property: -webkit-transform; 331 | -moz-transition-property: -moz-transform; 332 | transition-property: transform; 333 | 334 | $base: 2; 335 | $exponent: 1; 336 | $limit: 11; 337 | 338 | // Colors for all 11 states, false = no special color 339 | $special-colors: false false, // 2 340 | false false, // 4 341 | #f78e48 true, // 8 342 | #fc5e2e true, // 16 343 | #ff3333 true, // 32 344 | #ff0000 true, // 64 345 | false true, // 128 346 | false true, // 256 347 | false true, // 512 348 | false true, // 1024 349 | false true; // 2048 350 | 351 | // Build tile colors 352 | @while $exponent <= $limit { 353 | $power: pow($base, $exponent); 354 | 355 | &.tile-#{$power} .tile-inner { 356 | // Calculate base background color 357 | $gold-percent: ($exponent - 1) / ($limit - 1) * 100; 358 | $mixed-background: mix($tile-gold-color, $tile-color, $gold-percent); 359 | 360 | $nth-color: nth($special-colors, $exponent); 361 | 362 | $special-background: nth($nth-color, 1); 363 | $bright-color: nth($nth-color, 2); 364 | 365 | @if $special-background { 366 | $mixed-background: mix($special-background, $mixed-background, 55%); 367 | } 368 | 369 | @if $bright-color { 370 | color: $bright-text-color; 371 | } 372 | 373 | // Set background 374 | background: $mixed-background; 375 | 376 | // Add glow 377 | $glow-opacity: max($exponent - 4, 0) / ($limit - 4); 378 | 379 | @if not $special-background { 380 | box-shadow: 0 0 30px 10px rgba($tile-gold-glow-color, $glow-opacity / 1.8), 381 | inset 0 0 0 1px rgba(white, $glow-opacity / 3); 382 | } 383 | 384 | // Adjust font size for bigger numbers 385 | @if $power >= 100 and $power < 1000 { 386 | font-size: 45px; 387 | 388 | // Media queries placed here to avoid carrying over the rest of the logic 389 | @include smaller($mobile-threshold) { 390 | font-size: 25px; 391 | } 392 | } @else if $power >= 1000 { 393 | font-size: 35px; 394 | 395 | @include smaller($mobile-threshold) { 396 | font-size: 15px; 397 | } 398 | } 399 | } 400 | 401 | $exponent: $exponent + 1; 402 | } 403 | 404 | // Super tiles (above 2048) 405 | &.tile-super .tile-inner { 406 | color: $bright-text-color; 407 | background: mix(#333, $tile-gold-color, 95%); 408 | 409 | font-size: 30px; 410 | 411 | @include smaller($mobile-threshold) { 412 | font-size: 10px; 413 | } 414 | } 415 | } 416 | 417 | @include keyframes(appear) { 418 | 0% { 419 | opacity: 0; 420 | @include transform(scale(0)); 421 | } 422 | 423 | 100% { 424 | opacity: 1; 425 | @include transform(scale(1)); 426 | } 427 | } 428 | 429 | .tile-new .tile-inner { 430 | @include animation(appear 200ms ease $transition-speed); 431 | @include animation-fill-mode(backwards); 432 | } 433 | 434 | @include keyframes(pop) { 435 | 0% { 436 | @include transform(scale(0)); 437 | } 438 | 439 | 50% { 440 | @include transform(scale(1.2)); 441 | } 442 | 443 | 100% { 444 | @include transform(scale(1)); 445 | } 446 | } 447 | 448 | .tile-merged .tile-inner { 449 | z-index: 20; 450 | @include animation(pop 200ms ease $transition-speed); 451 | @include animation-fill-mode(backwards); 452 | } 453 | 454 | .above-game { 455 | @include clearfix; 456 | } 457 | 458 | .game-intro { 459 | float: left; 460 | line-height: 42px; 461 | margin-bottom: 0; 462 | } 463 | 464 | .restart-button { 465 | @include button; 466 | display: block; 467 | text-align: center; 468 | float: right; 469 | } 470 | 471 | .game-explanation { 472 | margin-top: 50px; 473 | } 474 | 475 | @include smaller($mobile-threshold) { 476 | // Redefine variables for smaller screens 477 | $field-width: 280px; 478 | $grid-spacing: 10px; 479 | $grid-row-cells: 4; 480 | $tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells; 481 | $tile-border-radius: 3px; 482 | $game-container-margin-top: 17px; 483 | 484 | html, body { 485 | font-size: 15px; 486 | } 487 | 488 | body { 489 | margin: 20px 0; 490 | padding: 0 20px; 491 | } 492 | 493 | h1.title { 494 | font-size: 27px; 495 | margin-top: 15px; 496 | } 497 | 498 | .container { 499 | width: $field-width; 500 | margin: 0 auto; 501 | } 502 | 503 | .score-container, .best-container { 504 | margin-top: 0; 505 | padding: 15px 10px; 506 | min-width: 40px; 507 | } 508 | 509 | .heading { 510 | margin-bottom: 10px; 511 | } 512 | 513 | // Show intro and restart button side by side 514 | .game-intro { 515 | width: 55%; 516 | display: block; 517 | box-sizing: border-box; 518 | line-height: 1.65; 519 | } 520 | 521 | .restart-button { 522 | width: 42%; 523 | padding: 0; 524 | display: block; 525 | box-sizing: border-box; 526 | margin-top: 2px; 527 | } 528 | 529 | // Render the game field at the right width 530 | @include game-field; 531 | 532 | // Rest of the font-size adjustments in the tile class 533 | .tile .tile-inner { 534 | font-size: 35px; 535 | } 536 | 537 | .game-message { 538 | p { 539 | font-size: 30px !important; 540 | height: 30px !important; 541 | line-height: 30px !important; 542 | margin-top: 90px !important; 543 | } 544 | 545 | .lower { 546 | margin-top: 30px !important; 547 | } 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /samples/basic/build.sbt: -------------------------------------------------------------------------------- 1 | import sbtappengine.Plugin.{AppengineKeys => gae} 2 | 3 | import play.PlayProject 4 | 5 | name := "PlayFramework-AppEngine" 6 | 7 | scalaVersion := "2.10.2" 8 | 9 | resolvers += "Scala AppEngine Sbt Repo" at "http://siderakis.github.com/maven" 10 | 11 | libraryDependencies ++= Seq( 12 | "com.siderakis" %% "futuraes" % "0.1-SNAPSHOT", 13 | "com.siderakis" %% "playframework-appengine-mvc" % "0.2-SNAPSHOT", 14 | "javax.servlet" % "servlet-api" % "2.5" % "provided", 15 | "org.mortbay.jetty" % "jetty" % "6.1.22" % "container" 16 | ) 17 | 18 | appengineSettings 19 | 20 | (gae.onStartHooks in gae.devServer in Compile) += { () => 21 | println("hello") 22 | } 23 | 24 | (gae.onStopHooks in gae.devServer in Compile) += { () => 25 | println("bye") 26 | } 27 | 28 | PlayProject.defaultPlaySettings 29 | -------------------------------------------------------------------------------- /samples/basic/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.0-RC5 2 | -------------------------------------------------------------------------------- /samples/basic/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.10.3" 2 | 3 | resolvers += "Scala AppEngine Sbt Repo" at "http://siderakis.github.com/maven" 4 | 5 | addSbtPlugin("com.siderakis" %% "playframework-appengine-routes" % "0.2-SNAPSHOT") 6 | 7 | addSbtPlugin("com.eed3si9n" % "sbt-appengine" % "0.6.2") 8 | -------------------------------------------------------------------------------- /samples/basic/src/main/conf/routes: -------------------------------------------------------------------------------- 1 | GET / controllers.PlayController.index 2 | GET /speed controllers.PlayController.speed(name: String ?= "anonymous") 3 | GET /hello/:name controllers.PlayController.meow(name: String) 4 | -------------------------------------------------------------------------------- /samples/basic/src/main/scala/ApplicationController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api.mvc._ 4 | import play.{Result, Controller} 5 | import javax.servlet.http.{HttpServletResponse, HttpServletRequest} 6 | import com.google.appengine.api.urlfetch.URLFetchServiceFactory 7 | import java.net.URL 8 | import scala.concurrent.ExecutionContextAE 9 | import ExecutionContextAE.ImplicitsAE._ 10 | import scala.concurrent._ 11 | import com.google.appengine.api.users.{UserServiceFactory, User} 12 | import play.api.mvc.DiscardingCookie 13 | import play.api.mvc.Cookie 14 | 15 | /** 16 | * User: nick 17 | * Date: 11/22/13 18 | */ 19 | object PlayController extends Controller { 20 | 21 | def index = Action { 22 | Ok("Simple Index").as("text") 23 | } 24 | 25 | def speed(name: String) = Action.async { 26 | 27 | implicit def f2future[T](f: java.util.concurrent.Future[T]) = future(f.get) 28 | 29 | val ws = URLFetchServiceFactory.getURLFetchService 30 | 31 | // http://engineering.linkedin.com/play/play-framework-linkedin 32 | val start = System.currentTimeMillis() 33 | def getLatency(r: Any): Long = System.currentTimeMillis() - start 34 | val googleTime = ws.fetchAsync(new URL("http://www.google.com")).map(getLatency) 35 | val yahooTime = ws.fetchAsync(new URL("http://www.yahoo.com")).map(getLatency) 36 | val bingTime = ws.fetchAsync(new URL("http://www.bing.com")).map(getLatency) 37 | 38 | Future.sequence(Seq(googleTime, yahooTime, bingTime)).map { 39 | case times => 40 | Ok(s"

hello $name,

here is some data:" + 41 | Map("google" -> times(0), "yahoo" -> times(1), "bing" -> times(2)).mapValues(_ + "ms").mkString("
  • ", "
  • ", "
") 42 | ) 43 | } 44 | } 45 | 46 | 47 | def meow(name: String) = Action { 48 | request => 49 | 50 | //can access HttpServletRequest and HttpServletResponse directly 51 | val servletReq: HttpServletRequest = request.req 52 | val contentType = servletReq.getContentType 53 | 54 | if (name == "kitty") 55 | Accepted(s"$name says meow"). 56 | withCookies(new Cookie("i_can_haz", "cookies")). 57 | withHeaders(CONTENT_TYPE -> contentType) 58 | else 59 | NotAcceptable(s"sorry $name, kitties only"). 60 | withHeaders("do_try" -> "kitty"). 61 | discardingCookies(DiscardingCookie("i_can_haz")). 62 | as("text/html") 63 | 64 | } 65 | 66 | /** 67 | * Wrap an existing request. Useful to extend a request. 68 | */ 69 | class WrappedRequest(request: RequestHeader) extends Request { 70 | def queryString = request.queryString 71 | 72 | def path = request.path 73 | 74 | def method = request.method 75 | 76 | def req: HttpServletRequest = request.req 77 | 78 | def resp: HttpServletResponse = request.resp 79 | } 80 | 81 | 82 | } 83 | -------------------------------------------------------------------------------- /samples/basic/src/main/webapp/WEB-INF/appengine-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ns-labs 4 | playframework-appengine 5 | true 6 | 7 | -------------------------------------------------------------------------------- /samples/basic/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | PlayAppEngineServlet 7 | play.PlayAppEngineServlet 8 | 9 | 10 | PlayAppEngineServlet 11 | /* 12 | 13 | 14 | PlayAppEngineServlet 15 | / 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/realtime-clock/build.sbt: -------------------------------------------------------------------------------- 1 | import sbtappengine.Plugin.{AppengineKeys => gae} 2 | 3 | import play.PlayProject 4 | 5 | name := "RealTime-Clock" 6 | 7 | scalaVersion := "2.10.2" 8 | 9 | resolvers += "Scala AppEngine Sbt Repo" at "http://siderakis.github.com/maven" 10 | 11 | resolvers += "Typesafe repository releases" at "http://repo.typesafe.com/typesafe/releases/" 12 | 13 | libraryDependencies ++= Seq( 14 | "com.siderakis" %% "futuraes" % "0.1-SNAPSHOT", 15 | "com.siderakis" %% "playframework-appengine-mvc" % "0.2-SNAPSHOT", 16 | "javax.servlet" % "servlet-api" % "2.5" % "provided", 17 | "org.mortbay.jetty" % "jetty" % "6.1.22" % "container", 18 | "play" %% "play-iteratees" % "2.1.5", 19 | "com.siderakis" %% "futuraes" % "0.1-SNAPSHOT" 20 | ) 21 | 22 | appengineSettings 23 | 24 | PlayProject.defaultPlaySettings 25 | 26 | Twirl.settings 27 | 28 | -------------------------------------------------------------------------------- /samples/realtime-clock/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.0-RC5 2 | -------------------------------------------------------------------------------- /samples/realtime-clock/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := "2.10.3" 2 | 3 | resolvers += "Scala AppEngine Sbt Repo" at "http://siderakis.github.com/maven" 4 | 5 | addSbtPlugin("com.siderakis" %% "playframework-appengine-routes" % "0.2-SNAPSHOT") 6 | 7 | addSbtPlugin("com.eed3si9n" % "sbt-appengine" % "0.6.2") 8 | 9 | // needed because sbt-twirl depends on twirl-compiler which is only available 10 | // at repo.spray.io 11 | resolvers += "spray repo" at "http://repo.spray.io" 12 | 13 | addSbtPlugin("io.spray" % "sbt-twirl" % "0.7.0") 14 | -------------------------------------------------------------------------------- /samples/realtime-clock/src/main/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.Application.index 7 | 8 | # The clock Comet stream 9 | GET /clock controllers.Application.liveClock 10 | 11 | 12 | POST /_ah/channel/connected/ controllers.Application.connected 13 | POST /_ah/channel/disconnected/ controllers.Application.connected 14 | 15 | # Map static resources from the /public folder to the /assets URL path 16 | # GET /assets/*file controllers.Assets.at(path="/public", file) 17 | -------------------------------------------------------------------------------- /samples/realtime-clock/src/main/scala/controllers/ApplicationController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import play.api._ 4 | import play.api.mvc._ 5 | import com.google.appengine.api.channel.{ChannelFailureException, ChannelMessage, ChannelServiceFactory} 6 | import java.util.Date 7 | import twirl.api.Html 8 | import com.google.appengine.api.ThreadManager 9 | import play.api.libs.iteratee 10 | import scala.util.{Failure, Success, Try} 11 | import scala.Some 12 | import scala.Some 13 | import play.api.libs.iteratee.Enumeratee.CheckDone 14 | import scala.collection.mutable 15 | 16 | //import play.api.libs.Comet 17 | 18 | import play.api.libs.iteratee._ 19 | 20 | //import play.api.libs.concurrent._ 21 | //import play.{Result, Controller} 22 | 23 | import play.api.mvc._ 24 | import play.{Result, Controller} 25 | import javax.servlet.http.{HttpServletResponse, HttpServletRequest} 26 | import com.google.appengine.api.urlfetch.URLFetchServiceFactory 27 | import java.net.URL 28 | import scala.concurrent._ 29 | 30 | // import ExecutionContextAE.ImplicitsAE._ 31 | 32 | import play.api.mvc.DiscardingCookie 33 | import play.api.mvc.Cookie 34 | import scala.concurrent.duration._ 35 | import scala.concurrent.duration.FiniteDuration 36 | 37 | //import play.api.libs.concurrent.Execution.Implicits.defaultContext 38 | 39 | import ExecutionContextAE.ImplicitsAE._ 40 | 41 | 42 | //https://developers.google.com/appengine/docs/java/modules/#Java_Background_threads 43 | //https://developers.google.com/appengine/docs/java/channel/ 44 | //https://www.playframework.com/documentation/2.2.x/Enumerators 45 | //https://github.com/playframework/playframework/blob/321af079941f64cdd2cf32b407d4026f7e49dfec/framework/src/play/src/main/scala/play/api/mvc/Results.scala 46 | 47 | object Application extends Controller { 48 | 49 | case class Channel(userId: Long, connected: mutable.Seq[Boolean]){ 50 | 51 | 52 | } 53 | 54 | trait ChannelManager { 55 | 56 | def getFreeClient(user: String): String 57 | 58 | def onPresentsChange(clientId: String, connected: Boolean): Unit 59 | 60 | def sendMessage(user: String, message: String): Unit 61 | 62 | 63 | } 64 | 65 | object ChannelManagerMemory extends ChannelManager{ 66 | def getFreeClient(user: String): String = ??? 67 | 68 | def onPresentsChange(clientId: String, connected: Boolean): Unit = ??? 69 | 70 | def sendMessage(user: String, message: String): 71 | } 72 | 73 | /** 74 | * A String Enumerator producing a formatted Time message every 100 millis. 75 | * A callback enumerator is pure an can be applied on several Iteratee. 76 | */ 77 | lazy val clock: Enumerator[String] = { 78 | 79 | import java.util._ 80 | import java.text._ 81 | import scala.concurrent.Promise 82 | val dateFormat = new SimpleDateFormat("HH mm ss") 83 | 84 | Enumerator.generateM[String] { 85 | println("CLOCK TICK") 86 | // Promise.timeout(Some(dateFormat.format(new Date)), 1000 milliseconds) 87 | 88 | val p = Promise[Option[String]]() 89 | Thread.sleep(1000) 90 | p.complete(Try(if (stillConnected("foo")) Some(dateFormat.format(new Date)) else None)) 91 | p.future 92 | } 93 | } 94 | 95 | def stillConnected(name: String) = present.contains(name) 96 | 97 | def connected() = Action { 98 | req => 99 | val who = ChannelServiceFactory.getChannelService.parsePresence(req.req) 100 | val did = if (who.isConnected) "connected" else "disconnected" 101 | println(s"${who.clientId} just $did") 102 | if (who.isConnected) { 103 | // store connected state for each token? 104 | doBackgroundStuff(who.clientId()) 105 | } else { 106 | present.update("foo", present("foo") - token) 107 | } 108 | Ok("") 109 | } 110 | 111 | def doBackgroundStuff(token: String) { 112 | val thread = ThreadManager.createBackgroundThread(new Runnable() { 113 | def run() { 114 | println("BACKGROUND THREAD") 115 | 116 | // TODO terminate on channel close event 117 | val send = Iteratee.foreach[String] { 118 | data => 119 | println("SENDING MESSAGE") 120 | ChannelServiceFactory.getChannelService.sendMessage(new ChannelMessage(token, data)) 121 | } 122 | 123 | clock &> Enumeratee.take[String](60) run send //&> takeWhile(present.contains("foo")) run send 124 | 125 | 126 | } 127 | }) 128 | thread.start() 129 | 130 | } 131 | 132 | val present = mutable.Map[String, Seq[String]]().withDefaultValue(Seq()) 133 | 134 | def index() = Action { 135 | //Ok(views.html.index()) 136 | val token = ChannelServiceFactory.getChannelService.createChannel("foo") 137 | 138 | present.update("foo", present("foo") ++ Seq(token)) 139 | 140 | Ok(html.index.render(token).toString) 141 | } 142 | 143 | // def takeWhile[E](f: => Boolean): iteratee.Enumeratee[E, E] = new CheckDone[E, E] { 144 | // 145 | // def step[A](f: => Boolean)(k: K[E, A]): K[E, Iteratee[E, A]] = { 146 | // case in@(Input.El(_) | Input.Empty) if f => new Enumeratee.CheckDone[E, E] { 147 | // def continue[A](k: K[E, A]) = Cont(step(f)(k)) 148 | // } &> k(in) 149 | // case Input.EOF => Done(Cont(k), Input.EOF) 150 | // } 151 | // 152 | // def continue[A](k: K[E, A]) = Cont(step(f)(k)) 153 | // 154 | // } 155 | 156 | def liveClock = Action { 157 | //Ok.chunked(clock &> Comet(callback = "parent.clockChanged")) 158 | Ok("TODO") 159 | } 160 | 161 | } 162 | 163 | -------------------------------------------------------------------------------- /samples/realtime-clock/src/main/scala/controllers/package.scala: -------------------------------------------------------------------------------- 1 | package play.api.libs { 2 | 3 | /** 4 | * The Iteratee monad provides strict, safe, and functional I/O. 5 | */ 6 | package object iteratee { 7 | 8 | type K[E, A] = Input[E] => Iteratee[E, A] 9 | 10 | } 11 | 12 | } 13 | 14 | package play.api.libs.iteratee { 15 | 16 | import com.google.appengine.api.ThreadManager 17 | 18 | private[iteratee] object internal { 19 | import scala.concurrent.ExecutionContext 20 | import java.util.concurrent.Executors 21 | 22 | implicit lazy val defaultExecutionContext: scala.concurrent.ExecutionContext = { 23 | val numberOfThreads = 10 24 | val threadFactory = ThreadManager.backgroundThreadFactory() 25 | 26 | 27 | ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(numberOfThreads, threadFactory)) 28 | } 29 | } 30 | } 31 | 32 | 33 | 34 | 35 | /* __ *\ 36 | ** ________ ___ / / ___ Scala API ** 37 | ** / __/ __// _ | / / / _ | (c) 2003-2013, LAMP/EPFL ** 38 | ** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** 39 | ** /____/\___/_/ |_/____/_/ | | ** 40 | ** |/ ** 41 | \* */ 42 | 43 | package scala.concurrent.impl{ 44 | 45 | //import scala.concurrent.impl.Promise 46 | import scala.concurrent.{ExecutionContext, CanAwait, OnCompleteRunnable, TimeoutException, ExecutionException} 47 | import scala.concurrent.duration.{Duration, Deadline, FiniteDuration} 48 | import scala.annotation.tailrec 49 | import scala.util.control.NonFatal 50 | import scala.util.{Try, Success, Failure} 51 | 52 | private[concurrent] trait Promise[T] extends scala.concurrent.Promise[T] with scala.concurrent.Future[T] { 53 | def future: this.type = this 54 | } 55 | 56 | /* Precondition: `executor` is prepared, i.e., `executor` has been returned from invocation of `prepare` on some other `ExecutionContext`. 57 | */ 58 | private class CallbackRunnable[T](val executor: ExecutionContext, val onComplete: Try[T] => Any) extends Runnable with OnCompleteRunnable { 59 | // must be filled in before running it 60 | var value: Try[T] = null 61 | 62 | override def run() = { 63 | require(value ne null) // must set value to non-null before running! 64 | try onComplete(value) catch { 65 | case NonFatal(e) => executor reportFailure e 66 | } 67 | } 68 | 69 | def executeWithValue(v: Try[T]): Unit = { 70 | require(value eq null) // can't complete it twice 71 | value = v 72 | // Note that we cannot prepare the ExecutionContext at this point, since we might 73 | // already be running on a different thread! 74 | try executor.execute(this) catch { 75 | case NonFatal(t) => executor reportFailure t 76 | } 77 | } 78 | } 79 | 80 | private[concurrent] object Promise { 81 | 82 | /** An already completed Future is given its result at creation. 83 | * 84 | * Useful in Future-composition when a value to contribute is already available. 85 | */ 86 | final class KeptPromise[T](suppliedValue: Try[T]) extends Promise[T] { 87 | 88 | val value = Some(resolveTry(suppliedValue)) 89 | 90 | override def isCompleted: Boolean = true 91 | 92 | def tryComplete(value: Try[T]): Boolean = false 93 | 94 | def onComplete[U](func: Try[T] => U)(implicit executor: ExecutionContext): Unit = { 95 | val completedAs = value.get 96 | val preparedEC = executor.prepare 97 | (new CallbackRunnable(preparedEC, func)).executeWithValue(completedAs) 98 | } 99 | 100 | def ready(atMost: Duration)(implicit permit: CanAwait): this.type = this 101 | 102 | def result(atMost: Duration)(implicit permit: CanAwait): T = value.get.get 103 | } 104 | 105 | private def resolveTry[T](source: Try[T]): Try[T] = source match { 106 | case Failure(t) => resolver(t) 107 | case _ => source 108 | } 109 | 110 | private def resolver[T](throwable: Throwable): Try[T] = throwable match { 111 | case t: scala.runtime.NonLocalReturnControl[_] => Success(t.value.asInstanceOf[T]) 112 | case t: scala.util.control.ControlThrowable => Failure(new ExecutionException("Boxed ControlThrowable", t)) 113 | case t: InterruptedException => Failure(new ExecutionException("Boxed InterruptedException", t)) 114 | case e: Error => Failure(new ExecutionException("Boxed Error", e)) 115 | case t => Failure(t) 116 | } 117 | 118 | /** 119 | * App Engine promise implementation. 120 | */ 121 | class DefaultPromise[T] extends Object with Promise[T] { 122 | self => 123 | 124 | /** 125 | * Atomically update variable to newState if it is currently 126 | * holding oldState. 127 | * @return true if successful 128 | */ 129 | def updateState(oldState: AnyRef, newState: AnyRef) = { 130 | // println(s"updateState: $oldState and $newState") 131 | obj.synchronized( 132 | if (obj == oldState) { 133 | obj = newState 134 | true 135 | } else { 136 | false 137 | } 138 | ) 139 | } 140 | 141 | @volatile var obj: AnyRef = Nil 142 | 143 | def getState() = { 144 | obj.synchronized(obj) 145 | } 146 | 147 | // Start at "No callbacks" 148 | updateState(null, Nil) 149 | 150 | 151 | protected final def tryAwait(atMost: Duration): Boolean = { 152 | @tailrec 153 | def awaitUnsafe(deadline: Deadline, nextWait: FiniteDuration): Boolean = { 154 | if (!isCompleted && nextWait > Duration.Zero) { 155 | val ms = nextWait.toMillis 156 | val ns = (nextWait.toNanos % 1000000l).toInt // as per object.wait spec 157 | 158 | synchronized { 159 | if (!isCompleted) wait(ms, ns) 160 | } 161 | 162 | awaitUnsafe(deadline, deadline.timeLeft) 163 | } else 164 | isCompleted 165 | } 166 | @tailrec 167 | def awaitUnbounded(): Boolean = { 168 | if (isCompleted) true 169 | else { 170 | synchronized { 171 | if (!isCompleted) wait() 172 | } 173 | awaitUnbounded() 174 | } 175 | } 176 | 177 | import Duration.Undefined 178 | atMost match { 179 | case u if u eq Undefined => throw new IllegalArgumentException("cannot wait for Undefined period") 180 | case Duration.Inf => awaitUnbounded 181 | case Duration.MinusInf => isCompleted 182 | case f: FiniteDuration => if (f > Duration.Zero) awaitUnsafe(f.fromNow, f) else isCompleted 183 | } 184 | } 185 | 186 | @throws(classOf[TimeoutException]) 187 | @throws(classOf[InterruptedException]) 188 | def ready(atMost: Duration)(implicit permit: CanAwait): this.type = 189 | if (isCompleted || tryAwait(atMost)) this 190 | else throw new TimeoutException("Futures timed out after [" + atMost + "]") 191 | 192 | @throws(classOf[Exception]) 193 | def result(atMost: Duration)(implicit permit: CanAwait): T = 194 | ready(atMost).value.get match { 195 | case Failure(e) => throw e 196 | case Success(r) => r 197 | } 198 | 199 | def value: Option[Try[T]] = getState match { 200 | case c: Try[_] => Some(c.asInstanceOf[Try[T]]) 201 | case _ => None 202 | } 203 | 204 | override def isCompleted: Boolean = getState match { 205 | // Cheaper than boxing result into Option due to "def value" 206 | case _: Try[_] => true 207 | case _ => false 208 | } 209 | 210 | def tryComplete(value: Try[T]): Boolean = { 211 | val resolved = resolveTry(value) 212 | (try { 213 | @tailrec 214 | def tryComplete(v: Try[T]): List[CallbackRunnable[T]] = { 215 | getState match { 216 | case raw: List[_] => 217 | val cur = raw.asInstanceOf[List[CallbackRunnable[T]]] 218 | if (updateState(cur, v)) cur else tryComplete(v) 219 | case _ => null 220 | } 221 | } 222 | tryComplete(resolved) 223 | } finally { 224 | synchronized { 225 | notifyAll() 226 | } //Notify any evil blockers 227 | }) match { 228 | case null => false 229 | case rs if rs.isEmpty => true 230 | case rs => rs.foreach(r => r.executeWithValue(resolved)); true 231 | } 232 | } 233 | 234 | def onComplete[U](func: Try[T] => U)(implicit executor: ExecutionContext): Unit = { 235 | val preparedEC = executor.prepare 236 | val runnable = new CallbackRunnable[T](preparedEC, func) 237 | 238 | @tailrec //Tries to add the callback, if already completed, it dispatches the callback to be executed 239 | def dispatchOrAddCallback(): Unit = 240 | getState match { 241 | case r: Try[_] => runnable.executeWithValue(r.asInstanceOf[Try[T]]) 242 | case listeners: List[_] => if (updateState(listeners, runnable :: listeners)) () else dispatchOrAddCallback() 243 | } 244 | dispatchOrAddCallback() 245 | } 246 | 247 | 248 | 249 | 250 | 251 | /** Link this promise to the root of another promise using `link()`. Should only be 252 | * be called by Future.flatMap. 253 | */ 254 | protected[concurrent] final def linkRootOf(target: DefaultPromise[T]): Unit = link(target.compressedRoot()) 255 | 256 | 257 | 258 | /** Get the promise at the root of the chain of linked promises. Used by `compressedRoot()`. 259 | * The `compressedRoot()` method should be called instead of this method, as it is important 260 | * to compress the link chain whenever possible. 261 | */ 262 | @tailrec 263 | private def root: DefaultPromise[T] = { 264 | getState match { 265 | case linked: DefaultPromise[_] => linked.asInstanceOf[DefaultPromise[T]].root 266 | case _ => this 267 | } 268 | } 269 | 270 | /** Get the root promise for this promise, compressing the link chain to that 271 | * promise if necessary. 272 | * 273 | * For promises that are not linked, the result of calling 274 | * `compressedRoot()` will the promise itself. However for linked promises, 275 | * this method will traverse each link until it locates the root promise at 276 | * the base of the link chain. 277 | * 278 | * As a side effect of calling this method, the link from this promise back 279 | * to the root promise will be updated ("compressed") to point directly to 280 | * the root promise. This allows intermediate promises in the link chain to 281 | * be garbage collected. Also, subsequent calls to this method should be 282 | * faster as the link chain will be shorter. 283 | */ 284 | @tailrec 285 | private def compressedRoot(): DefaultPromise[T] = { 286 | getState match { 287 | case linked: DefaultPromise[_] => 288 | val target = linked.asInstanceOf[DefaultPromise[T]].root 289 | if (linked eq target) target else if (updateState(linked, target)) target else compressedRoot() 290 | case _ => this 291 | } 292 | } 293 | 294 | /** Tries to add the callback, if already completed, it dispatches the callback to be executed. 295 | * Used by `onComplete()` to add callbacks to a promise and by `link()` to transfer callbacks 296 | * to the root promise when linking two promises togehter. 297 | */ 298 | @tailrec 299 | private def dispatchOrAddCallback(runnable: CallbackRunnable[T]): Unit = { 300 | getState match { 301 | case r: Try[_] => runnable.executeWithValue(r.asInstanceOf[Try[T]]) 302 | case _: DefaultPromise[_] => compressedRoot().dispatchOrAddCallback(runnable) 303 | case listeners: List[_] => if (updateState(listeners, runnable :: listeners)) () else dispatchOrAddCallback(runnable) 304 | } 305 | } 306 | 307 | /** Link this promise to another promise so that both promises share the same 308 | * externally-visible state. Depending on the current state of this promise, this 309 | * may involve different things. For example, any onComplete listeners will need 310 | * to be transferred. 311 | * 312 | * If this promise is already completed, then the same effect as linking - 313 | * sharing the same completed value - is achieved by simply sending this 314 | * promise's result to the target promise. 315 | */ 316 | @tailrec 317 | private def link(target: DefaultPromise[T]): Unit = if (this ne target) { 318 | getState match { 319 | case r: Try[_] => 320 | if (!target.tryComplete(r.asInstanceOf[Try[T]])) { 321 | // Currently linking is done from Future.flatMap, which should ensure only 322 | // one promise can be completed. Therefore this situation is unexpected. 323 | throw new IllegalStateException("Cannot link completed promises together") 324 | } 325 | case _: DefaultPromise[_] => 326 | compressedRoot().link(target) 327 | case listeners: List[_] => if (updateState(listeners, target)) { 328 | if (!listeners.isEmpty) listeners.asInstanceOf[List[CallbackRunnable[T]]].foreach(target.dispatchOrAddCallback(_)) 329 | } else link(target) 330 | } 331 | } 332 | } 333 | 334 | 335 | } 336 | 337 | 338 | } 339 | 340 | package scala.concurrent{ 341 | 342 | import scala.language.higherKinds 343 | 344 | import java.util.concurrent.{TimeUnit} 345 | import scala.concurrent.{Future, ExecutionContext, Promise => SPromise} 346 | import scala.util.{Failure, Success, Try} 347 | import scala.concurrent.duration.FiniteDuration 348 | 349 | 350 | /** Promise is an object which can be completed with a value or failed 351 | * with an exception. 352 | * 353 | * @define promiseCompletion 354 | * If the promise has already been fulfilled, failed or has timed out, 355 | * calling this method will throw an IllegalStateException. 356 | * 357 | * @define allowedThrowables 358 | * If the throwable used to fail this promise is an error, a control exception 359 | * or an interrupted exception, it will be wrapped as a cause within an 360 | * `ExecutionException` which will fail the promise. 361 | * 362 | * @define nonDeterministic 363 | * Note: Using this method may result in non-deterministic concurrent programs. 364 | */ 365 | trait Promise[T] { 366 | 367 | // used for internal callbacks defined in 368 | // the lexical scope of this trait; 369 | // _never_ for application callbacks. 370 | private implicit def internalExecutor: ExecutionContext = Future.InternalCallbackExecutor 371 | 372 | /** Future containing the value of this promise. 373 | */ 374 | def future: Future[T] 375 | 376 | /** Returns whether the promise has already been completed with 377 | * a value or an exception. 378 | * 379 | * $nonDeterministic 380 | * 381 | * @return `true` if the promise is already completed, `false` otherwise 382 | */ 383 | def isCompleted: Boolean 384 | 385 | /** Completes the promise with either an exception or a value. 386 | * 387 | * @param result Either the value or the exception to complete the promise with. 388 | * 389 | * $promiseCompletion 390 | */ 391 | def complete(result: Try[T]): this.type = 392 | if (tryComplete(result)) this else throw new IllegalStateException("Promise already completed.") 393 | 394 | /** Tries to complete the promise with either a value or the exception. 395 | * 396 | * $nonDeterministic 397 | * 398 | * @return If the promise has already been completed returns `false`, or `true` otherwise. 399 | */ 400 | def tryComplete(result: Try[T]): Boolean 401 | 402 | /** Completes this promise with the specified future, once that future is completed. 403 | * 404 | * @return This promise 405 | */ 406 | final def completeWith(other: Future[T]): this.type = { 407 | other onComplete { this complete _ } 408 | this 409 | } 410 | 411 | /** Attempts to complete this promise with the specified future, once that future is completed. 412 | * 413 | * @return This promise 414 | */ 415 | final def tryCompleteWith(other: Future[T]): this.type = { 416 | other onComplete { this tryComplete _ } 417 | this 418 | } 419 | 420 | /** Completes the promise with a value. 421 | * 422 | * @param v The value to complete the promise with. 423 | * 424 | * $promiseCompletion 425 | */ 426 | def success(v: T): this.type = complete(Success(v)) 427 | 428 | /** Tries to complete the promise with a value. 429 | * 430 | * $nonDeterministic 431 | * 432 | * @return If the promise has already been completed returns `false`, or `true` otherwise. 433 | */ 434 | def trySuccess(value: T): Boolean = tryComplete(Success(value)) 435 | 436 | /** Completes the promise with an exception. 437 | * 438 | * @param t The throwable to complete the promise with. 439 | * 440 | * $allowedThrowables 441 | * 442 | * $promiseCompletion 443 | */ 444 | def failure(t: Throwable): this.type = complete(Failure(t)) 445 | 446 | /** Tries to complete the promise with an exception. 447 | * 448 | * $nonDeterministic 449 | * 450 | * @return If the promise has already been completed returns `false`, or `true` otherwise. 451 | */ 452 | def tryFailure(t: Throwable): Boolean = tryComplete(Failure(t)) 453 | } 454 | 455 | /** 456 | * useful helper methods to create and compose Promises 457 | */ 458 | object Promise { 459 | 460 | /** Creates a promise object which can be completed with a value. 461 | * 462 | * @tparam T the type of the value in the promise 463 | * @return the newly created `Promise` object 464 | */ 465 | def apply[T](): Promise[T] = new impl.Promise.DefaultPromise[T]() 466 | 467 | /** Creates an already completed Promise with the specified exception. 468 | * 469 | * @tparam T the type of the value in the promise 470 | * @return the newly created `Promise` object 471 | */ 472 | def failed[T](exception: Throwable): Promise[T] = fromTry(Failure(exception)) 473 | 474 | /** Creates an already completed Promise with the specified result. 475 | * 476 | * @tparam T the type of the value in the promise 477 | * @return the newly created `Promise` object 478 | */ 479 | def successful[T](result: T): Promise[T] = fromTry(Success(result)) 480 | 481 | /** Creates an already completed Promise with the specified result or exception. 482 | * 483 | * @tparam T the type of the value in the promise 484 | * @return the newly created `Promise` object 485 | */ 486 | def fromTry[T](result: Try[T]): Promise[T] = new impl.Promise.KeptPromise[T](result) 487 | 488 | /** 489 | * Constructs a Future which will contain value "message" after the given duration elapses. 490 | * This is useful only when used in conjunction with other Promises 491 | * @param message message to be displayed 492 | * @param duration duration for the scheduled promise 493 | * @return a scheduled promise 494 | */ 495 | def timeout[A](message: => A, duration: scala.concurrent.duration.Duration)(implicit ec: ExecutionContext): Future[A] = { 496 | timeout(message, duration.toMillis) 497 | } 498 | 499 | /** 500 | * Constructs a Future which will contain value "message" after the given duration elapses. 501 | * This is useful only when used in conjunction with other Promises 502 | * @param message message to be displayed 503 | * @param duration duration for the scheduled promise 504 | * @return a scheduled promise 505 | */ 506 | def timeout[A](message: => A, duration: Long, unit: TimeUnit = TimeUnit.MILLISECONDS)(implicit ec: ExecutionContext): Future[A] = { 507 | val p = SPromise[A]() 508 | //import play.api.Play.current 509 | //Akka.system.scheduler.scheduleOnce(FiniteDuration(duration, unit)) { 510 | Thread.sleep(FiniteDuration(duration, unit).toMillis) 511 | p.complete(Try(message)) 512 | //} 513 | p.future 514 | } 515 | 516 | } 517 | } -------------------------------------------------------------------------------- /samples/realtime-clock/src/main/twirl/index.scala.html: -------------------------------------------------------------------------------- 1 | @(token:String) 2 | @main { 3 | 4 |

Comet clock

5 | 6 |

7 | 8 |

9 | Clock events are pushed from the Server using a Comet connection. 10 |

11 | 12 | 33 | 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /samples/realtime-clock/src/main/twirl/main.scala.html: -------------------------------------------------------------------------------- 1 | @(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | Comet clock 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | @content 16 | 17 | -------------------------------------------------------------------------------- /samples/realtime-clock/src/main/webapp/WEB-INF/appengine-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ns-labs 4 | default 5 | playframework-appengine-clock 6 | true 7 | B1 8 | 9 | 1 10 | 11 | 12 | 13 | channel_presence 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/realtime-clock/src/main/webapp/WEB-INF/application.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 10 | 11 | GAE Java SuperFun app 12 | SuperFun 13 | 14 | 15 | 16 | 17 | 18 | default 19 | default 20 | 21 | 22 | -------------------------------------------------------------------------------- /samples/realtime-clock/src/main/webapp/WEB-INF/dispatch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | * 6 | default 7 | 8 | 9 | 10 | */favicon.ico 11 | default 12 | 13 | -------------------------------------------------------------------------------- /samples/realtime-clock/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | PlayAppEngineServlet 7 | play.PlayAppEngineServlet 8 | 9 | 10 | PlayAppEngineServlet 11 | /* 12 | 13 | 14 | PlayAppEngineServlet 15 | / 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /samples/realtime-clock/src/main/webapp/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, Arial; 3 | font-weight: lighter; 4 | text-align: center; 5 | } 6 | 7 | h1 { 8 | color: #333; 9 | } 10 | 11 | p { 12 | color: #666; 13 | } 14 | 15 | #clock { 16 | display: block; 17 | margin: 50px auto; 18 | width: 800px; 19 | text-align: center; 20 | } 21 | 22 | #clock span { 23 | position: relative; 24 | font-size: 64px; 25 | display: inline-block; 26 | background: #222; 27 | margin-right: 3px; 28 | padding: 0 10px; 29 | color: #fff; 30 | text-shadow: 1px 1px 1px #000; 31 | border-radius: 6px; 32 | box-shadow: 1px 1px 2px rgba(0,0,0,.3); 33 | background-image: linear-gradient(bottom, rgb(23,21,21) 33%, rgb(46,45,44) 70%); 34 | background-image: -o-linear-gradient(bottom, rgb(23,21,21) 33%, rgb(46,45,44) 70%); 35 | background-image: -moz-linear-gradient(bottom, rgb(23,21,21) 33%, rgb(46,45,44) 70%); 36 | background-image: -webkit-linear-gradient(bottom, rgb(23,21,21) 33%, rgb(46,45,44) 70%); 37 | background-image: -ms-linear-gradient(bottom, rgb(23,21,21) 33%, rgb(46,45,44) 70%); 38 | background-image: -webkit-gradient( 39 | linear, 40 | left bottom, 41 | left top, 42 | color-stop(0.33, rgb(23,21,21)), 43 | color-stop(0.7, rgb(46,45,44)) 44 | ); 45 | } 46 | 47 | #clock span:before { 48 | content: '–'; 49 | position: absolute; 50 | left: 0; 51 | right: 0; 52 | top: 48%; 53 | bottom: 51%; 54 | text-indent: -999em; 55 | color: #000; 56 | background: #000; 57 | } 58 | 59 | #comet { 60 | display: none; 61 | } -------------------------------------------------------------------------------- /sbt-routes-plugin/build.sbt: -------------------------------------------------------------------------------- 1 | organization := "com.siderakis" 2 | 3 | version := "0.2-SNAPSHOT" 4 | 5 | name := "playframework-appengine-routes" 6 | 7 | scalaVersion := "2.10.2" 8 | 9 | libraryDependencies ++= Seq( 10 | "com.github.scala-incubator.io" %% "scala-io-core" % "0.4.2", 11 | "com.github.scala-incubator.io" %% "scala-io-file" % "0.4.2", 12 | "org.specs2" %% "specs2" % "2.3.11" % "test" 13 | ) 14 | 15 | sbtPlugin := true 16 | 17 | publishMavenStyle := true 18 | 19 | scalacOptions in Test ++= Seq("-Yrangepos") 20 | 21 | publishTo := Some(Resolver.file("Local", Path.userHome / "siderakis.github.com" / "maven" asFile)(Patterns(true, Resolver.mavenStyleBasePattern))) -------------------------------------------------------------------------------- /sbt-routes-plugin/src/main/scala/PlayPlugin.scala: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import play.router.RoutesCompiler.compile 4 | import play.router.RoutesCompiler.GeneratedSource 5 | import play.router.RoutesCompiler.RoutesCompilationError 6 | import sbt._ 7 | import sbt.Keys._ 8 | 9 | 10 | object PlayProject extends Plugin with PlayKeys with PlayCommands with PlaySettings 11 | 12 | trait PlayCommands { 13 | 14 | val RouteFiles = (state: State, confDirectory: File, generatedDir: File, additionalImports: Seq[String]) => { 15 | val scalaRoutes = generatedDir ** "routes_*.scala" 16 | scalaRoutes.get.map(GeneratedSource).foreach(_.sync()) 17 | try { { 18 | (confDirectory * "*.routes").get ++ (confDirectory * "routes").get 19 | }.map { 20 | routesFile => 21 | compile(routesFile, generatedDir, additionalImports) 22 | } 23 | } catch { 24 | case RoutesCompilationError(source, message, line, column) => { 25 | throw new RuntimeException("Error with Routes file: " + message) // reportCompilationError(state, RoutesCompilationException(source, message, line, column.map(_ - 1))) 26 | } 27 | case e => throw e 28 | } 29 | 30 | scalaRoutes.get.map(_.getAbsoluteFile) 31 | 32 | } 33 | } 34 | 35 | 36 | trait PlayKeys { 37 | val confDirectory = SettingKey[File]("play-conf") 38 | 39 | val routesImport = SettingKey[Seq[String]]("play-routes-imports") 40 | 41 | } 42 | 43 | trait PlaySettings { 44 | this: PlayCommands with PlayKeys => 45 | 46 | lazy val defaultPlaySettings = Seq[Setting[_]]( 47 | 48 | routesImport := Seq.empty[String], 49 | confDirectory <<= sourceDirectory / "main/conf", 50 | 51 | sourceGenerators in Compile <+= (state, confDirectory, sourceManaged in Compile, routesImport) map RouteFiles 52 | ) 53 | 54 | } 55 | -------------------------------------------------------------------------------- /sbt-routes-plugin/src/test/resources/duplicateHandlers.routes: -------------------------------------------------------------------------------- 1 | GET /foo controllers.FooController.foo(bar: Boolean = false) 2 | GET /foo2 controllers.FooController.foo(bar: Boolean = false, baz: Boolean = true) 3 | GET /bar controllers.BarController.bar(baz) -------------------------------------------------------------------------------- /sbt-routes-plugin/src/test/resources/generating.routes: -------------------------------------------------------------------------------- 1 | GET /foo controllers.FooController.foo(bar: Boolean = false) -------------------------------------------------------------------------------- /sbt-routes-plugin/src/test/scala/play/routes/RoutesCompilerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009-2013 Typesafe Inc. 3 | */ 4 | package play.router 5 | 6 | import org.specs2.mutable.Specification 7 | import play.router.RoutesCompiler._ 8 | import java.io.File 9 | import org.specs2.execute.Result 10 | 11 | object RoutesCompilerSpec extends Specification { 12 | 13 | sequential 14 | 15 | "route file parser" should { 16 | 17 | def parseRoute(line: String) = { 18 | val rule = parseRule(line) 19 | rule must beAnInstanceOf[Route] 20 | rule.asInstanceOf[Route] 21 | } 22 | 23 | def parseRule(line: String) = { 24 | val parser = new RouteFileParser 25 | val result = parser.parse(line) 26 | def describeResult[T](result: parser.ParseResult[T]) = result match { 27 | case parser.NoSuccess(msg, _) => msg 28 | case _ => "successful" 29 | } 30 | result.successful aka describeResult(result) must_== true 31 | result.get.size must_== 1 32 | result.get.head 33 | } 34 | 35 | def parseError(line: String): Result = { 36 | val parser = new RouteFileParser 37 | val result = parser.parse(line) 38 | result must beAnInstanceOf[parser.NoSuccess] 39 | } 40 | 41 | "parse the HTTP method" in { 42 | parseRoute("GET /s p.c.m").verb must_== HttpVerb("GET") 43 | } 44 | 45 | "parse a static path" in { 46 | parseRoute("GET /s p.c.m").path must_== PathPattern(Seq(StaticPart("s"))) 47 | } 48 | 49 | "parse a path with dynamic parts and it should be encodeable" in { 50 | parseRoute("GET /s/:d/s p.c.m").path must_== PathPattern(Seq(StaticPart("s/"), DynamicPart("d", "[^/]+", true), StaticPart("/s"))) 51 | } 52 | 53 | "parse a path with multiple dynamic parts and it should not be encodeable" in { 54 | parseRoute("GET /s/*e p.c.m").path must_== PathPattern(Seq(StaticPart("s/"), DynamicPart("e", ".+", false))) 55 | } 56 | 57 | "path with regex should not be encodeable" in { 58 | parseRoute("GET /s/$id<[0-9]+> p.c.m").path must_== PathPattern(Seq(StaticPart("s/"), DynamicPart("id", "[0-9]+", false))) 59 | 60 | } 61 | 62 | "parse a single element package" in { 63 | parseRoute("GET /s p.c.m").call.packageName must_== "p" 64 | } 65 | 66 | "parse a multiple element package" in { 67 | parseRoute("GET /s p1.p2.c.m").call.packageName must_== "p1.p2" 68 | } 69 | 70 | "parse a controller" in { 71 | parseRoute("GET /s p.c.m").call.controller must_== "c" 72 | } 73 | 74 | "parse a method" in { 75 | parseRoute("GET /s p.c.m").call.method must_== "m" 76 | } 77 | 78 | "parse a parameterless method" in { 79 | parseRoute("GET /s p.c.m").call.parameters must beNone 80 | } 81 | 82 | "parse a zero argument method" in { 83 | parseRoute("GET /s p.c.m()").call.parameters must_== Some(Seq()) 84 | } 85 | 86 | "parse method with arguments" in { 87 | parseRoute("GET /s p.c.m(s1, s2)").call.parameters must_== Some(Seq(Parameter("s1", "String", None, None), Parameter("s2", "String", None, None))) 88 | } 89 | 90 | "parse argument type" in { 91 | parseRoute("GET /s p.c.m(i: Int)").call.parameters.get.head.typeName must_== "Int" 92 | } 93 | 94 | "parse argument default value" in { 95 | parseRoute("GET /s p.c.m(i: Int ?= 3)").call.parameters.get.head.default must beSome("3") 96 | } 97 | 98 | "parse argument fixed value" in { 99 | parseRoute("GET /s p.c.m(i: Int = 3)").call.parameters.get.head.fixed must beSome("3") 100 | } 101 | 102 | "parse a non instantiating route" in { 103 | parseRoute("GET /s p.c.m").call.instantiate must_== false 104 | } 105 | 106 | "parse an instantiating route" in { 107 | parseRoute("GET /s @p.c.m").call.instantiate must_== true 108 | } 109 | 110 | "parse an include" in { 111 | val rule = parseRule("-> /s someFile") 112 | rule must beAnInstanceOf[Include] 113 | rule.asInstanceOf[Include].router must_== "someFile" 114 | rule.asInstanceOf[Include].prefix must_== "s" 115 | } 116 | 117 | "parse a comment with a route" in { 118 | parseRoute("# some comment\nGET /s p.c.m").comments must containTheSameElementsAs(Seq(Comment(" some comment"))) 119 | } 120 | 121 | "throw an error for an unexpected line" in parseError("foo") 122 | "throw an error for an invalid path" in parseError("GET s p.c.m") 123 | "throw an error for no path" in parseError("GET") 124 | "throw an error for no method" in parseError("GET /s") 125 | "throw an error if no method specified" in parseError("GET /s p.c") 126 | "throw an error for an invalid include path" in parseError("-> s someFile") 127 | "throw an error if no include file specified" in parseError("-> /s") 128 | } 129 | 130 | "route file compiler" should { 131 | 132 | def withTempDir[T](block: File => T) = { 133 | val tmp = File.createTempFile("RoutesCompilerSpec", "") 134 | tmp.delete() 135 | tmp.mkdir() 136 | try { 137 | block(tmp) 138 | } finally { 139 | def rm(file: File): Unit = file match { 140 | case dir if dir.isDirectory => 141 | dir.listFiles().foreach(rm) 142 | dir.delete() 143 | case f => f.delete() 144 | } 145 | rm(tmp) 146 | } 147 | } 148 | 149 | // "not generate reverse ref routing if its disabled" in withTempDir { tmp => 150 | // val f = new File(this.getClass.getClassLoader.getResource("generating.routes").toURI) 151 | // RoutesCompiler.compile(f, tmp, Seq.empty, generateReverseRouter = true, generateRefReverseRouter = false) 152 | // 153 | // val generatedJavaRoutes = new File(tmp, "controllers/routes.java") 154 | // val contents = scala.io.Source.fromFile(generatedJavaRoutes).getLines().mkString("") 155 | // contents.contains("public static class ref") must beFalse 156 | // } 157 | 158 | "generate routes classes for route definitions that pass the checks" in withTempDir { tmp => 159 | val file = new File(this.getClass.getClassLoader.getResource("generating.routes").toURI) 160 | RoutesCompiler.compile(file, tmp, Seq.empty) 161 | 162 | val generatedRoutes = new File(tmp, "generating/routes_routing.scala") 163 | generatedRoutes.exists() must beTrue 164 | 165 | val generatedReverseRoutes = new File(tmp, "generating/routes_reverseRouting.scala") 166 | generatedReverseRoutes.exists() must beTrue 167 | } 168 | 169 | "check if there are no routes using overloaded handler methods" in withTempDir { tmp => 170 | val file = new File(this.getClass.getClassLoader.getResource("duplicateHandlers.routes").toURI) 171 | RoutesCompiler.compile(file, tmp, Seq.empty) must throwA [RoutesCompilationError] 172 | } 173 | } 174 | } --------------------------------------------------------------------------------