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

com.twitter.finagle.http.HttpMuxer.scala Maven / Gradle / Ivy

package com.twitter.finagle.http

import com.twitter.finagle.util.LoadService
import com.twitter.finagle.Service
import com.twitter.util.Future
import java.util.logging.Logger

/**
 * A service that dispatches incoming requests to registered handlers.
 * In order to choose which handler to dispatch the request to, we take the path of the request and match it with
 * the patterns of the pre-registered handlers. The pattern matching follows these rules:
 *
 *  - Patterns ending with "/" use exclusive prefix matching. Eg: the pattern "foo/bar/" matches these paths:
 *            "foo/bar/", "foo/bar/baz", etc but NOT "foo/bar"
 *    Similarly, the pattern "/" matches all paths
 *
 *  - Patterns not ending with "/" use exact matching. Eg: the pattern "foo/bar" ONLY matches this path:
 *            "foo/bar"
 *
 *  - Special case:
 *      The pattern "" matches only "/" and ""
 *
 *  NOTE: When multiple pattern matches exist, the longest pattern wins.
 */
class HttpMuxer(protected[this] val handlers: Seq[(String, Service[Request, Response])])
  extends Service[Request, Response] {

  def this() = this(Seq[(String, Service[Request, Response])]())

  private[this] val sorted: Seq[(String, Service[Request, Response])] =
    handlers.sortBy { case (pattern, _) => pattern.length }.reverse

  def patterns: Seq[String] = sorted map { case(p, _) => p }

  /**
   * Create a new Mux service with the specified pattern added. If the pattern already exists, overwrite existing value.
   * Pattern ending with "/" indicates prefix matching; otherwise exact matching.
   */
  def withHandler(pattern: String, service: Service[Request, Response]): HttpMuxer = {
    val norm = normalize(pattern)
    new HttpMuxer(handlers.filterNot { case (pat, _) => pat == norm } :+ ((norm, service)))
  }

  /**
   * Extract path from Request; look for a matching pattern; if found, dispatch the
   * Request to the registered service; otherwise create a NOT_FOUND response
   */
  def apply(request: Request): Future[Response] = {
    val path = normalize(request.path)

    // find the longest pattern that matches (the patterns are already sorted)
    val matching = sorted.find { case (pattern, _) =>
      if (pattern == "")
        path == "/" || path == "" // special cases
      else if (pattern.endsWith("/"))
        path.startsWith(pattern) // prefix match
      else
        path == pattern // exact match
    }

    matching match {
      case Some((_, service)) => service(request)
      case None => Future.value(Response(request.version, Status.NotFound))
    }
  }

  /**
   * - ensure path starts with "/"
   * - get rid of excessive "/"s. For example "/a//b///c/" => "/a/b/c/"
   * - return "" if path is ""
   * - return "/" if path is "/" or "///" etc
   */
  private[this] def normalize(path: String) = {
    val suffix = if (path.endsWith("/")) "/" else ""
    val p = path.split("/").filterNot(_.isEmpty).mkString("/")
    if (p == "") suffix else "/" + p + suffix
  }
}

/**
 * Singleton default multiplex service.
 *
 * @see [[HttpMuxers]] for Java compatibility APIs.
 */
object HttpMuxer extends Service[Request, Response] {
  @volatile private[this] var underlying = new HttpMuxer()

  override def apply(request: Request): Future[Response] =
    underlying(request)

  /**
   * add handlers to mutate dispatching strategies.
   */
  def addHandler(pattern: String, service: Service[Request, Response]): Unit = synchronized {
    underlying = underlying.withHandler(pattern, service)
  }

  def addRichHandler(pattern: String, service: Service[Request, Response]): Unit =
    addHandler(pattern, service)

  def patterns: Seq[String] = underlying.patterns

  private[this] val log = Logger.getLogger(getClass.getName)

  for (handler <- LoadService[HttpMuxHandler]()) {
    log.info("HttpMuxer[%s] = %s(%s)".format(handler.pattern, handler.getClass.getName, handler))
    addHandler(handler.pattern, handler)
  }
}

/**
 * Java compatibility APIs for [[HttpMuxer]].
 */
object HttpMuxers {

  /** See [[HttpMuxer.apply]] */
  def apply(request: Request): Future[Response] = HttpMuxer(request)

  /** See [[HttpMuxer.patterns]] */
  def patterns: Seq[String] = HttpMuxer.patterns

}

/**
 * Trait HttpMuxHandler is used for service-loading HTTP handlers.
 */
trait HttpMuxHandler extends Service[Request, Response] {
  /** The pattern that this handler gets bound to */
  val pattern: String
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy