All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scalatra.PathPatternParsers.scala Maven / Gradle / Ivy

The newest version!
package org.scalatra

import scala.util.matching.Regex
import scala.util.parsing.combinator.RegexParsers

/**
 * A path pattern optionally matches a request path and extracts path
 * parameters.
 */
case class PathPattern(regex: Regex, captureGroupNames: List[String] = Nil) {

  def apply(path: String): Option[MultiParams] = {
    // This is a performance hotspot.  Hideous mutatations ahead.
    val m = regex.pattern.matcher(path)
    var multiParams = Map[String, Seq[String]]()
    if (m.matches) {
      var i = 0
      captureGroupNames foreach { name =>
        i += 1
        val value = m.group(i)
        if (value != null) {
          val values = multiParams.getOrElse(name, Vector()) :+ value
          multiParams = multiParams.updated(name, values)
        }
      }
      Some(multiParams)
    } else None
  }

  def +(pathPattern: PathPattern): PathPattern = PathPattern(
    new Regex(this.regex.toString + pathPattern.regex.toString),
    this.captureGroupNames ::: pathPattern.captureGroupNames)

}

/**
 * Parses a string into a path pattern for routing.
 */
trait PathPatternParser {

  def apply(pattern: String): PathPattern

}

object PathPatternParser {

  val PathReservedCharacters = "/?#"

}

trait RegexPathPatternParser extends PathPatternParser with RegexParsers {

  /**
   * This parser gradually builds a regular expression.  Some intermediate
   * strings are not valid regexes, so we wait to compile until the end.
   */
  protected case class PartialPathPattern(regex: String, captureGroupNames: List[String] = Nil) {

    def toPathPattern: PathPattern = PathPattern(regex.r, captureGroupNames)

    def +(other: PartialPathPattern): PartialPathPattern = PartialPathPattern(
      this.regex + other.regex,
      this.captureGroupNames ::: other.captureGroupNames)
  }
}

/**
 * A Sinatra-compatible route path pattern parser.
 */
class SinatraPathPatternParser extends RegexPathPatternParser {

  def apply(pattern: String): PathPattern =
    parseAll(pathPattern, pattern) match {
      case Success(pathPattern, _) =>
        (PartialPathPattern("^") + pathPattern + PartialPathPattern("$")).toPathPattern
      case _ =>
        throw new IllegalArgumentException("Invalid path pattern: " + pattern)
    }

  private def pathPattern: Parser[PartialPathPattern] = rep(token) ^^ { _.reduceLeft { _ + _ } }

  private def token: Parser[PartialPathPattern] = splat | namedGroup | literal

  private def splat: Parser[PartialPathPattern] = "*" ^^^ PartialPathPattern("(.*?)", List("splat"))

  private def namedGroup: Parser[PartialPathPattern] = ":" ~> """\w+""".r ^^
    { groupName => PartialPathPattern("([^/]+)", List(groupName)) }

  private def literal: Parser[PartialPathPattern] = metaChar | normalChar

  private def metaChar: Parser[PartialPathPattern] = """[\.\+\(\)\$]""".r ^^
    { c => PartialPathPattern("\\" + c) }

  private def normalChar: Parser[PartialPathPattern] = ".".r ^^ { c => PartialPathPattern(c) }

}

object SinatraPathPatternParser {

  def apply(pattern: String): PathPattern = new SinatraPathPatternParser().apply(pattern)

}

/**
 * Path pattern parser based on Rack::Mount::Strexp, which is used by Rails.
 */
class RailsPathPatternParser extends RegexPathPatternParser {

  def apply(pattern: String): PathPattern =
    parseAll(target, pattern) match {
      case Success(target, _) => target
      case _ =>
        throw new IllegalArgumentException("Invalid path pattern: " + pattern)
    }

  private def target = expr ^^
    { e => PartialPathPattern("\\A" + e.regex + "\\Z", e.captureGroupNames).toPathPattern }

  private def expr: Parser[PartialPathPattern] = rep1(token) ^^
    { _.reduceLeft { _ + _ } }

  private def token: Parser[PartialPathPattern] = param | glob | optional | static

  private def param: Parser[PartialPathPattern] = ":" ~> identifier ^^
    { name => PartialPathPattern("([^/.]+)", List(name)) }

  private def identifier = """[a-zA-Z_]\w*""".r

  private def glob: Parser[PartialPathPattern] = "*" ~> identifier ^^
    { name => PartialPathPattern("(.+)", List(name)) }

  private def optional: Parser[PartialPathPattern] = "(" ~> expr <~ ")" ^^
    { e => PartialPathPattern("(?:" + e.regex + ")?", e.captureGroupNames) }

  private def static: Parser[PartialPathPattern] = (escaped | char) ^^
    { str => PartialPathPattern(str) }

  private def escaped = literal("\\") ~> (char | paren)

  private def char = metachar | stdchar

  private def metachar = """[.^$|?+*{}\\\[\]-]""".r ^^ { "\\" + _ }

  private def stdchar = """[^()]""".r

  private def paren = ("(" | ")") ^^ { "\\" + _ }
}

object RailsPathPatternParser {

  def apply(pattern: String): PathPattern = new RailsPathPatternParser().apply(pattern)

}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy