alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Play Framework/Scala example source code file (RoutesCompiler.scala)

This example Play Framework source code file (RoutesCompiler.scala) is included in my "Source Code Warehouse" project. The intent of this project is to help you more easily find Play Framework (and Scala) source code examples by using tags.

All credit for the original source code belongs to Play Framework; I'm just trying to make examples easier to find. (For my Scala work, see my Scala examples and tutorials.)

Play Framework tags/keywords

boolean, collection, file, io, list, none, option, parse, parser, play, play framework, route, seq, some, string, utilities

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 examples

Here 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

 

new blog posts

 

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.