|
Play Framework/Scala example source code file (RoutesCompiler.scala)
The RoutesCompiler.scala Play Framework example source code/* * Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com> */ package play.router import scala.io.Codec import scala.util.parsing.input._ import scala.util.parsing.combinator._ import scala.collection.immutable.ListMap import java.io.File import org.apache.commons.io.FileUtils /** * provides a compiler for routes */ object RoutesCompiler { val scalaReservedWords = List( "abstract", "case", "catch", "class", "def", "do", "else", "extends", "false", "final", "finally", "for", "forSome", "if", "implicit", "import", "lazy", "macro", "match", "new", "null", "object", "override", "package", "private", "protected", "return", "sealed", "super", "then", "this", "throw", "trait", "try", "true", "type", "val", "var", "while", "with", "yield", // Not scala keywords, but are used in the router "queryString" ) case class HttpVerb(value: String) { override def toString = value } case class HandlerCall(packageName: String, controller: String, instantiate: Boolean, method: String, parameters: Option[Seq[Parameter]]) extends Positional { val dynamic = if (instantiate) "@" else "" override def toString = dynamic + packageName + "." + controller + dynamic + "." + method + parameters.map { params => "(" + params.mkString(", ") + ")" }.getOrElse("") } case class Parameter(name: String, typeName: String, fixed: Option[String], default: Option[String]) extends Positional { override def toString = name + ":" + typeName + fixed.map(" = " + _).getOrElse("") + default.map(" ?= " + _).getOrElse("") } sealed trait Rule extends Positional case class Route(verb: HttpVerb, path: PathPattern, call: HandlerCall, comments: List[Comment] = List()) extends Rule case class Include(prefix: String, router: String) extends Rule case class Comment(comment: String) object Hash { def apply(routesFile: File, imports: Seq[String]): String = { import java.security.MessageDigest val digest = MessageDigest.getInstance("SHA-1") digest.reset() digest.update(FileUtils.readFileToByteArray(routesFile) ++ imports.flatMap(_.getBytes)) digest.digest().map(0xFF & _).map { "%02x".format(_) }.foldLeft("") { _ + _ } } } // --- Parser private[router] class RouteFileParser extends JavaTokenParsers { override def skipWhitespace = false override val whiteSpace = """[ \t]+""".r override def phrase[T](p: Parser[T]) = new Parser[T] { lastNoSuccess = null def apply(in: Input) = p(in) match { case s @ Success(out, in1) => if (in1.atEnd) s else if (lastNoSuccess == null || lastNoSuccess.next.pos < in1.pos) Failure("end of input expected", in1) else lastNoSuccess case _ => lastNoSuccess } } def EOF: util.matching.Regex = "\\z".r def namedError[A](p: Parser[A], msg: String): Parser[A] = Parser[A] { i => p(i) match { case Failure(_, in) => Failure(msg, in) case o => o } } def several[T](p: => Parser[T]): Parser[List[T]] = Parser { in => import scala.collection.mutable.ListBuffer val elems = new ListBuffer[T] def continue(in: Input): ParseResult[List[T]] = { val p0 = p // avoid repeatedly re-evaluating by-name parser @scala.annotation.tailrec def applyp(in0: Input): ParseResult[List[T]] = p0(in0) match { case Success(x, rest) => elems += x; applyp(rest) case Failure(_, _) => Success(elems.toList, in0) case err: Error => err } applyp(in) } continue(in) } def separator: Parser[String] = namedError(whiteSpace, "Whitespace expected") def ignoreWhiteSpace: Parser[Option[String]] = opt(whiteSpace) // This won't be needed when we upgrade to Scala 2.11, we will then be able to use JavaTokenParser.ident: // https://github.com/scala/scala/pull/1466 def javaIdent: Parser[String] = """\p{javaJavaIdentifierStart}\p{javaJavaIdentifierPart}*""".r def identifier: Parser[String] = namedError(javaIdent, "Identifier expected") def end: util.matching.Regex = """\s*""".r def comment: Parser[Comment] = "#" ~> ".*".r ^^ { case c => Comment(c) } def newLine: Parser[String] = namedError((("\r"?) ~> "\n"), "End of line expected") def blankLine: Parser[Unit] = ignoreWhiteSpace ~> newLine ^^ { case _ => () } def parentheses: Parser[String] = { "(" ~ (several((parentheses | not(")") ~> """.""".r))) ~ commit(")") ^^ { case p1 ~ charList ~ p2 => p1 + charList.mkString + p2 } } def brackets: Parser[String] = { "[" ~ (several((parentheses | not("]") ~> """.""".r))) ~ commit("]") ^^ { case p1 ~ charList ~ p2 => p1 + charList.mkString + p2 } } def string: Parser[String] = { "\"" ~ (several((parentheses | not("\"") ~> """.""".r))) ~ commit("\"") ^^ { case p1 ~ charList ~ p2 => p1 + charList.mkString + p2 } } def multiString: Parser[String] = { "\"\"\"" ~ (several((parentheses | not("\"\"\"") ~> """.""".r))) ~ commit("\"\"\"") ^^ { case p1 ~ charList ~ p2 => p1 + charList.mkString + p2 } } def httpVerb: Parser[HttpVerb] = namedError("GET" | "POST" | "PUT" | "PATCH" | "HEAD" | "DELETE" | "OPTIONS", "HTTP Verb expected") ^^ { case v => HttpVerb(v) } def singleComponentPathPart: Parser[DynamicPart] = (":" ~> identifier) ^^ { case name => DynamicPart(name, """[^/]+""", encode = true) } def multipleComponentsPathPart: Parser[DynamicPart] = ("*" ~> identifier) ^^ { case name => DynamicPart(name, """.+""", encode = false) } def regexComponentPathPart: Parser[DynamicPart] = "$" ~> identifier ~ ("<" ~> (not(">") ~> """[^\s]""".r +) <~ ">" ^^ { case c => c.mkString }) ^^ { case name ~ regex => DynamicPart(name, regex, encode = false) } def staticPathPart: Parser[StaticPart] = (not(":") ~> not("*") ~> not("$") ~> """[^\s]""".r +) ^^ { case chars => StaticPart(chars.mkString) } def path: Parser[PathPattern] = "/" ~ ((positioned(singleComponentPathPart) | positioned(multipleComponentsPathPart) | positioned(regexComponentPathPart) | staticPathPart) *) ^^ { case _ ~ parts => PathPattern(parts) } def space(s: String): Parser[String] = (ignoreWhiteSpace ~> s <~ ignoreWhiteSpace) def parameterType: Parser[String] = ":" ~> ignoreWhiteSpace ~> simpleType def simpleType: Parser[String] = { ((stableId <~ ignoreWhiteSpace) ~ opt(typeArgs)) ^^ { case sid ~ ta => sid.toString + ta.getOrElse("") } | (space("(") ~ types ~ space(")")) ^^ { case _ ~ b ~ _ => "(" + b + ")" } } def typeArgs: Parser[String] = { (space("[") ~ types ~ space("]") ~ opt(typeArgs)) ^^ { case _ ~ ts ~ _ ~ ta => "[" + ts + "]" + ta.getOrElse("") } | (space("#") ~ identifier ~ opt(typeArgs)) ^^ { case _ ~ id ~ ta => "#" + id + ta.getOrElse("") } } def types: Parser[String] = rep1sep(simpleType, space(",")) ^^ (_ mkString ",") def stableId: Parser[String] = rep1sep(identifier, space(".")) ^^ (_ mkString ".") def expression: Parser[String] = (multiString | string | parentheses | brackets | """[^),?=\n]""".r +) ^^ { case p => p.mkString } def parameterFixedValue: Parser[String] = "=" ~ ignoreWhiteSpace ~ expression ^^ { case a ~ _ ~ b => a + b } def parameterDefaultValue: Parser[String] = "?=" ~ ignoreWhiteSpace ~ expression ^^ { case a ~ _ ~ b => a + b } def parameter: Parser[Parameter] = (identifier <~ ignoreWhiteSpace) ~ opt(parameterType) ~ (ignoreWhiteSpace ~> opt(parameterDefaultValue | parameterFixedValue)) ^^ { case name ~ t ~ d => Parameter(name, t.getOrElse("String"), d.filter(_.startsWith("=")).map(_.drop(1)), d.filter(_.startsWith("?")).map(_.drop(2))) } def parameters: Parser[List[Parameter]] = "(" ~> repsep(ignoreWhiteSpace ~> positioned(parameter) <~ ignoreWhiteSpace, ",") <~ ")" // Absolute method consists of a series of Java identifiers representing the package name, controller and method. // Since the Scala parser is greedy, we can't easily extract this out, so just parse at least 3 def absoluteMethod: Parser[List[String]] = namedError(javaIdent ~ "." ~ javaIdent ~ "." ~ rep1sep(javaIdent, ".") ^^ { case first ~ _ ~ second ~ _ ~ rest => first :: second :: rest }, "Controller method call expected") def call: Parser[HandlerCall] = opt("@") ~ absoluteMethod ~ opt(parameters) ^^ { case instantiate ~ absMethod ~ parameters => { val (packageParts, classAndMethod) = absMethod.splitAt(absMethod.size - 2) val packageName = packageParts.mkString(".") val className = classAndMethod(0) val methodName = classAndMethod(1) val dynamic = !instantiate.isEmpty HandlerCall(packageName, className, dynamic, methodName, parameters) } } def router: Parser[String] = rep1sep(identifier, ".") ^^ { case parts => parts.mkString(".") } def route = httpVerb ~! separator ~ path ~ separator ~ positioned(call) ~ ignoreWhiteSpace ^^ { case v ~ _ ~ p ~ _ ~ c ~ _ => Route(v, p, c) } def include = "->" ~! separator ~ path ~ separator ~ router ~ ignoreWhiteSpace ^^ { case _ ~ _ ~ p ~ _ ~ r ~ _ => Include(p.toString, r) } def sentence: Parser[Product with Serializable] = namedError((comment | positioned(include) | positioned(route)), "HTTP Verb (GET, POST, ...), include (->) or comment (#) expected") <~ (newLine | EOF) def parser: Parser[List[Rule]] = phrase((blankLine | sentence *) <~ end) ^^ { case routes => routes.reverse.foldLeft(List[(Option[Rule], List[Comment])]()) { case (s, r @ Route(_, _, _, _)) => (Some(r), List()) :: s case (s, i @ Include(_, _)) => (Some(i), List()) :: s case (s, c @ ()) => (None, List()) :: s case ((r, comments) :: others, c @ Comment(_)) => (r, c :: comments) :: others case (s, _) => s }.collect { case (Some(r @ Route(_, _, _, _)), comments) => r.copy(comments = comments).setPos(r.pos) case (Some(i @ Include(_, _)), _) => i } } def parse(text: String): ParseResult[List[Rule]] = { parser(new CharSequenceReader(text)) } } import java.io.File case class RoutesCompilationError(source: File, message: String, line: Option[Int], column: Option[Int]) case class GeneratedSource(file: File) { val lines = if (file.exists) FileUtils.readFileToString(file, implicitly[Codec].name).split('\n').toList else Nil val source = lines.find(_.startsWith("// @SOURCE:")).map(m => new File(m.trim.drop(11))) def isGenerated: Boolean = source.isDefined def sync(): Boolean = { if (!source.exists(_.exists)) file.delete() else false } def needsRecompilation(imports: Seq[String]): Boolean = { val hash = lines.find(_.startsWith("// @HASH:")).map(m => m.trim.drop(9)).getOrElse("") source.filter(_.exists).map { p => Hash(p, imports) != hash }.getOrElse(true) } def mapLine(generatedLine: Int): Option[Int] = { lines.take(generatedLine).reverse.collect { case l if l.startsWith("// @LINE:") => Integer.parseInt(l.trim.drop(9)) }.headOption } } object MaybeGeneratedSource { def unapply(source: File): Option[GeneratedSource] = { val generated = GeneratedSource(source) if (generated.isGenerated) { Some(generated) } else { None } } } /** * Compile the given routes file * * @param file The routes file to compile * @param generatedDir The directory to place the generated source code in * @param additionalImports Additional imports to add to the output files * @param generateReverseRouter Whether the reverse router should be generated * @param generateRefReverseRouter Whether the ref router should be generated * @param namespaceReverseRouter Whether the reverse router should be namespaced * @return Either the list of files that were generated (right) or the routes compilation errors (left) */ def compile(file: File, generatedDir: File, additionalImports: Seq[String], generateReverseRouter: Boolean = true, generateRefReverseRouter: Boolean = true, namespaceReverseRouter: Boolean = false): Either[Seq[RoutesCompilationError], Seq[File]] = { val namespace = Option(file.getName).filter(_.endsWith(".routes")).map(_.dropRight(".routes".size)) val packageDir = namespace.map(pkg => new File(generatedDir, pkg.replace('.', '/'))).getOrElse(generatedDir) val parser = new RouteFileParser val routeFile = file.getAbsoluteFile val routesContent = FileUtils.readFileToString(routeFile) parser.parse(routesContent) match { case parser.Success(parsed, _) => check(file, parsed.collect { case r: Route => r }) match { case Nil => val generated = generate(routeFile, namespace, parsed, additionalImports, generateReverseRouter, generateRefReverseRouter, namespaceReverseRouter) Right(generated.map { case (filename, content) => val file = new File(generatedDir, filename) FileUtils.writeStringToFile(file, content, implicitly[Codec].name) file }) case errors => Left(errors) } case parser.NoSuccess(message, in) => Left(Seq(RoutesCompilationError(file, message, Some(in.pos.line), Some(in.pos.column)))) } } /** * Precheck routes coherence or throw exceptions early */ private def check(file: java.io.File, routes: List[Route]): Seq[RoutesCompilationError] = { import scala.collection.mutable._ val errors = ListBuffer.empty[RoutesCompilationError] routes.foreach { route => if (route.call.packageName.isEmpty) { errors += RoutesCompilationError( file, "Missing package name", Some(route.call.pos.line), Some(route.call.pos.column)) } if (route.call.controller.isEmpty) { errors += RoutesCompilationError( file, "Missing Controller", Some(route.call.pos.line), Some(route.call.pos.column)) } route.path.parts.collect { case part @ DynamicPart(name, regex, _) => { route.call.parameters.getOrElse(Nil).find(_.name == name).map { p => if (p.fixed.isDefined || p.default.isDefined) { errors += RoutesCompilationError( file, "It is not allowed to specify a fixed or default value for parameter: '" + name + "' extracted from the path", Some(p.pos.line), Some(p.pos.column)) } try { java.util.regex.Pattern.compile(regex) } catch { case e: Exception => { errors += RoutesCompilationError( file, e.getMessage, Some(part.pos.line), Some(part.pos.column)) } } }.getOrElse { errors += RoutesCompilationError( file, "Missing parameter in call definition: " + name, Some(part.pos.line), Some(part.pos.column)) } } } } // make sure there are no routes using overloaded handler methods, or handler methods with default parameters without declaring them all val sameHandlerMethodGroup = routes.groupBy { r => r.call.packageName + r.call.controller + r.call.method } val sameHandlerMethodParameterCountGroup = sameHandlerMethodGroup.groupBy { g => (g._1, g._2.groupBy(route => route.call.parameters.map(p => p.length).getOrElse(0))) } sameHandlerMethodParameterCountGroup.find(g => g._1._2.size > 1).foreach { overloadedRouteGroup => val firstOverloadedRoute = overloadedRouteGroup._2.values.head.head errors += RoutesCompilationError( file, "Using different overloaded methods is not allowed. If you are using a single method in combination with default parameters, make sure you declare them all explicitly.", Some(firstOverloadedRoute.call.pos.line), Some(firstOverloadedRoute.call.pos.column) ) } errors.toList } private def markLines(routes: Rule*): String = { routes.map("// @LINE:" + _.pos.line).reverse.mkString("\n") } /** * Generate the actual Scala code for this router */ private def generate(file: File, namespace: Option[String], rules: List[Rule], additionalImports: Seq[String], reverseRouter: Boolean, reverseRefRouter: Boolean, namespaceReverseRouter: Boolean): Seq[(String, String)] = { val filePrefix = namespace.map(_.replace('.', '/') + "/").getOrElse("") + "/routes" val (path, hash, date) = (file.getCanonicalPath.replace(File.separator, "/"), Hash(file, additionalImports), new java.util.Date().toString) val routes = rules.collect { case r: Route => r } val files = Seq(filePrefix + "_routing.scala" -> generateRouter(path, hash, date, namespace, additionalImports, rules)) if (reverseRouter) { (files :+ filePrefix + "_reverseRouting.scala" -> generateReverseRouter(path, hash, date, namespace, additionalImports, routes, reverseRefRouter, namespaceReverseRouter)) ++ generateJavaWrappers(path, hash, date, rules, reverseRefRouter, namespace.filter(_ => namespaceReverseRouter)) } else { files } } private def prefixImport(i: String): String = { if (!i.startsWith("_root_.")) { "_root_." + i } else { i } } def generateRouter(path: String, hash: String, date: String, namespace: Option[String], additionalImports: Seq[String], rules: List[Rule]) = """ |// @SOURCE:%s |// @HASH:%s |// @DATE:%s |%s | |import play.core._ |import play.core.Router._ |import play.core.Router.HandlerInvokerFactory._ |import play.core.j._ | |import play.api.mvc._ |%s | |import Router.queryString | |object Routes extends Router.Routes { | |import ReverseRouteContext.empty | |private var _prefix = "/" | |def setPrefix(prefix: String) { | _prefix = prefix | List[(String,Routes)](%s).foreach { | case (p, router) => router.setPrefix(prefix + (if(prefix.endsWith("/")) "" else "/") + p) | } |} | |def prefix = _prefix | |lazy val defaultPrefix = { if(Routes.prefix.endsWith("/")) "" else "/" } | |%s | |def routes:PartialFunction[RequestHeader,Handler] = { |%s |} | |} """.stripMargin.format( path, hash, date, namespace.map("package " + _).getOrElse(""), additionalImports.map(prefixImport).map("import " + _).mkString("\n"), rules.collect { case Include(p, r) => "(\"" + p + "\"," + r + ")" }.mkString(","), routeDefinitions(namespace.getOrElse(""), rules), routing(namespace.getOrElse(""), rules) ) def generateReverseRouter(path: String, hash: String, date: String, namespace: Option[String], additionalImports: Seq[String], routes: List[Route], reverseRefRouter: Boolean, namespaceReverseRouter: Boolean) = """ |// @SOURCE:%s |// @HASH:%s |// @DATE:%s | |import %sRoutes.{prefix => _prefix, defaultPrefix => _defaultPrefix} |import play.core._ |import play.core.Router._ |import play.core.Router.HandlerInvokerFactory._ |import play.core.j._ | |import play.api.mvc._ |%s | |import Router.queryString | |%s | |%s | |%s """.stripMargin.format( path, hash, date, namespace.map(_ + ".").getOrElse(""), additionalImports.map(prefixImport).map("import " + _).mkString("\n"), reverseRouting(routes, namespace.filter(_ => namespaceReverseRouter)), javaScriptReverseRouting(routes, namespace.filter(_ => namespaceReverseRouter)), if (reverseRefRouter) refReverseRouting(routes, namespace.filter(_ => namespaceReverseRouter)) else "" ) def generateJavaWrappers(path: String, hash: String, date: String, rules: List[Rule], reverseRefRouter: Boolean, namespace: Option[String]) = { rules.collect { case r: Route => r }.groupBy(_.call.packageName).map { case (pn, routes) => { val packageName = namespace.map(_ + "." + pn).getOrElse(pn) def reverseRoutes = routes.groupBy(_.call.controller).map { case (controller, routes) => { "public static final " + packageName + ".Reverse" + controller + " " + controller + " = new " + packageName + ".Reverse" + controller + "();" } }.mkString("\n") def javaScriptRoutes = """ |public static class javascript { |%s |} """.stripMargin.format(routes.groupBy(_.call.controller).map { case (controller, _) => { "public static final " + packageName + ".javascript.Reverse" + controller + " " + controller + " = new " + packageName + ".javascript.Reverse" + controller + "();" } }.mkString("\n")) def refRoutes = """ |public static class ref { |%s |} """.stripMargin.format(routes.groupBy(_.call.controller).map { case (controller, _) => { "public static final " + packageName + ".ref.Reverse" + controller + " " + controller + " = new " + packageName + ".ref.Reverse" + controller + "();" } }.mkString("\n")) (packageName.replace(".", "/") + "/routes.java") -> { """ |// @SOURCE:%s |// @HASH:%s |// @DATE:%s | |package %s; | |public class routes { |%s |%s |%s |} """.stripMargin.format( path, hash, date, packageName, reverseRoutes, javaScriptRoutes, if (reverseRefRouter) refRoutes else "" ) } } } } /** * Generate the reverse routing operations */ def javaScriptReverseRouting(routes: List[Route], namespace: Option[String]): String = { routes.groupBy(_.call.packageName).map { case (packageName, routes) => { """ |%s |package %s.javascript { |import ReverseRouteContext.empty |%s |} """.stripMargin.format( markLines(routes: _*), namespace.map(_ + "." + packageName).getOrElse(packageName), routes.groupBy(_.call.controller).map { case (controller, routes) => """ |%s |class Reverse%s { | |%s | |} """.stripMargin.format( markLines(routes: _*), // alias controller.replace(".", "_"), // reverse method routes.groupBy(r => r.call.method -> r.call.parameters.getOrElse(Nil).map(p => p.typeName)).map { case ((m, _), routes) => assert(routes.size > 0, "Empty routes set???") val parameters = routes(0).call.parameters.getOrElse(Nil) val reverseParameters = parameters.zipWithIndex.filterNot { case (p, i) => { val fixeds = routes.map(_.call.parameters.get(i).fixed).distinct fixeds.size == 1 && fixeds(0) != None } } def genCall(route: Route, localNames: Map[String, String] = Map()) = " return _wA({method:\"%s\", url:%s%s})".format( route.verb.value, "\"\"\"\" + _prefix + " + { if (route.path.parts.isEmpty) "" else "{ _defaultPrefix } + " } + "\"\"\"\"" + route.path.parts.map { case StaticPart(part) => " + \"" + part + "\"" case DynamicPart(name, _, encode) => { route.call.parameters.getOrElse(Nil).find(_.name == name).map { param => if (encode && encodeable(param.typeName)) " + (\"\"\" + implicitly[PathBindable[" + param.typeName + "]].javascriptUnbind + \"\"\")" + """("""" + param.name + """", encodeURIComponent(""" + localNames.get(param.name).getOrElse(param.name) + """))""" else " + (\"\"\" + implicitly[PathBindable[" + param.typeName + "]].javascriptUnbind + \"\"\")" + """("""" + param.name + """", """ + localNames.get(param.name).getOrElse(param.name) + """)""" }.getOrElse { throw new Error("missing key " + name) } } }.mkString, { val queryParams = route.call.parameters.getOrElse(Nil).filterNot { p => p.fixed.isDefined || route.path.parts.collect { case DynamicPart(name, _, _) => name }.contains(p.name) } if (queryParams.size == 0) { "" } else { """ + _qS([%s])""".format( queryParams.map { p => ("(\"\"\" + implicitly[QueryStringBindable[" + p.typeName + "]].javascriptUnbind + \"\"\")" + """("""" + p.name + """", """ + localNames.get(p.name).getOrElse(p.name) + """)""") -> p }.map { case (u, Parameter(name, typeName, None, Some(default))) => """(""" + localNames.get(name).getOrElse(name) + " == null ? null : " + u + ")" case (u, Parameter(name, typeName, None, None)) => u }.mkString(", ")) } }) routes match { case Seq(route) => { """ |%s |def %s : JavascriptReverseRoute = JavascriptReverseRoute( | "%s", | %s | function(%s) { |%s | } | %s |) """.stripMargin.format( markLines(route), route.call.method, packageName + "." + controller + "." + route.call.method, "\"\"\"", reverseParameters.map(_._1.name).mkString(","), genCall(route), "\"\"\"") } case Seq(route, routes @ _*) => { """ |%s |def %s : JavascriptReverseRoute = JavascriptReverseRoute( | "%s", | %s | function(%s) { |%s | } | %s |) """.stripMargin.format( markLines((route +: routes): _*), route.call.method, packageName + "." + controller + "." + route.call.method, "\"\"\"", reverseParameters.map(_._1.name).mkString(", "), // route selection (route +: routes).map { route => val localNames = reverseParameters.map { case (lp, i) => route.call.parameters.get(i).name -> lp.name }.toMap " if (%s) {\n%s\n }".format( // Fixed constraints Option(route.call.parameters.getOrElse(Nil).filter { p => localNames.contains(p.name) && p.fixed.isDefined }.map { p => p.name + " == \"\"\" + implicitly[JavascriptLiteral[" + p.typeName + "]].to(" + p.fixed.get + ") + \"\"\"" }).filterNot(_.isEmpty).map(_.mkString(" && ")).getOrElse("true"), genCall(route, localNames)) }.mkString("\n"), "\"\"\"") } } }.mkString("\n") ) }.mkString("\n")) } }.mkString("\n") } /** * Generate the routing refs */ def refReverseRouting(routes: List[Route], namespace: Option[String]): String = { routes.groupBy(_.call.packageName).map { case (packageName, routes) => { """ |%s |package %s.ref { |%s |%s |} """.stripMargin.format( markLines(routes: _*), namespace.map(_ + "." + packageName).getOrElse(packageName), // This import statement is inserted mostly for the doc code samples, to ensure that controllers relative // to the namespace are in scope namespace.map("import " + _ + "._").getOrElse(""), routes.groupBy(_.call.controller).map { case (controller, routes) => """ |%s |class Reverse%s { | |%s | |} """.stripMargin.format( markLines(routes: _*), // alias controller.replace(".", "_"), // reverse method routes.groupBy(r => (r.call.method, r.call.parameters.getOrElse(Nil).map(p => p.typeName))).map { case ((m, _), routes) => assert(routes.size > 0, "Empty routes set???") val route = routes(0) val parameters = route.call.parameters.getOrElse(Nil) val reverseSignature = parameters.map(p => safeKeyword(p.name) + ":" + p.typeName).mkString(", ") val controllerCall = if (route.call.instantiate) { "play.api.Play.maybeApplication.map(_.global).getOrElse(play.api.DefaultGlobal).getControllerInstance(classOf[" + packageName + "." + controller + "])." + route.call.method + "(" + { parameters.map(x => safeKeyword(x.name)).mkString(", ") } + ")" } else { packageName + "." + controller + "." + route.call.method + "(" + { parameters.map(x => safeKeyword(x.name)).mkString(", ") } + ")" } """ |%s |def %s(%s): play.api.mvc.HandlerRef[_] = new play.api.mvc.HandlerRef( | %s, HandlerDef(this.getClass.getClassLoader, "%s", "%s", "%s", %s, "%s", %s, _prefix + %s) |) """.stripMargin.format( markLines(route), route.call.method, reverseSignature, controllerCall, namespace.getOrElse(""), packageName + "." + controller, route.call.method, "Seq(" + { parameters.map("classOf[" + _.typeName + "]").mkString(", ") } + ")", route.verb, "\"\"\"" + route.comments.map(_.comment).mkString("\n") + "\"\"\"", "\"\"\"" + route.path + "\"\"\"" ) }.mkString("\n") ) }.mkString("\n")) } }.mkString("\n") } /** * Generate the reverse routing operations */ def reverseRouting(routes: List[Route], namespace: Option[String]): String = { routes.groupBy(_.call.packageName).map { case (packageName, routes) => { """ |%s |package %s { |%s |} """.stripMargin.format( markLines(routes: _*), namespace.map(_ + "." + packageName).getOrElse(packageName), routes.groupBy(_.call.controller).map { case (controller, routes) => """ |%s |class Reverse%s { | |%s | |} """.stripMargin.format( markLines(routes: _*), // alias controller.replace(".", "_"), // reverse method routes.groupBy(r => (r.call.method, r.call.parameters.getOrElse(Nil).map(p => p.typeName))).map { case ((m, _), routes) => assert(routes.size > 0, "Empty routes set???") val parameters = routes(0).call.parameters.getOrElse(Nil) val reverseParameters = parameters.zipWithIndex.filterNot { case (p, i) => { val fixeds = routes.map(_.call.parameters.get(i).fixed).distinct fixeds.size == 1 && fixeds(0) != None } } def genReverseRouteContext(route: Route) = { val fixedParams = route.call.parameters.getOrElse(Nil).collect { case Parameter(name, _, Some(fixed), _) => "(\"%s\", %s)".format(name, fixed) } if (fixedParams.isEmpty) { "import ReverseRouteContext.empty" } else { "implicit val _rrc = new ReverseRouteContext(Map(%s))".format(fixedParams.mkString(", ")) } } val reverseSignature = reverseParameters.map(p => safeKeyword(p._1.name) + ":" + p._1.typeName + { Option(routes.map(_.call.parameters.get(p._2).default).distinct).filter(_.size == 1).flatMap(_.headOption).map { case None => "" case Some(default) => " = " + default }.getOrElse("") }).mkString(", ") def genCall(route: Route, localNames: Map[String, String] = Map()) = """Call("%s", %s%s)""".format( route.verb.value, "_prefix" + { if (route.path.parts.isEmpty) "" else """ + { _defaultPrefix } + """ } + route.path.parts.map { case StaticPart(part) => "\"" + part + "\"" case DynamicPart(name, _, encode) => { route.call.parameters.getOrElse(Nil).find(_.name == name).map { param => if (encode && encodeable(param.typeName)) """implicitly[PathBindable[""" + param.typeName + """]].unbind("""" + param.name + """", dynamicString(""" + safeKeyword(localNames.get(param.name).getOrElse(param.name)) + """))""" else """implicitly[PathBindable[""" + param.typeName + """]].unbind("""" + param.name + """", """ + safeKeyword(localNames.get(param.name).getOrElse(param.name)) + """)""" }.getOrElse { throw new Error("missing key " + name) } } }.mkString(" + "), { val queryParams = route.call.parameters.getOrElse(Nil).filterNot { p => p.fixed.isDefined || route.path.parts.collect { case DynamicPart(name, _, _) => name }.contains(p.name) } if (queryParams.size == 0) { "" } else { """ + queryString(List(%s))""".format( queryParams.map { p => ("""implicitly[QueryStringBindable[""" + p.typeName + """]].unbind("""" + p.name + """", """ + safeKeyword(localNames.get(p.name).getOrElse(p.name)) + """)""") -> p }.map { case (u, Parameter(name, typeName, None, Some(default))) => """if(""" + localNames.get(name).getOrElse(name) + """ == """ + default + """) None else Some(""" + u + """)""" case (u, Parameter(name, typeName, None, None)) => "Some(" + u + ")" }.mkString(", ")) } }) routes match { case Seq(route: RoutesCompiler.Route) => { """ |%s |def %s(%s): Call = { | %s | %s |} """.stripMargin.format( markLines(route), route.call.method, reverseSignature, genReverseRouteContext(route), genCall(route)) } case Seq(route: RoutesCompiler.Route, routes @ _*) => { """ |%s |def %s(%s): Call = { | (%s) match { |%s | } |} """.stripMargin.format( markLines((route +: routes): _*), route.call.method, reverseSignature, reverseParameters.map(x => safeKeyword(x._1.name) + ": @unchecked").mkString(", "), // route selection // We will generate a list of routes. Then we should remove duplicates from them. // Routes are considered duplicates if parameters and parameters constraints (see // definition below) are identical. ListMap((route +: routes).reverse.map { route => val localNames = reverseParameters.map { case (lp, i) => route.call.parameters.get(i).name -> lp.name }.toMap val markers = markLines(route) // Routes like /dummy controllers.Application.dummy(foo: String) // foo is the parameter val parameters = reverseParameters.map(x => safeKeyword(x._1.name)).mkString(", ") // Routes like /dummy controllers.Application.dummy(foo = "bar") // foo = "bar" is a constraint val parametersConstraints = route.call.parameters.getOrElse(Nil).filter { p => localNames.contains(p.name) && p.fixed.isDefined }.map { p => p.name + " == " + p.fixed.get } match { case Nil => "" case nonEmpty => "if " + nonEmpty.mkString(" && ") } val reverseRouteContext = genReverseRouteContext(route) val call = genCall(route, localNames) val result = """|%s |case (%s) %s => | %s | %s """.stripMargin.format(markers, parameters, parametersConstraints, reverseRouteContext, call) (parameters -> parametersConstraints) -> result }: _*).values.toSeq.reverse .mkString("\n")) } } }.mkString("\n") ) }.mkString("\n") ) } }.mkString("\n") } private def baseIdent(r: Route, i: Int): String = r.call.packageName.replace(".", "_") + "_" + r.call.controller.replace(".", "_") + "_" + r.call.method + i private def routeIdent(r: Route, i: Int): String = baseIdent(r, i) + "_route" private def invokerIdent(r: Route, i: Int): String = baseIdent(r, i) + "_invoker" private def controllerMethodCall(r: Route, paramFormat: Parameter => String): String = { val methodPart = if (r.call.instantiate) { "play.api.Play.maybeApplication.map(_.global).getOrElse(play.api.DefaultGlobal).getControllerInstance(classOf[" + r.call.packageName + "." + r.call.controller + "])." + r.call.method } else { r.call.packageName + "." + r.call.controller + "." + r.call.method } val paramPart = r.call.parameters.map { params => params.map(paramFormat).mkString(", ") }.map("(" + _ + ")").getOrElse("") methodPart + paramPart } /** * Generate the routes definitions */ def routeDefinitions(routerPackage: String, rules: List[Rule]): String = { rules.zipWithIndex.map { case (r @ Route(_, _, _, _), i) => val pattern = "PathPattern(List(StaticPart(Routes.prefix)" + { if (r.path.parts.isEmpty) "" else """,StaticPart(Routes.defaultPrefix),""" } + r.path.parts.map(_.toString).mkString(",") + "))" val fakeCall = controllerMethodCall(r, p => s"fakeValue[${p.typeName}]") val handlerDef = """HandlerDef(this.getClass.getClassLoader, """" + routerPackage + """", """" + r.call.packageName + "." + r.call.controller + """", """" + r.call.method + """", """ + r.call.parameters.filterNot(_.isEmpty).map { params => params.map("classOf[" + _.typeName + "]").mkString(", ") }.map("Seq(" + _ + ")").getOrElse("Nil") + ""","""" + r.verb + """", """ + "\"\"\"" + r.comments.map(_.comment).mkString("\n") + "\"\"\", Routes.prefix + \"\"\"" + r.path + "\"\"\")" s""" |${markLines(r)} |private[this] lazy val ${routeIdent(r, i)} = Route("${r.verb.value}", ${pattern}) |private[this] lazy val ${invokerIdent(r, i)} = createInvoker( |${fakeCall}, |${handlerDef}) """.stripMargin case (r @ Include(_, _), i) => """ |%s |lazy val %s%s = Include(%s) """.stripMargin.format( markLines(r), r.router.replace(".", "_"), i, r.router ) }.mkString("\n") + """| |def documentation = List(%s).foldLeft(List.empty[(String,String,String)]) { (s,e) => e.asInstanceOf[Any] match { | case r @ (_,_,_) => s :+ r.asInstanceOf[(String,String,String)] | case l => s ++ l.asInstanceOf[List[(String,String,String)]] |}} """.stripMargin.format( rules.map { case Route(verb, path, call, _) if path.parts.isEmpty => "(\"\"\"" + verb + "\"\"\", prefix,\"\"\"" + call + "\"\"\")" case Route(verb, path, call, _) => "(\"\"\"" + verb + "\"\"\", prefix + (if(prefix.endsWith(\"/\")) \"\" else \"/\") + \"\"\"" + path + "\"\"\",\"\"\"" + call + "\"\"\")" case Include(prefix, router) => router + ".documentation" }.mkString(",")) } private[this] def safeKeyword(keyword: String) = scalaReservedWords.find(_ == keyword).map( "playframework_escape_%s".format(_) ).getOrElse(keyword) private[this] def encodeable(paramType: String) = paramType == "String" /** * Generate the routing stuff */ def routing(routerPackage: String, routes: List[Rule]): String = { Option(routes.zipWithIndex.map { case (r @ Include(_, _), i) => """ |%s |case %s%s(handler) => handler """.stripMargin.format( markLines(r), r.router.replace(".", "_"), i ) case (r @ Route(_, _, _, _), i) => val binding = r.call.parameters.filterNot(_.isEmpty).map { params => params.map { p => p.fixed.map { v => """Param[""" + p.typeName + """]("""" + p.name + """", Right(""" + v + """))""" }.getOrElse { """params.""" + (if (r.path.has(p.name)) "fromPath" else "fromQuery") + """[""" + p.typeName + """]("""" + p.name + """", """ + p.default.map("Some(" + _ + ")").getOrElse("None") + """)""" } }.mkString(", ") }.map("(" + _ + ")").getOrElse("") val localNames = r.call.parameters.filterNot(_.isEmpty).map { params => params.map(x => safeKeyword(x.name)).mkString(", ") }.map("(" + _ + ") =>").getOrElse("") val call = controllerMethodCall(r, x => safeKeyword(x.name)) s""" |${markLines(r)} |case ${routeIdent(r, i)}(params) => { | call${binding} { ${localNames} | ${invokerIdent(r, i)}.call(${call}) | } |} """.stripMargin }.mkString("\n")).filterNot(_.isEmpty).getOrElse { """Map.empty""" // Empty partial function } } } Other Play Framework source code examplesHere is a short list of links related to this Play Framework RoutesCompiler.scala source code file: |
... this post is sponsored by my books ... | |
#1 New Release! |
FP Best Seller |
Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.
A percentage of advertising revenue from
pages under the /java/jwarehouse
URI on this website is
paid back to open source projects.