+str #17361: Unified http java/scala projects except marshallers
This commit is contained in:
parent
454a393af1
commit
be82e85ffc
182 changed files with 13693 additions and 0 deletions
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.util
|
||||
|
||||
import akka.http.scaladsl.server.Route
|
||||
|
||||
private[util] abstract class ApplyConverterInstances {
|
||||
[#implicit def hac1[[#T1#]] = new ApplyConverter[Tuple1[[#T1#]]] {
|
||||
type In = ([#T1#]) ⇒ Route
|
||||
def apply(fn: In): (Tuple1[[#T1#]]) ⇒ Route = {
|
||||
case Tuple1([#t1#]) ⇒ fn([#t1#])
|
||||
}
|
||||
}#
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.util
|
||||
|
||||
private[util] abstract class ConstructFromTupleInstances {
|
||||
[#implicit def instance1[[#T1#], R](construct: ([#T1#]) => R): ConstructFromTuple[Tuple1[[#T1#]], R] =
|
||||
new ConstructFromTuple[Tuple1[[#T1#]], R] {
|
||||
def apply(tup: Tuple1[[#T1#]]): R = construct([#tup._1#])
|
||||
}#
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.util
|
||||
|
||||
import TupleOps.AppendOne
|
||||
|
||||
private[util] abstract class TupleAppendOneInstances {
|
||||
type Aux[P, S, Out0] = AppendOne[P, S] { type Out = Out0 }
|
||||
|
||||
implicit def append0[T1]: Aux[Unit, T1, Tuple1[T1]] =
|
||||
new AppendOne[Unit, T1] {
|
||||
type Out = Tuple1[T1]
|
||||
def apply(prefix: Unit, last: T1): Tuple1[T1] = Tuple1(last)
|
||||
}
|
||||
|
||||
[1..21#implicit def append1[[#T1#], L]: Aux[Tuple1[[#T1#]], L, Tuple2[[#T1#], L]] =
|
||||
new AppendOne[Tuple1[[#T1#]], L] {
|
||||
type Out = Tuple2[[#T1#], L]
|
||||
def apply(prefix: Tuple1[[#T1#]], last: L): Tuple2[[#T1#], L] = Tuple2([#prefix._1#], last)
|
||||
}#
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.util
|
||||
|
||||
import TupleOps.FoldLeft
|
||||
import BinaryPolyFunc.Case
|
||||
|
||||
private[util] abstract class TupleFoldInstances {
|
||||
|
||||
type Aux[In, T, Op, Out0] = FoldLeft[In, T, Op] { type Out = Out0 }
|
||||
|
||||
implicit def t0[In, Op]: Aux[In, Unit, Op, In] =
|
||||
new FoldLeft[In, Unit, Op] {
|
||||
type Out = In
|
||||
def apply(zero: In, tuple: Unit) = zero
|
||||
}
|
||||
|
||||
implicit def t1[In, A, Op](implicit f: Case[In, A, Op]): Aux[In, Tuple1[A], Op, f.Out] =
|
||||
new FoldLeft[In, Tuple1[A], Op] {
|
||||
type Out = f.Out
|
||||
def apply(zero: In, tuple: Tuple1[A]) = f(zero, tuple._1)
|
||||
}
|
||||
|
||||
[2..22#implicit def t1[In, [2..#T0#], X, T1, Op](implicit fold: Aux[In, Tuple0[[2..#T0#]], Op, X], f: Case[X, T1, Op]): Aux[In, Tuple1[[#T1#]], Op, f.Out] =
|
||||
new FoldLeft[In, Tuple1[[#T1#]], Op] {
|
||||
type Out = f.Out
|
||||
def apply(zero: In, t: Tuple1[[#T1#]]) =
|
||||
f(fold(zero, Tuple0([2..#t._0#])), t._1)
|
||||
}#
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
/**
|
||||
* Helper class to steer around SI-9013.
|
||||
*
|
||||
* It's currently impossible to implement a trait containing @varargs methods
|
||||
* if the trait is written in Scala. Therefore, derive from this class and
|
||||
* implement the method without varargs.
|
||||
* FIXME: remove once SI-9013 is fixed.
|
||||
*
|
||||
* See https://issues.scala-lang.org/browse/SI-9013
|
||||
*/
|
||||
abstract class AbstractDirective implements Directive {
|
||||
@Override
|
||||
public Route route(Route first, Route... others) {
|
||||
return createRoute(first, others);
|
||||
}
|
||||
|
||||
protected abstract Route createRoute(Route first, Route[] others);
|
||||
}
|
||||
35
akka-http/src/main/java/akka/http/javadsl/server/Coder.java
Normal file
35
akka-http/src/main/java/akka/http/javadsl/server/Coder.java
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
import akka.http.scaladsl.coding.Deflate$;
|
||||
import akka.http.scaladsl.coding.Gzip$;
|
||||
import akka.http.scaladsl.coding.NoCoding$;
|
||||
import akka.stream.FlowMaterializer;
|
||||
import akka.util.ByteString;
|
||||
import scala.concurrent.Future;
|
||||
|
||||
/**
|
||||
* A coder is an implementation of the predefined encoders/decoders defined for HTTP.
|
||||
*/
|
||||
public enum Coder {
|
||||
NoCoding(NoCoding$.MODULE$), Deflate(Deflate$.MODULE$), Gzip(Gzip$.MODULE$);
|
||||
|
||||
private akka.http.scaladsl.coding.Coder underlying;
|
||||
|
||||
Coder(akka.http.scaladsl.coding.Coder underlying) {
|
||||
this.underlying = underlying;
|
||||
}
|
||||
|
||||
public ByteString encode(ByteString input) {
|
||||
return underlying.encode(input);
|
||||
}
|
||||
public Future<ByteString> decode(ByteString input, FlowMaterializer mat) {
|
||||
return underlying.decode(input, mat);
|
||||
}
|
||||
public akka.http.scaladsl.coding.Coder _underlyingScalaCoder() {
|
||||
return underlying;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
|
||||
/**
|
||||
* A directive is the basic building block for building routes by composing
|
||||
* any kind of request or response processing into the main route a request
|
||||
* flows through. It is a factory that creates a route when given a sequence of
|
||||
* route alternatives to be augmented with the function the directive
|
||||
* represents.
|
||||
*
|
||||
* The `path`-Directive, for example, filters incoming requests by checking if
|
||||
* the URI of the incoming request matches the pattern and only invokes its inner
|
||||
* routes for those requests.
|
||||
*/
|
||||
public interface Directive {
|
||||
/**
|
||||
* Creates the Route given a sequence of inner route alternatives.
|
||||
*/
|
||||
Route route(Route first, Route... others);
|
||||
}
|
||||
43
akka-http/src/main/resources/reference.conf
Normal file
43
akka-http/src/main/resources/reference.conf
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#######################################
|
||||
# akka-http Reference Config File #
|
||||
#######################################
|
||||
|
||||
# This is the reference config file that contains all the default settings.
|
||||
# Make your edits/overrides in your application.conf.
|
||||
|
||||
akka.http.routing {
|
||||
# Enables/disables the returning of more detailed error messages to the
|
||||
# client in the error response
|
||||
# Should be disabled for browser-facing APIs due to the risk of XSS attacks
|
||||
# and (probably) enabled for internal or non-browser APIs
|
||||
# (Note that akka-http will always produce log messages containing the full error details)
|
||||
verbose-error-messages = off
|
||||
|
||||
# Enables/disables ETag and `If-Modified-Since` support for FileAndResourceDirectives
|
||||
file-get-conditional = on
|
||||
|
||||
# Enables/disables the rendering of the "rendered by" footer in directory listings
|
||||
render-vanity-footer = yes
|
||||
|
||||
# The maximum size between two requested ranges. Ranges with less space in between will be coalesced.
|
||||
#
|
||||
# When multiple ranges are requested, a server may coalesce any of the ranges that overlap or that are separated
|
||||
# by a gap that is smaller than the overhead of sending multiple parts, regardless of the order in which the
|
||||
# corresponding byte-range-spec appeared in the received Range header field. Since the typical overhead between
|
||||
# parts of a multipart/byteranges payload is around 80 bytes, depending on the selected representation's
|
||||
# media type and the chosen boundary parameter length, it can be less efficient to transfer many small
|
||||
# disjoint parts than it is to transfer the entire selected representation.
|
||||
range-coalescing-threshold = 80
|
||||
|
||||
# The maximum number of allowed ranges per request.
|
||||
# Requests with more ranges will be rejected due to DOS suspicion.
|
||||
range-count-limit = 16
|
||||
|
||||
# The maximum number of bytes per ByteString a decoding directive will produce
|
||||
# for an entity data stream.
|
||||
decode-max-bytes-per-chunk = 1m
|
||||
|
||||
# Fully qualified config path which holds the dispatcher configuration
|
||||
# to be used by FlowMaterialiser when creating Actors for IO operations.
|
||||
file-io-dispatcher = ${akka.stream.file-io-dispatcher}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.reflect.ClassTag
|
||||
import akka.http.javadsl.server.{ RequestContext, RequestVal }
|
||||
import akka.http.impl.util.JavaMapping.Implicits._
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] trait ExtractionImplBase[T] extends RequestVal[T] {
|
||||
protected[http] implicit def classTag: ClassTag[T]
|
||||
def resultClass: Class[T] = classTag.runtimeClass.asInstanceOf[Class[T]]
|
||||
|
||||
def get(ctx: RequestContext): T =
|
||||
ctx.request.asScala.header[ExtractionMap].flatMap(_.get(this))
|
||||
.getOrElse(throw new RuntimeException(s"Value wasn't extracted! $this"))
|
||||
}
|
||||
|
||||
private[http] abstract class ExtractionImpl[T](implicit val classTag: ClassTag[T]) extends ExtractionImplBase[T]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import akka.http.javadsl.server.Marshaller
|
||||
import akka.http.scaladsl.marshalling
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
// FIXME: too lenient visibility, currently used to implement Java marshallers, needs proper API, see #16439
|
||||
case class MarshallerImpl[T](scalaMarshaller: ExecutionContext ⇒ marshalling.ToResponseMarshaller[T]) extends Marshaller[T]
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.reflect.ClassTag
|
||||
import akka.http.javadsl.server.Parameter
|
||||
import akka.http.scaladsl.server.directives.{ ParameterDirectives, BasicDirectives }
|
||||
import akka.http.scaladsl.server.Directive1
|
||||
import akka.http.scaladsl.server.directives.ParameterDirectives.ParamMagnet
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] class ParameterImpl[T: ClassTag](val underlying: ExecutionContext ⇒ ParamMagnet { type Out = Directive1[T] })
|
||||
extends StandaloneExtractionImpl[T] with Parameter[T] {
|
||||
|
||||
//def extract(ctx: RequestContext): Future[T] =
|
||||
def directive: Directive1[T] =
|
||||
BasicDirectives.extractExecutionContext.flatMap { implicit ec ⇒
|
||||
ParameterDirectives.parameter(underlying(ec))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.reflect.ClassTag
|
||||
import akka.http.javadsl.server.PathMatcher
|
||||
import akka.http.scaladsl.server.{ PathMatcher ⇒ ScalaPathMatcher }
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] class PathMatcherImpl[T: ClassTag](val matcher: ScalaPathMatcher[Tuple1[T]])
|
||||
extends ExtractionImpl[T] with PathMatcher[T]
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.concurrent.Future
|
||||
import akka.http.javadsl.{ model ⇒ jm }
|
||||
import akka.http.impl.util.JavaMapping.Implicits._
|
||||
import akka.http.scaladsl.server.{ RequestContext ⇒ ScalaRequestContext }
|
||||
import akka.http.javadsl.server._
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] final case class RequestContextImpl(underlying: ScalaRequestContext) extends RequestContext {
|
||||
import underlying.executionContext
|
||||
|
||||
// provides auto-conversion to japi.RouteResult
|
||||
import RouteResultImpl._
|
||||
|
||||
def request: jm.HttpRequest = underlying.request
|
||||
def unmatchedPath: String = underlying.unmatchedPath.toString
|
||||
|
||||
def completeWith(futureResult: Future[RouteResult]): RouteResult =
|
||||
futureResult.flatMap {
|
||||
case r: RouteResultImpl ⇒ r.underlying
|
||||
}
|
||||
def complete(text: String): RouteResult = underlying.complete(text)
|
||||
def completeWithStatus(statusCode: Int): RouteResult =
|
||||
completeWithStatus(jm.StatusCodes.get(statusCode))
|
||||
def completeWithStatus(statusCode: jm.StatusCode): RouteResult =
|
||||
underlying.complete(statusCode.asScala)
|
||||
def completeAs[T](marshaller: Marshaller[T], value: T): RouteResult = marshaller match {
|
||||
case MarshallerImpl(m) ⇒
|
||||
implicit val marshaller = m(underlying.executionContext)
|
||||
underlying.complete(value)
|
||||
case _ ⇒ throw new IllegalArgumentException("Unsupported marshaller: $marshaller")
|
||||
}
|
||||
def complete(response: jm.HttpResponse): RouteResult = underlying.complete(response.asScala)
|
||||
|
||||
def notFound(): RouteResult = underlying.reject()
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.language.implicitConversions
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.immutable
|
||||
import akka.http.javadsl.model.ContentType
|
||||
import akka.http.scaladsl.server.directives.{ UserCredentials, ContentTypeResolver }
|
||||
import akka.http.scaladsl.server.directives.FileAndResourceDirectives.DirectoryRenderer
|
||||
import akka.http.scaladsl.model.HttpHeader
|
||||
import akka.http.scaladsl.model.headers.CustomHeader
|
||||
import akka.http.scaladsl.server.{ Route ⇒ ScalaRoute, Directive0, Directives }
|
||||
import akka.http.impl.util.JavaMapping.Implicits._
|
||||
import akka.http.scaladsl.server
|
||||
import akka.http.javadsl.server._
|
||||
import RouteStructure._
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] trait ExtractionMap extends CustomHeader {
|
||||
def get[T](key: RequestVal[T]): Option[T]
|
||||
def set[T](key: RequestVal[T], value: T): ExtractionMap
|
||||
}
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] object ExtractionMap {
|
||||
implicit def apply(map: Map[RequestVal[_], Any]): ExtractionMap =
|
||||
new ExtractionMap {
|
||||
def get[T](key: RequestVal[T]): Option[T] =
|
||||
map.get(key).asInstanceOf[Option[T]]
|
||||
|
||||
def set[T](key: RequestVal[T], value: T): ExtractionMap =
|
||||
ExtractionMap(map.updated(key, value))
|
||||
|
||||
// CustomHeader methods
|
||||
override def suppressRendering: Boolean = true
|
||||
def name(): String = "ExtractedValues"
|
||||
def value(): String = "<empty>"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] object RouteImplementation extends Directives with server.RouteConcatenation {
|
||||
def apply(route: Route): ScalaRoute = route match {
|
||||
case RouteAlternatives(children) ⇒
|
||||
val converted = children.map(RouteImplementation.apply)
|
||||
converted.reduce(_ ~ _)
|
||||
case RawPathPrefix(elements, children) ⇒
|
||||
val inner = apply(RouteAlternatives(children))
|
||||
|
||||
def one[T](matcher: PathMatcher[T]): Directive0 =
|
||||
rawPathPrefix(matcher.asInstanceOf[PathMatcherImpl[T]].matcher) flatMap { value ⇒
|
||||
addExtraction(matcher, value)
|
||||
}
|
||||
elements.map(one(_)).reduce(_ & _).apply(inner)
|
||||
|
||||
case GetFromResource(path, contentType, classLoader) ⇒
|
||||
getFromResource(path, contentType.asScala, classLoader)
|
||||
case GetFromResourceDirectory(path, classLoader, resolver) ⇒
|
||||
getFromResourceDirectory(path, classLoader)(scalaResolver(resolver))
|
||||
case GetFromFile(file, contentType) ⇒
|
||||
getFromFile(file, contentType.asScala)
|
||||
case GetFromDirectory(directory, true, resolver) ⇒
|
||||
extractExecutionContext { implicit ec ⇒
|
||||
getFromBrowseableDirectory(directory.getPath)(DirectoryRenderer.defaultDirectoryRenderer, scalaResolver(resolver))
|
||||
}
|
||||
case FileAndResourceRouteWithDefaultResolver(constructor) ⇒
|
||||
RouteImplementation(constructor(new directives.ContentTypeResolver {
|
||||
def resolve(fileName: String): ContentType = ContentTypeResolver.Default(fileName)
|
||||
}))
|
||||
|
||||
case MethodFilter(m, children) ⇒
|
||||
val inner = apply(RouteAlternatives(children))
|
||||
method(m.asScala).apply(inner)
|
||||
|
||||
case Extract(extractions, children) ⇒
|
||||
val inner = apply(RouteAlternatives(children))
|
||||
extractRequestContext.flatMap { ctx ⇒
|
||||
extractions.map { e ⇒
|
||||
e.directive.flatMap(addExtraction(e.asInstanceOf[RequestVal[Any]], _))
|
||||
}.reduce(_ & _)
|
||||
}.apply(inner)
|
||||
|
||||
case BasicAuthentication(authenticator, children) ⇒
|
||||
val inner = apply(RouteAlternatives(children))
|
||||
authenticateBasicAsync(authenticator.realm, { creds ⇒
|
||||
val javaCreds =
|
||||
creds match {
|
||||
case UserCredentials.Missing ⇒
|
||||
new BasicUserCredentials {
|
||||
def available: Boolean = false
|
||||
def userName: String = throw new IllegalStateException("Credentials missing")
|
||||
def verifySecret(secret: String): Boolean = throw new IllegalStateException("Credentials missing")
|
||||
}
|
||||
case p @ UserCredentials.Provided(name) ⇒
|
||||
new BasicUserCredentials {
|
||||
def available: Boolean = true
|
||||
def userName: String = name
|
||||
def verifySecret(secret: String): Boolean = p.verifySecret(secret)
|
||||
}
|
||||
}
|
||||
|
||||
authenticator.authenticate(javaCreds)
|
||||
}).flatMap { user ⇒
|
||||
addExtraction(authenticator.asInstanceOf[RequestVal[Any]], user)
|
||||
}.apply(inner)
|
||||
|
||||
case EncodeResponse(coders, children) ⇒
|
||||
val scalaCoders = coders.map(_._underlyingScalaCoder())
|
||||
encodeResponseWith(scalaCoders.head, scalaCoders.tail: _*).apply(apply(RouteAlternatives(children)))
|
||||
|
||||
case Conditional(eTag, lastModified, children) ⇒
|
||||
conditional(eTag.asScala, lastModified.asScala).apply(apply(RouteAlternatives(children)))
|
||||
|
||||
case o: OpaqueRoute ⇒
|
||||
(ctx ⇒ o.handle(new RequestContextImpl(ctx)).asInstanceOf[RouteResultImpl].underlying)
|
||||
|
||||
case p: Product ⇒ extractExecutionContext { implicit ec ⇒ complete(500, s"Not implemented: ${p.productPrefix}") }
|
||||
}
|
||||
|
||||
def addExtraction[T](key: RequestVal[T], value: T): Directive0 = {
|
||||
@tailrec def addToExtractionMap(headers: immutable.Seq[HttpHeader], prefix: Vector[HttpHeader] = Vector.empty): immutable.Seq[HttpHeader] =
|
||||
headers match {
|
||||
case (m: ExtractionMap) +: rest ⇒ m.set(key, value) +: (prefix ++ rest)
|
||||
case other +: rest ⇒ addToExtractionMap(rest, prefix :+ other)
|
||||
case Nil ⇒ ExtractionMap(Map(key -> value)) +: prefix
|
||||
}
|
||||
mapRequest(_.mapHeaders(addToExtractionMap(_)))
|
||||
}
|
||||
|
||||
private def scalaResolver(resolver: directives.ContentTypeResolver): ContentTypeResolver =
|
||||
ContentTypeResolver(f ⇒ resolver.resolve(f).asScala)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.language.implicitConversions
|
||||
import scala.concurrent.Future
|
||||
import akka.http.javadsl.{ server ⇒ js }
|
||||
import akka.http.scaladsl.{ server ⇒ ss }
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] class RouteResultImpl(val underlying: Future[ss.RouteResult]) extends js.RouteResult
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] object RouteResultImpl {
|
||||
implicit def autoConvert(result: Future[ss.RouteResult]): js.RouteResult =
|
||||
new RouteResultImpl(result)
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import java.io.File
|
||||
import scala.language.existentials
|
||||
import scala.collection.immutable
|
||||
import akka.http.javadsl.model.{ DateTime, ContentType, HttpMethod }
|
||||
import akka.http.javadsl.model.headers.EntityTag
|
||||
import akka.http.javadsl.server.directives.ContentTypeResolver
|
||||
import akka.http.javadsl.server._
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] object RouteStructure {
|
||||
trait DirectiveRoute extends Route {
|
||||
def children: immutable.Seq[Route]
|
||||
|
||||
require(children.nonEmpty)
|
||||
}
|
||||
case class RouteAlternatives(children: immutable.Seq[Route]) extends DirectiveRoute
|
||||
|
||||
case class MethodFilter(method: HttpMethod, children: immutable.Seq[Route]) extends DirectiveRoute {
|
||||
def filter(ctx: RequestContext): Boolean = ctx.request.method == method
|
||||
}
|
||||
|
||||
abstract case class FileAndResourceRouteWithDefaultResolver(routeConstructor: ContentTypeResolver ⇒ Route) extends Route
|
||||
case class GetFromResource(resourcePath: String, contentType: ContentType, classLoader: ClassLoader) extends Route
|
||||
case class GetFromResourceDirectory(resourceDirectory: String, classLoader: ClassLoader, resolver: ContentTypeResolver) extends Route
|
||||
case class GetFromFile(file: File, contentType: ContentType) extends Route
|
||||
case class GetFromDirectory(directory: File, browseable: Boolean, resolver: ContentTypeResolver) extends Route
|
||||
|
||||
case class RawPathPrefix(pathElements: immutable.Seq[PathMatcher[_]], children: immutable.Seq[Route]) extends DirectiveRoute
|
||||
case class Extract(extractions: Seq[StandaloneExtractionImpl[_]], children: immutable.Seq[Route]) extends DirectiveRoute
|
||||
case class BasicAuthentication(authenticator: HttpBasicAuthenticator[_], children: immutable.Seq[Route]) extends DirectiveRoute
|
||||
case class EncodeResponse(coders: immutable.Seq[Coder], children: immutable.Seq[Route]) extends DirectiveRoute
|
||||
|
||||
case class Conditional(entityTag: EntityTag, lastModified: DateTime, children: immutable.Seq[Route]) extends DirectiveRoute
|
||||
|
||||
abstract class OpaqueRoute(extractions: RequestVal[_]*) extends Route {
|
||||
def handle(ctx: RequestContext): RouteResult
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.reflect.ClassTag
|
||||
import akka.http.javadsl.server.RequestVal
|
||||
import akka.http.scaladsl.server._
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] abstract class StandaloneExtractionImpl[T: ClassTag] extends ExtractionImpl[T] with RequestVal[T] {
|
||||
def directive: Directive1[T]
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] abstract class ExtractingStandaloneExtractionImpl[T: ClassTag] extends StandaloneExtractionImpl[T] {
|
||||
def directive: Directive1[T] = Directives.extract(extract).flatMap(Directives.onSuccess(_))
|
||||
|
||||
def extract(ctx: RequestContext): Future[T]
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.reflect.ClassTag
|
||||
import akka.stream.FlowMaterializer
|
||||
import akka.http.javadsl.server.Unmarshaller
|
||||
import akka.http.scaladsl.unmarshalling.FromMessageUnmarshaller
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*
|
||||
*/
|
||||
// FIXME: too lenient visibility, currently used to implement Java marshallers, needs proper API, see #16439
|
||||
case class UnmarshallerImpl[T](scalaUnmarshaller: (ExecutionContext, FlowMaterializer) ⇒ FromMessageUnmarshaller[T])(implicit val classTag: ClassTag[T])
|
||||
extends Unmarshaller[T]
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import akka.http.javadsl.server.directives._
|
||||
import scala.collection.immutable
|
||||
import scala.annotation.varargs
|
||||
import akka.http.javadsl.model.HttpMethods
|
||||
|
||||
// FIXME: add support for the remaining directives, see #16436
|
||||
abstract class AllDirectives extends PathDirectives
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
object Directives extends AllDirectives {
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] def custom(f: immutable.Seq[Route] ⇒ Route): Directive =
|
||||
new AbstractDirective {
|
||||
def createRoute(first: Route, others: Array[Route]): Route = f(first +: others.toVector)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
/**
|
||||
* A route Handler that handles a request (that is encapsulated in a [[RequestContext]])
|
||||
* and returns a [[RouteResult]] with the response (or the rejection).
|
||||
*
|
||||
* Use the methods in [[RequestContext]] to create a [[RouteResult]]. A handler mustn't
|
||||
* return [[null]] as the result.
|
||||
*/
|
||||
trait Handler {
|
||||
def handle(ctx: RequestContext): RouteResult
|
||||
}
|
||||
|
||||
/**
|
||||
* A route handler with one additional argument.
|
||||
*/
|
||||
trait Handler1[T1] {
|
||||
def handle(ctx: RequestContext, t1: T1): RouteResult
|
||||
}
|
||||
|
||||
/**
|
||||
* A route handler with two additional arguments.
|
||||
*/
|
||||
trait Handler2[T1, T2] {
|
||||
def handle(ctx: RequestContext, t1: T1, t2: T2): RouteResult
|
||||
}
|
||||
|
||||
/**
|
||||
* A route handler with three additional arguments.
|
||||
*/
|
||||
trait Handler3[T1, T2, T3] {
|
||||
def handle(ctx: RequestContext, t1: T1, t2: T2, t3: T3): RouteResult
|
||||
}
|
||||
|
||||
/**
|
||||
* A route handler with four additional arguments.
|
||||
*/
|
||||
trait Handler4[T1, T2, T3, T4] {
|
||||
def handle(ctx: RequestContext, t1: T1, t2: T2, t3: T3, t4: T4): RouteResult
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import scala.concurrent.Future
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http.ServerBinding
|
||||
|
||||
/**
|
||||
* A convenience class to derive from to get everything from HttpService and Directives into scope.
|
||||
* Implement the [[HttpApp.createRoute]] method to provide the Route and then call [[HttpApp.bindRoute]]
|
||||
* to start the server on the specified interface.
|
||||
*/
|
||||
abstract class HttpApp
|
||||
extends AllDirectives
|
||||
with HttpServiceBase {
|
||||
protected def createRoute(): Route
|
||||
|
||||
/**
|
||||
* Starts an HTTP server on the given interface and port. Creates the route by calling the
|
||||
* user-implemented [[createRoute]] method and uses the route to handle requests of the server.
|
||||
*/
|
||||
def bindRoute(interface: String, port: Int, system: ActorSystem): Future[ServerBinding] =
|
||||
bindRoute(interface, port, createRoute(), system)
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import akka.http.impl.server.{ ExtractionImplBase, ExtractionImpl, RouteStructure }
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
|
||||
import scala.annotation.varargs
|
||||
import scala.concurrent.Future
|
||||
import scala.reflect
|
||||
import reflect.ClassTag
|
||||
|
||||
/**
|
||||
* Represents existing or missing HTTP Basic authentication credentials.
|
||||
*/
|
||||
trait BasicUserCredentials {
|
||||
/**
|
||||
* Were credentials provided in the request?
|
||||
*/
|
||||
def available: Boolean
|
||||
|
||||
/**
|
||||
* The username as sent in the request.
|
||||
*/
|
||||
def userName: String
|
||||
/**
|
||||
* Verifies the given secret against the one sent in the request.
|
||||
*/
|
||||
def verifySecret(secret: String): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement this class to provide an HTTP Basic authentication check. The [[authenticate]] method needs to be implemented
|
||||
* to check if the supplied or missing credentials are authenticated and provide a domain level object representing
|
||||
* the user as a [[RequestVal]].
|
||||
*/
|
||||
abstract class HttpBasicAuthenticator[T](val realm: String) extends AbstractDirective with ExtractionImplBase[T] with RequestVal[T] {
|
||||
protected[http] implicit def classTag: ClassTag[T] = reflect.classTag[AnyRef].asInstanceOf[ClassTag[T]]
|
||||
def authenticate(credentials: BasicUserCredentials): Future[Option[T]]
|
||||
|
||||
/**
|
||||
* Creates a return value for use in [[authenticate]] that successfully authenticates the requests and provides
|
||||
* the given user.
|
||||
*/
|
||||
def authenticateAs(user: T): Future[Option[T]] = FastFuture.successful(Some(user))
|
||||
|
||||
/**
|
||||
* Refuses access for this user.
|
||||
*/
|
||||
def refuseAccess(): Future[Option[T]] = FastFuture.successful(None)
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
protected[http] final def createRoute(first: Route, others: Array[Route]): Route =
|
||||
RouteStructure.BasicAuthentication(this, (first +: others).toVector)
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import scala.concurrent.Future
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.{ server, Http }
|
||||
import akka.http.scaladsl.Http.ServerBinding
|
||||
import akka.http.impl.server.RouteImplementation
|
||||
import akka.stream.{ ActorFlowMaterializer, FlowMaterializer }
|
||||
import akka.stream.scaladsl.{ Keep, Sink }
|
||||
|
||||
trait HttpServiceBase {
|
||||
/**
|
||||
* Starts a server on the given interface and port and uses the route to handle incoming requests.
|
||||
*/
|
||||
def bindRoute(interface: String, port: Int, route: Route, system: ActorSystem): Future[ServerBinding] = {
|
||||
implicit val sys = system
|
||||
implicit val mat = ActorFlowMaterializer()
|
||||
handleConnectionsWithRoute(interface, port, route, system, mat)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a server on the given interface and port and uses the route to handle incoming requests.
|
||||
*/
|
||||
def bindRoute(interface: String, port: Int, route: Route, system: ActorSystem, flowMaterializer: FlowMaterializer): Future[ServerBinding] =
|
||||
handleConnectionsWithRoute(interface, port, route, system, flowMaterializer)
|
||||
|
||||
/**
|
||||
* Uses the route to handle incoming connections and requests for the ServerBinding.
|
||||
*/
|
||||
def handleConnectionsWithRoute(interface: String, port: Int, route: Route, system: ActorSystem, flowMaterializer: FlowMaterializer): Future[ServerBinding] = {
|
||||
implicit val sys = system
|
||||
implicit val mat = flowMaterializer
|
||||
|
||||
import system.dispatcher
|
||||
val r: server.Route = RouteImplementation(route)
|
||||
Http(system).bind(interface, port).toMat(Sink.foreach(_.handleWith(r)))(Keep.left).run()(flowMaterializer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the entrypoints to create an Http server from a route.
|
||||
*/
|
||||
object HttpService extends HttpServiceBase
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
/**
|
||||
* A marker trait for a marshaller that converts a value of type [[T]] to an
|
||||
* HttpResponse.
|
||||
*/
|
||||
trait Marshaller[T]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import akka.http.scaladsl.marshalling.ToResponseMarshaller
|
||||
import akka.http.impl.server.MarshallerImpl
|
||||
|
||||
/**
|
||||
* A collection of predefined marshallers.
|
||||
*/
|
||||
object Marshallers {
|
||||
def STRING: Marshaller[String] = MarshallerImpl(implicit ctx ⇒ implicitly[ToResponseMarshaller[String]])
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import java.{ lang ⇒ jl }
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.reflect.ClassTag
|
||||
|
||||
import akka.http.scaladsl.server.Directive1
|
||||
import akka.http.scaladsl.server.directives.ParameterDirectives.ParamMagnet
|
||||
import akka.http.scaladsl.common.ToNameReceptacleEnhancements
|
||||
import akka.http.impl.server.ParameterImpl
|
||||
|
||||
/**
|
||||
* A RequestVal representing a query parameter of type T.
|
||||
*/
|
||||
trait Parameter[T] extends RequestVal[T]
|
||||
|
||||
/**
|
||||
* A collection of predefined parameters.
|
||||
* FIXME: add tests, see #16437
|
||||
*/
|
||||
object Parameters {
|
||||
import ToNameReceptacleEnhancements._
|
||||
|
||||
/**
|
||||
* A string query parameter.
|
||||
*/
|
||||
def string(name: String): Parameter[String] =
|
||||
fromScalaParam(implicit ec ⇒ ParamMagnet(name))
|
||||
|
||||
/**
|
||||
* An integer query parameter.
|
||||
*/
|
||||
def integer(name: String): Parameter[jl.Integer] =
|
||||
fromScalaParam[jl.Integer](implicit ec ⇒
|
||||
ParamMagnet(name.as[Int]).asInstanceOf[ParamMagnet { type Out = Directive1[jl.Integer] }])
|
||||
|
||||
private def fromScalaParam[T: ClassTag](underlying: ExecutionContext ⇒ ParamMagnet { type Out = Directive1[T] }): Parameter[T] =
|
||||
new ParameterImpl[T](underlying)
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import java.{ lang ⇒ jl, util ⇒ ju }
|
||||
import scala.reflect.ClassTag
|
||||
import scala.collection.JavaConverters._
|
||||
import akka.http.impl.server.PathMatcherImpl
|
||||
import akka.http.scaladsl.server.{ PathMatchers ⇒ ScalaPathMatchers, PathMatcher0, PathMatcher1 }
|
||||
|
||||
/**
|
||||
* A PathMatcher is used to match the (yet unmatched) URI path of incoming requests.
|
||||
* It is also a RequestVal that allows to access dynamic parts of the part in a
|
||||
* handler.
|
||||
*
|
||||
* Using a PathMatcher with the [[Directives.path]] or [[Directives.pathPrefix]] directives
|
||||
* "consumes" a part of the path which is recorded in [[RequestContext.unmatchedPath]].
|
||||
*/
|
||||
trait PathMatcher[T] extends RequestVal[T]
|
||||
|
||||
/**
|
||||
* A collection of predefined path matchers.
|
||||
*/
|
||||
object PathMatchers {
|
||||
val NEUTRAL: PathMatcher[Void] = matcher0(_.Neutral)
|
||||
val SLASH: PathMatcher[Void] = matcher0(_.Slash)
|
||||
val END: PathMatcher[Void] = matcher0(_.PathEnd)
|
||||
|
||||
def segment(name: String): PathMatcher[String] = matcher(_ ⇒ name -> name)
|
||||
|
||||
def integerNumber: PathMatcher[jl.Integer] = matcher(_.IntNumber.asInstanceOf[PathMatcher1[jl.Integer]])
|
||||
def hexIntegerNumber: PathMatcher[jl.Integer] = matcher(_.HexIntNumber.asInstanceOf[PathMatcher1[jl.Integer]])
|
||||
|
||||
def longNumber: PathMatcher[jl.Long] = matcher(_.LongNumber.asInstanceOf[PathMatcher1[jl.Long]])
|
||||
def hexLongNumber: PathMatcher[jl.Long] = matcher(_.HexLongNumber.asInstanceOf[PathMatcher1[jl.Long]])
|
||||
|
||||
def uuid: PathMatcher[ju.UUID] = matcher(_.JavaUUID)
|
||||
|
||||
def segment: PathMatcher[String] = matcher(_.Segment)
|
||||
def segments: PathMatcher[ju.List[String]] = matcher(_.Segments.map(_.asJava))
|
||||
def segments(maxNumber: Int): PathMatcher[ju.List[String]] = matcher(_.Segments(maxNumber).map(_.asJava))
|
||||
|
||||
def rest: PathMatcher[String] = matcher(_.Rest)
|
||||
|
||||
private def matcher[T: ClassTag](scalaMatcher: ScalaPathMatchers.type ⇒ PathMatcher1[T]): PathMatcher[T] =
|
||||
new PathMatcherImpl[T](scalaMatcher(ScalaPathMatchers))
|
||||
private def matcher0(scalaMatcher: ScalaPathMatchers.type ⇒ PathMatcher0): PathMatcher[Void] =
|
||||
new PathMatcherImpl[Void](scalaMatcher(ScalaPathMatchers).tmap(_ ⇒ Tuple1(null)))
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import akka.http.javadsl.model._
|
||||
import akka.util.ByteString
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
/**
|
||||
* The RequestContext represents the state of the request while it is routed through
|
||||
* the route structure.
|
||||
*/
|
||||
trait RequestContext {
|
||||
/**
|
||||
* The incoming request.
|
||||
*/
|
||||
def request: HttpRequest
|
||||
|
||||
/**
|
||||
* The still unmatched path of the request.
|
||||
*/
|
||||
def unmatchedPath: String
|
||||
|
||||
/**
|
||||
* Completes the request with a value of type T and marshals it using the given
|
||||
* marshaller.
|
||||
*/
|
||||
def completeAs[T](marshaller: Marshaller[T], value: T): RouteResult
|
||||
|
||||
/**
|
||||
* Completes the request with the given response.
|
||||
*/
|
||||
def complete(response: HttpResponse): RouteResult
|
||||
|
||||
/**
|
||||
* Completes the request with the given string as an entity of type `text/plain`.
|
||||
*/
|
||||
def complete(text: String): RouteResult
|
||||
|
||||
/**
|
||||
* Completes the request with the given status code and no entity.
|
||||
*/
|
||||
def completeWithStatus(statusCode: StatusCode): RouteResult
|
||||
|
||||
/**
|
||||
* Completes the request with the given status code and no entity.
|
||||
*/
|
||||
def completeWithStatus(statusCode: Int): RouteResult
|
||||
|
||||
/**
|
||||
* Defers completion of the request
|
||||
*/
|
||||
def completeWith(futureResult: Future[RouteResult]): RouteResult
|
||||
|
||||
/**
|
||||
* Explicitly rejects the request as not found. Other route alternatives
|
||||
* may still be able provide a response.
|
||||
*/
|
||||
def notFound(): RouteResult
|
||||
|
||||
// FIXME: provide proper support for rejections, see #16438
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
/**
|
||||
* Represents a value that can be extracted from a request.
|
||||
*/
|
||||
trait RequestVal[T] { outer ⇒
|
||||
/**
|
||||
* An accessor for the value given the [[RequestContext]].
|
||||
*
|
||||
* Note, that some RequestVals need to be actively specified in the route structure to
|
||||
* be extracted at a particular point during routing. One example is a [[PathMatcher]]
|
||||
* that needs to used with a [[directives.PathDirectives]] to specify which part of the
|
||||
* path should actually be extracted. Another example is an [[HttpBasicAuthenticator]]
|
||||
* that needs to be used in the route explicitly to be activated.
|
||||
*/
|
||||
def get(ctx: RequestContext): T
|
||||
|
||||
/**
|
||||
* The runtime type of the extracted value.
|
||||
*/
|
||||
def resultClass: Class[T]
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import java.{ util ⇒ ju }
|
||||
import scala.concurrent.Future
|
||||
import scala.reflect.ClassTag
|
||||
import akka.http.javadsl.model.HttpMethod
|
||||
import akka.http.scaladsl.server
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.server.directives.{ RouteDirectives, BasicDirectives }
|
||||
import akka.http.impl.server.{ UnmarshallerImpl, ExtractingStandaloneExtractionImpl, RequestContextImpl, StandaloneExtractionImpl }
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
import akka.http.impl.util.JavaMapping.Implicits._
|
||||
|
||||
/**
|
||||
* A collection of predefined [[RequestVals]].
|
||||
*/
|
||||
object RequestVals {
|
||||
/**
|
||||
* Creates an extraction that extracts the request body using the supplied Unmarshaller.
|
||||
*/
|
||||
def entityAs[T](unmarshaller: Unmarshaller[T]): RequestVal[T] =
|
||||
new ExtractingStandaloneExtractionImpl[T]()(unmarshaller.classTag) {
|
||||
def extract(ctx: server.RequestContext): Future[T] = {
|
||||
val u = unmarshaller.asInstanceOf[UnmarshallerImpl[T]].scalaUnmarshaller(ctx.executionContext, ctx.flowMaterializer)
|
||||
u(ctx.request)(ctx.executionContext)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the request method.
|
||||
*/
|
||||
def requestMethod: RequestVal[HttpMethod] =
|
||||
new ExtractingStandaloneExtractionImpl[HttpMethod] {
|
||||
def extract(ctx: server.RequestContext): Future[HttpMethod] = FastFuture.successful(ctx.request.method.asJava)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new [[RequestVal]] given a [[ju.Map]] and a [[RequestVal]] that represents the key.
|
||||
* The new RequestVal represents the existing value as looked up in the map. If the key doesn't
|
||||
* exist the request is rejected.
|
||||
*/
|
||||
def lookupInMap[T, U](key: RequestVal[T], clazz: Class[U], map: ju.Map[T, U]): RequestVal[U] =
|
||||
new StandaloneExtractionImpl[U]()(ClassTag(clazz)) {
|
||||
import BasicDirectives._
|
||||
import RouteDirectives._
|
||||
|
||||
def directive: Directive1[U] =
|
||||
extract(ctx ⇒ key.get(RequestContextImpl(ctx))).flatMap {
|
||||
case key if map.containsKey(key) ⇒ provide(map.get(key))
|
||||
case _ ⇒ reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
/**
|
||||
* A marker interface to denote an element that handles a request.
|
||||
*
|
||||
* This is an opaque interface that cannot be implemented manually.
|
||||
* Instead, see the predefined routes in [[Directives]] and use the [[Directives.handleWith]]
|
||||
* method to create custom routes.
|
||||
*/
|
||||
trait Route
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
/**
|
||||
* A marker trait to denote the result of handling a request. Use the methods in [[RequestContext]]
|
||||
* to create instances of results.
|
||||
*/
|
||||
trait RouteResult
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import scala.reflect.ClassTag
|
||||
|
||||
/**
|
||||
* A marker trait for an unmarshaller that converts an HttpRequest to a value of type T.
|
||||
*/
|
||||
trait Unmarshaller[T] {
|
||||
def classTag: ClassTag[T]
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server.directives
|
||||
|
||||
import java.lang.reflect.Method
|
||||
import scala.annotation.varargs
|
||||
import akka.http.javadsl.server._
|
||||
import akka.http.impl.server.RouteStructure._
|
||||
import akka.http.impl.server._
|
||||
|
||||
abstract class BasicDirectives {
|
||||
/**
|
||||
* Tries the given routes in sequence until the first one matches.
|
||||
*/
|
||||
@varargs
|
||||
def route(route: Route, others: Route*): Route =
|
||||
RouteAlternatives(route +: others.toVector)
|
||||
|
||||
/**
|
||||
* A route that completes the request with a static text
|
||||
*/
|
||||
def complete(text: String): Route =
|
||||
new OpaqueRoute() {
|
||||
def handle(ctx: RequestContext): RouteResult = ctx.complete(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* A route that completes the request using the given marshaller and value.
|
||||
*/
|
||||
def completeAs[T](marshaller: Marshaller[T], value: T): Route =
|
||||
new OpaqueRoute() {
|
||||
def handle(ctx: RequestContext): RouteResult = ctx.completeAs(marshaller, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* A route that extracts a value and completes the request with it.
|
||||
*/
|
||||
def extractAndComplete[T](marshaller: Marshaller[T], extraction: RequestVal[T]): Route =
|
||||
handle(extraction)(ctx ⇒ ctx.completeAs(marshaller, extraction.get(ctx)))
|
||||
|
||||
/**
|
||||
* A directive that makes sure that all the standalone extractions have been
|
||||
* executed and validated.
|
||||
*/
|
||||
@varargs
|
||||
def extractHere(extractions: RequestVal[_]*): Directive =
|
||||
Directives.custom(Extract(extractions.map(_.asInstanceOf[StandaloneExtractionImpl[_ <: AnyRef]]), _))
|
||||
|
||||
/**
|
||||
* A route that handles the request with the given opaque handler. Specify a set of extractions
|
||||
* that will be used in the handler to make sure they are available.
|
||||
*/
|
||||
@varargs
|
||||
def handleWith[T1](handler: Handler, extractions: RequestVal[_]*): Route =
|
||||
handle(extractions: _*)(handler.handle(_))
|
||||
|
||||
/**
|
||||
* A route that handles the request given the value of a single [[RequestVal]].
|
||||
*/
|
||||
def handleWith[T1](e1: RequestVal[T1], handler: Handler1[T1]): Route =
|
||||
handle(e1)(ctx ⇒ handler.handle(ctx, e1.get(ctx)))
|
||||
|
||||
/**
|
||||
* A route that handles the request given the values of the given [[RequestVal]]s.
|
||||
*/
|
||||
def handleWith[T1, T2](e1: RequestVal[T1], e2: RequestVal[T2], handler: Handler2[T1, T2]): Route =
|
||||
handle(e1, e2)(ctx ⇒ handler.handle(ctx, e1.get(ctx), e2.get(ctx)))
|
||||
|
||||
/**
|
||||
* A route that handles the request given the values of the given [[RequestVal]]s.
|
||||
*/
|
||||
def handleWith[T1, T2, T3](
|
||||
e1: RequestVal[T1], e2: RequestVal[T2], e3: RequestVal[T3], handler: Handler3[T1, T2, T3]): Route =
|
||||
handle(e1, e2, e3)(ctx ⇒ handler.handle(ctx, e1.get(ctx), e2.get(ctx), e3.get(ctx)))
|
||||
|
||||
/**
|
||||
* A route that handles the request given the values of the given [[RequestVal]]s.
|
||||
*/
|
||||
def handleWith[T1, T2, T3, T4](
|
||||
e1: RequestVal[T1], e2: RequestVal[T2], e3: RequestVal[T3], e4: RequestVal[T4], handler: Handler4[T1, T2, T3, T4]): Route =
|
||||
handle(e1, e2, e3, e4)(ctx ⇒ handler.handle(ctx, e1.get(ctx), e2.get(ctx), e3.get(ctx), e4.get(ctx)))
|
||||
|
||||
private[http] def handle(extractions: RequestVal[_]*)(f: RequestContext ⇒ RouteResult): Route = {
|
||||
val route =
|
||||
new OpaqueRoute() {
|
||||
def handle(ctx: RequestContext): RouteResult = f(ctx)
|
||||
}
|
||||
val saExtractions = extractions.collect { case sa: StandaloneExtractionImpl[_] ⇒ sa }
|
||||
if (saExtractions.isEmpty) route
|
||||
else extractHere(saExtractions: _*).route(route)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the route by reflectively calling the instance method specified by `instance`, and `methodName`.
|
||||
* Additionally, the value of all extractions will be passed to the function.
|
||||
*
|
||||
* For extraction types `Extraction[T1]`, `Extraction[T2]`, ... the shape of the method must match this pattern:
|
||||
*
|
||||
* public static RouteResult methodName(RequestContext ctx, T1 t1, T2 t2, ...)
|
||||
*/
|
||||
@varargs
|
||||
def handleWith(instance: AnyRef, methodName: String, extractions: RequestVal[_]*): Route =
|
||||
handleWith(instance.getClass, instance, methodName, extractions: _*)
|
||||
|
||||
/**
|
||||
* Handles the route by reflectively calling the static method specified by `clazz`, and `methodName`.
|
||||
* Additionally, the value of all extractions will be passed to the function.
|
||||
*
|
||||
* For extraction types `Extraction[T1]`, `Extraction[T2]`, ... the shape of the method must match this pattern:
|
||||
*
|
||||
* public static RouteResult methodName(RequestContext ctx, T1 t1, T2 t2, ...)
|
||||
*/
|
||||
@varargs
|
||||
def handleWith(clazz: Class[_], methodName: String, extractions: RequestVal[_]*): Route =
|
||||
handleWith(clazz, null, methodName, extractions: _*)
|
||||
|
||||
/**
|
||||
* Handles the route by calling the method specified by `clazz`, `instance`, and `methodName`. Additionally, the value
|
||||
* of all extractions will be passed to the function.
|
||||
*
|
||||
* For extraction types `Extraction[T1]`, `Extraction[T2]`, ... the shape of the method must match this pattern:
|
||||
*
|
||||
* public static RouteResult methodName(RequestContext ctx, T1 t1, T2 t2, ...)
|
||||
*/
|
||||
@varargs
|
||||
def handleWith(clazz: Class[_], instance: AnyRef, methodName: String, extractions: RequestVal[_]*): Route = {
|
||||
def chooseOverload(methods: Seq[Method]): (RequestContext, Seq[Any]) ⇒ RouteResult = {
|
||||
val extractionTypes = extractions.map(_.resultClass).toList
|
||||
val RequestContextClass = classOf[RequestContext]
|
||||
|
||||
import java.{ lang ⇒ jl }
|
||||
def paramMatches(expected: Class[_], actual: Class[_]): Boolean = expected match {
|
||||
case e if e isAssignableFrom actual ⇒ true
|
||||
case jl.Long.TYPE if actual == classOf[jl.Long] ⇒ true
|
||||
case jl.Integer.TYPE if actual == classOf[jl.Integer] ⇒ true
|
||||
case jl.Short.TYPE if actual == classOf[jl.Short] ⇒ true
|
||||
case jl.Character.TYPE if actual == classOf[jl.Character] ⇒ true
|
||||
case jl.Byte.TYPE if actual == classOf[jl.Byte] ⇒ true
|
||||
case jl.Double.TYPE if actual == classOf[jl.Double] ⇒ true
|
||||
case jl.Float.TYPE if actual == classOf[jl.Float] ⇒ true
|
||||
case _ ⇒ false
|
||||
}
|
||||
def paramsMatch(params: Seq[Class[_]]): Boolean = {
|
||||
val res =
|
||||
params.size == extractionTypes.size &&
|
||||
(params, extractionTypes).zipped.forall(paramMatches)
|
||||
|
||||
res
|
||||
}
|
||||
def returnTypeMatches(method: Method): Boolean =
|
||||
method.getReturnType == classOf[RouteResult]
|
||||
|
||||
object ParameterTypes {
|
||||
def unapply(method: Method): Option[List[Class[_]]] = Some(method.getParameterTypes.toList)
|
||||
}
|
||||
|
||||
methods.filter(returnTypeMatches).collectFirst {
|
||||
case method @ ParameterTypes(RequestContextClass :: rest) if paramsMatch(rest) ⇒ {
|
||||
if (!method.isAccessible) method.setAccessible(true) // FIXME: test what happens if this fails
|
||||
(ctx: RequestContext, params: Seq[Any]) ⇒ method.invoke(instance, (ctx +: params).toArray.asInstanceOf[Array[AnyRef]]: _*).asInstanceOf[RouteResult]
|
||||
}
|
||||
|
||||
case method @ ParameterTypes(rest) if paramsMatch(rest) ⇒ {
|
||||
if (!method.isAccessible) method.setAccessible(true)
|
||||
(ctx: RequestContext, params: Seq[Any]) ⇒ method.invoke(instance, params.toArray.asInstanceOf[Array[AnyRef]]: _*).asInstanceOf[RouteResult]
|
||||
}
|
||||
}.getOrElse(throw new RuntimeException("No suitable method found"))
|
||||
}
|
||||
def lookupMethod() = {
|
||||
val candidateMethods = clazz.getMethods.filter(_.getName == methodName)
|
||||
chooseOverload(candidateMethods)
|
||||
}
|
||||
|
||||
val method = lookupMethod()
|
||||
|
||||
handle(extractions: _*)(ctx ⇒ method(ctx, extractions.map(_.get(ctx))))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.javadsl.model.DateTime
|
||||
import akka.http.javadsl.model.headers.EntityTag
|
||||
import akka.http.impl.server.RouteStructure
|
||||
|
||||
import scala.annotation.varargs
|
||||
|
||||
abstract class CacheConditionDirectives extends BasicDirectives {
|
||||
/**
|
||||
* Wraps its inner route with support for Conditional Requests as defined
|
||||
* by tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-26
|
||||
*
|
||||
* In particular the algorithm defined by tools.ietf.org/html/draft-ietf-httpbis-p4-conditional-26#section-6
|
||||
* is implemented by this directive.
|
||||
*/
|
||||
@varargs
|
||||
def conditional(entityTag: EntityTag, lastModified: DateTime, innerRoutes: Route*): Route =
|
||||
RouteStructure.Conditional(entityTag, lastModified, innerRoutes.toVector)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server.directives
|
||||
|
||||
import scala.annotation.varargs
|
||||
import akka.http.impl.server.RouteStructure
|
||||
import akka.http.javadsl.server.{ Coder, Directive, Directives, Route }
|
||||
|
||||
abstract class CodingDirectives extends CacheConditionDirectives {
|
||||
/**
|
||||
* Wraps the inner routes with encoding support. The response will be encoded
|
||||
* using one of the predefined coders, `Gzip`, `Deflate`, or `NoCoding` depending on
|
||||
* a potential [[akka.http.javadsl.model.headers.AcceptEncoding]] header from the client.
|
||||
*/
|
||||
@varargs def encodeResponse(innerRoutes: Route*): Route =
|
||||
// FIXME: make sure this list stays synchronized with the Scala one
|
||||
RouteStructure.EncodeResponse(List(Coder.NoCoding, Coder.Gzip, Coder.Deflate), innerRoutes.toVector)
|
||||
|
||||
/**
|
||||
* A directive that Wraps its inner routes with encoding support.
|
||||
* The response will be encoded using one of the given coders with the precedence given
|
||||
* by the order of the coders in this call.
|
||||
*
|
||||
* In any case, a potential [[akka.http.javadsl.model.headers.AcceptEncoding]] header from the client
|
||||
* will be respected (or otherwise, if no matching .
|
||||
*/
|
||||
@varargs def encodeResponse(coders: Coder*): Directive =
|
||||
Directives.custom(RouteStructure.EncodeResponse(coders.toList, _))
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server.directives
|
||||
|
||||
import java.io.File
|
||||
import akka.http.javadsl.model.{ MediaType, ContentType }
|
||||
import akka.http.javadsl.server.Route
|
||||
import akka.http.scaladsl.server
|
||||
import akka.http.impl.server.RouteStructure._
|
||||
|
||||
/**
|
||||
* Implement this interface to provide a custom mapping from a file name to a [[ContentType]].
|
||||
*/
|
||||
trait ContentTypeResolver {
|
||||
def resolve(fileName: String): ContentType
|
||||
}
|
||||
|
||||
/**
|
||||
* A resolver that assumes the given constant [[ContentType]] for all files.
|
||||
*/
|
||||
case class StaticContentTypeResolver(contentType: ContentType) extends ContentTypeResolver {
|
||||
def resolve(fileName: String): ContentType = contentType
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to customize one of the predefined routes of [[FileAndResourceRoute]] to respond
|
||||
* with a particular content type.
|
||||
*
|
||||
* The default behavior is to determine the content type by file extension.
|
||||
*/
|
||||
trait FileAndResourceRoute extends Route {
|
||||
/**
|
||||
* Returns a variant of this route that responds with the given constant [[ContentType]].
|
||||
*/
|
||||
def withContentType(contentType: ContentType): Route
|
||||
|
||||
/**
|
||||
* Returns a variant of this route that responds with the given constant [[MediaType]].
|
||||
*/
|
||||
def withContentType(mediaType: MediaType): Route
|
||||
|
||||
/**
|
||||
* Returns a variant of this route that uses the specified [[ContentTypeResolver]] to determine
|
||||
* which [[ContentType]] to respond with by file name.
|
||||
*/
|
||||
def resolveContentTypeWith(resolver: ContentTypeResolver): Route
|
||||
}
|
||||
|
||||
object FileAndResourceRoute {
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] def apply(f: ContentTypeResolver ⇒ Route): FileAndResourceRoute =
|
||||
new FileAndResourceRouteWithDefaultResolver(f) with FileAndResourceRoute {
|
||||
def withContentType(contentType: ContentType): Route = resolveContentTypeWith(StaticContentTypeResolver(contentType))
|
||||
def withContentType(mediaType: MediaType): Route = withContentType(mediaType.toContentType)
|
||||
|
||||
def resolveContentTypeWith(resolver: ContentTypeResolver): Route = f(resolver)
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] def forFixedName(fileName: String)(f: ContentType ⇒ Route): FileAndResourceRoute =
|
||||
new FileAndResourceRouteWithDefaultResolver(resolver ⇒ f(resolver.resolve(fileName))) with FileAndResourceRoute {
|
||||
def withContentType(contentType: ContentType): Route = resolveContentTypeWith(StaticContentTypeResolver(contentType))
|
||||
def withContentType(mediaType: MediaType): Route = withContentType(mediaType.toContentType)
|
||||
|
||||
def resolveContentTypeWith(resolver: ContentTypeResolver): Route = f(resolver.resolve(fileName))
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FileAndResourceDirectives extends CodingDirectives {
|
||||
/**
|
||||
* Completes GET requests with the content of the given resource loaded from the default ClassLoader.
|
||||
* If the resource cannot be found or read the Route rejects the request.
|
||||
*/
|
||||
def getFromResource(path: String): Route =
|
||||
getFromResource(path, defaultClassLoader)
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content of the given resource loaded from the given ClassLoader.
|
||||
* If the resource cannot be found or read the Route rejects the request.
|
||||
*/
|
||||
def getFromResource(path: String, classLoader: ClassLoader): Route =
|
||||
FileAndResourceRoute.forFixedName(path)(GetFromResource(path, _, classLoader))
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content from the resource identified by the given
|
||||
* directoryPath and the unmatched path.
|
||||
*/
|
||||
def getFromResourceDirectory(directoryPath: String): FileAndResourceRoute =
|
||||
getFromResourceDirectory(directoryPath, defaultClassLoader)
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content from the resource identified by the given
|
||||
* directoryPath and the unmatched path from the given ClassLoader.
|
||||
*/
|
||||
def getFromResourceDirectory(directoryPath: String, classLoader: ClassLoader): FileAndResourceRoute =
|
||||
FileAndResourceRoute(GetFromResourceDirectory(directoryPath, classLoader, _))
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content of the given file.
|
||||
*/
|
||||
def getFromFile(file: File): FileAndResourceRoute = FileAndResourceRoute.forFixedName(file.getPath)(GetFromFile(file, _))
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content of the file at the path.
|
||||
*/
|
||||
def getFromFile(path: String): FileAndResourceRoute = getFromFile(new File(path))
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content from the file identified by the given
|
||||
* directory and the unmatched path of the request.
|
||||
*/
|
||||
def getFromDirectory(directory: File): FileAndResourceRoute = FileAndResourceRoute(GetFromDirectory(directory, browseable = false, _))
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content from the file identified by the given
|
||||
* directoryPath and the unmatched path of the request.
|
||||
*/
|
||||
def getFromDirectory(directoryPath: String): FileAndResourceRoute = getFromDirectory(new File(directoryPath))
|
||||
|
||||
/**
|
||||
* Same as [[getFromDirectory]] but generates a listing of files if the path is a directory.
|
||||
*/
|
||||
def getFromBrowseableDirectory(directory: File): FileAndResourceRoute = FileAndResourceRoute(GetFromDirectory(directory, browseable = true, _))
|
||||
|
||||
/**
|
||||
* Same as [[getFromDirectory]] but generates a listing of files if the path is a directory.
|
||||
*/
|
||||
def getFromBrowseableDirectory(directoryPath: String): FileAndResourceRoute = FileAndResourceRoute(GetFromDirectory(new File(directoryPath), browseable = true, _))
|
||||
|
||||
protected def defaultClassLoader: ClassLoader = server.directives.FileAndResourceDirectives.defaultClassLoader
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server.directives
|
||||
|
||||
import akka.http.javadsl.model.{ HttpMethods, HttpMethod }
|
||||
import akka.http.javadsl.server.Route
|
||||
import akka.http.impl.server.RouteStructure
|
||||
|
||||
import scala.annotation.varargs
|
||||
|
||||
abstract class MethodDirectives extends FileAndResourceDirectives {
|
||||
/** Handles the inner routes if the incoming request is a GET request, rejects the request otherwise */
|
||||
@varargs
|
||||
def get(innerRoutes: Route*): Route = method(HttpMethods.GET, innerRoutes: _*)
|
||||
|
||||
/** Handles the inner routes if the incoming request is a POST request, rejects the request otherwise */
|
||||
@varargs
|
||||
def post(innerRoutes: Route*): Route = method(HttpMethods.POST, innerRoutes: _*)
|
||||
|
||||
/** Handles the inner routes if the incoming request is a PUT request, rejects the request otherwise */
|
||||
@varargs
|
||||
def put(innerRoutes: Route*): Route = method(HttpMethods.PUT, innerRoutes: _*)
|
||||
|
||||
/** Handles the inner routes if the incoming request is a DELETE request, rejects the request otherwise */
|
||||
@varargs
|
||||
def delete(innerRoutes: Route*): Route = method(HttpMethods.DELETE, innerRoutes: _*)
|
||||
|
||||
/** Handles the inner routes if the incoming request is a HEAD request, rejects the request otherwise */
|
||||
@varargs
|
||||
def head(innerRoutes: Route*): Route = method(HttpMethods.HEAD, innerRoutes: _*)
|
||||
|
||||
/** Handles the inner routes if the incoming request is a OPTIONS request, rejects the request otherwise */
|
||||
@varargs
|
||||
def options(innerRoutes: Route*): Route = method(HttpMethods.OPTIONS, innerRoutes: _*)
|
||||
|
||||
/** Handles the inner routes if the incoming request is a PATCH request, rejects the request otherwise */
|
||||
@varargs
|
||||
def patch(innerRoutes: Route*): Route = method(HttpMethods.PATCH, innerRoutes: _*)
|
||||
|
||||
/** Handles the inner routes if the incoming request is a request with the given method, rejects the request otherwise */
|
||||
@varargs
|
||||
def method(method: HttpMethod, innerRoutes: Route*): Route = RouteStructure.MethodFilter(method, innerRoutes.toVector)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.impl.server.RouteStructure
|
||||
|
||||
import scala.annotation.varargs
|
||||
import scala.collection.immutable
|
||||
|
||||
abstract class PathDirectives extends MethodDirectives {
|
||||
/**
|
||||
* Tries to consumes the complete unmatched path given a number of PathMatchers. Between each
|
||||
* of the matchers a `/` will be matched automatically.
|
||||
*
|
||||
* A matcher can either be a matcher of type `PathMatcher`, or a literal string.
|
||||
*/
|
||||
@varargs
|
||||
def path(matchers: AnyRef*): Directive =
|
||||
forMatchers(joinWithSlash(convertMatchers(matchers)) :+ PathMatchers.END)
|
||||
|
||||
@varargs
|
||||
def pathPrefix(matchers: AnyRef*): Directive =
|
||||
forMatchers(joinWithSlash(convertMatchers(matchers)))
|
||||
|
||||
def pathSingleSlash: Directive = forMatchers(List(PathMatchers.SLASH, PathMatchers.END))
|
||||
|
||||
@varargs
|
||||
def rawPathPrefix(matchers: AnyRef*): Directive =
|
||||
forMatchers(convertMatchers(matchers))
|
||||
|
||||
private def forMatchers(matchers: immutable.Seq[PathMatcher[_]]): Directive =
|
||||
Directives.custom(RouteStructure.RawPathPrefix(matchers, _))
|
||||
|
||||
private def joinWithSlash(matchers: immutable.Seq[PathMatcher[_]]): immutable.Seq[PathMatcher[_]] = {
|
||||
def join(result: immutable.Seq[PathMatcher[_]], next: PathMatcher[_]): immutable.Seq[PathMatcher[_]] =
|
||||
result :+ PathMatchers.SLASH :+ next
|
||||
|
||||
matchers.foldLeft(immutable.Seq.empty[PathMatcher[_]])(join)
|
||||
}
|
||||
|
||||
private def convertMatchers(matchers: Seq[AnyRef]): immutable.Seq[PathMatcher[_]] = {
|
||||
def parse(matcher: AnyRef): PathMatcher[_] = matcher match {
|
||||
case p: PathMatcher[_] ⇒ p
|
||||
case name: String ⇒ PathMatchers.segment(name)
|
||||
}
|
||||
|
||||
matchers.map(parse).toVector
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.client
|
||||
|
||||
import scala.collection.immutable
|
||||
import scala.concurrent.{ Await, ExecutionContext }
|
||||
import scala.concurrent.duration._
|
||||
import scala.reflect.ClassTag
|
||||
import akka.util.Timeout
|
||||
import akka.event.{ Logging, LoggingAdapter }
|
||||
import akka.http.scaladsl.marshalling._
|
||||
import akka.http.scaladsl.model._
|
||||
import headers.HttpCredentials
|
||||
import HttpMethods._
|
||||
|
||||
trait RequestBuilding extends TransformerPipelineSupport {
|
||||
type RequestTransformer = HttpRequest ⇒ HttpRequest
|
||||
|
||||
class RequestBuilder(val method: HttpMethod) {
|
||||
def apply(): HttpRequest =
|
||||
apply("/")
|
||||
|
||||
def apply(uri: String): HttpRequest =
|
||||
apply(uri, HttpEntity.Empty)
|
||||
|
||||
def apply[T](uri: String, content: T)(implicit m: ToEntityMarshaller[T], ec: ExecutionContext): HttpRequest =
|
||||
apply(uri, Some(content))
|
||||
|
||||
def apply[T](uri: String, content: Option[T])(implicit m: ToEntityMarshaller[T], ec: ExecutionContext): HttpRequest =
|
||||
apply(Uri(uri), content)
|
||||
|
||||
def apply(uri: String, entity: RequestEntity): HttpRequest =
|
||||
apply(Uri(uri), entity)
|
||||
|
||||
def apply(uri: Uri): HttpRequest =
|
||||
apply(uri, HttpEntity.Empty)
|
||||
|
||||
def apply[T](uri: Uri, content: T)(implicit m: ToEntityMarshaller[T], ec: ExecutionContext): HttpRequest =
|
||||
apply(uri, Some(content))
|
||||
|
||||
def apply[T](uri: Uri, content: Option[T])(implicit m: ToEntityMarshaller[T], timeout: Timeout = Timeout(1.second), ec: ExecutionContext): HttpRequest =
|
||||
content match {
|
||||
case None ⇒ apply(uri, HttpEntity.Empty)
|
||||
case Some(value) ⇒
|
||||
val entity = Await.result(Marshal(value).to[RequestEntity], timeout.duration)
|
||||
apply(uri, entity)
|
||||
}
|
||||
|
||||
def apply(uri: Uri, entity: RequestEntity): HttpRequest =
|
||||
HttpRequest(method, uri, Nil, entity)
|
||||
}
|
||||
|
||||
val Get = new RequestBuilder(GET)
|
||||
val Post = new RequestBuilder(POST)
|
||||
val Put = new RequestBuilder(PUT)
|
||||
val Patch = new RequestBuilder(PATCH)
|
||||
val Delete = new RequestBuilder(DELETE)
|
||||
val Options = new RequestBuilder(OPTIONS)
|
||||
val Head = new RequestBuilder(HEAD)
|
||||
|
||||
// TODO: reactivate after HTTP message encoding has been ported
|
||||
//def encode(encoder: Encoder): RequestTransformer = encoder.encode(_, flow)
|
||||
|
||||
def addHeader(header: HttpHeader): RequestTransformer = _.mapHeaders(header +: _)
|
||||
|
||||
def addHeader(headerName: String, headerValue: String): RequestTransformer =
|
||||
HttpHeader.parse(headerName, headerValue) match {
|
||||
case HttpHeader.ParsingResult.Ok(h, Nil) ⇒ addHeader(h)
|
||||
case result ⇒ throw new IllegalArgumentException(result.errors.head.formatPretty)
|
||||
}
|
||||
|
||||
def addHeaders(first: HttpHeader, more: HttpHeader*): RequestTransformer = _.mapHeaders(_ ++ (first +: more))
|
||||
|
||||
def mapHeaders(f: immutable.Seq[HttpHeader] ⇒ immutable.Seq[HttpHeader]): RequestTransformer = _.mapHeaders(f)
|
||||
|
||||
def removeHeader(headerName: String): RequestTransformer =
|
||||
_ mapHeaders (_ filterNot (_.name equalsIgnoreCase headerName))
|
||||
|
||||
def removeHeader[T <: HttpHeader: ClassTag]: RequestTransformer =
|
||||
removeHeader(implicitly[ClassTag[T]].runtimeClass)
|
||||
|
||||
def removeHeader(clazz: Class[_]): RequestTransformer =
|
||||
_ mapHeaders (_ filterNot clazz.isInstance)
|
||||
|
||||
def removeHeaders(names: String*): RequestTransformer =
|
||||
_ mapHeaders (_ filterNot (header ⇒ names exists (_ equalsIgnoreCase header.name)))
|
||||
|
||||
def addCredentials(credentials: HttpCredentials) = addHeader(headers.Authorization(credentials))
|
||||
|
||||
def logRequest(log: LoggingAdapter, level: Logging.LogLevel = Logging.DebugLevel) = logValue[HttpRequest](log, level)
|
||||
|
||||
def logRequest(logFun: HttpRequest ⇒ Unit) = logValue[HttpRequest](logFun)
|
||||
|
||||
implicit def header2AddHeader(header: HttpHeader): RequestTransformer = addHeader(header)
|
||||
}
|
||||
|
||||
object RequestBuilding extends RequestBuilding
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.client
|
||||
|
||||
import scala.concurrent.{ Future, ExecutionContext }
|
||||
import akka.event.{ Logging, LoggingAdapter }
|
||||
|
||||
trait TransformerPipelineSupport {
|
||||
|
||||
def logValue[T](log: LoggingAdapter, level: Logging.LogLevel = Logging.DebugLevel): T ⇒ T =
|
||||
logValue { value ⇒ log.log(level, value.toString) }
|
||||
|
||||
def logValue[T](logFun: T ⇒ Unit): T ⇒ T = { response ⇒
|
||||
logFun(response)
|
||||
response
|
||||
}
|
||||
|
||||
implicit class WithTransformation[A](value: A) {
|
||||
def ~>[B](f: A ⇒ B): B = f(value)
|
||||
}
|
||||
|
||||
implicit class WithTransformerConcatenation[A, B](f: A ⇒ B) extends (A ⇒ B) {
|
||||
def apply(input: A) = f(input)
|
||||
def ~>[AA, BB, R](g: AA ⇒ BB)(implicit aux: TransformerAux[A, B, AA, BB, R]) =
|
||||
new WithTransformerConcatenation[A, R](aux(f, g))
|
||||
}
|
||||
}
|
||||
|
||||
object TransformerPipelineSupport extends TransformerPipelineSupport
|
||||
|
||||
trait TransformerAux[A, B, AA, BB, R] {
|
||||
def apply(f: A ⇒ B, g: AA ⇒ BB): A ⇒ R
|
||||
}
|
||||
|
||||
object TransformerAux {
|
||||
implicit def aux1[A, B, C] = new TransformerAux[A, B, B, C, C] {
|
||||
def apply(f: A ⇒ B, g: B ⇒ C): A ⇒ C = f andThen g
|
||||
}
|
||||
implicit def aux2[A, B, C](implicit ec: ExecutionContext) =
|
||||
new TransformerAux[A, Future[B], B, C, Future[C]] {
|
||||
def apply(f: A ⇒ Future[B], g: B ⇒ C): A ⇒ Future[C] = f(_).map(g)
|
||||
}
|
||||
implicit def aux3[A, B, C](implicit ec: ExecutionContext) =
|
||||
new TransformerAux[A, Future[B], B, Future[C], Future[C]] {
|
||||
def apply(f: A ⇒ Future[B], g: B ⇒ Future[C]): A ⇒ Future[C] = f(_).flatMap(g)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
/** Marker trait for A combined Encoder and Decoder */
|
||||
trait Coder extends Encoder with Decoder
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import akka.http.scaladsl.model.{ HttpRequest, HttpResponse, ResponseEntity, RequestEntity }
|
||||
import akka.util.ByteString
|
||||
import akka.stream.scaladsl.Flow
|
||||
|
||||
/** An abstraction to transform data bytes of HttpMessages or HttpEntities */
|
||||
sealed trait DataMapper[T] {
|
||||
def transformDataBytes(t: T, transformer: Flow[ByteString, ByteString, _]): T
|
||||
}
|
||||
object DataMapper {
|
||||
implicit val mapRequestEntity: DataMapper[RequestEntity] =
|
||||
new DataMapper[RequestEntity] {
|
||||
def transformDataBytes(t: RequestEntity, transformer: Flow[ByteString, ByteString, _]): RequestEntity =
|
||||
t.transformDataBytes(transformer)
|
||||
}
|
||||
implicit val mapResponseEntity: DataMapper[ResponseEntity] =
|
||||
new DataMapper[ResponseEntity] {
|
||||
def transformDataBytes(t: ResponseEntity, transformer: Flow[ByteString, ByteString, _]): ResponseEntity =
|
||||
t.transformDataBytes(transformer)
|
||||
}
|
||||
|
||||
implicit val mapRequest: DataMapper[HttpRequest] = mapMessage(mapRequestEntity)((m, f) ⇒ m.withEntity(f(m.entity)))
|
||||
implicit val mapResponse: DataMapper[HttpResponse] = mapMessage(mapResponseEntity)((m, f) ⇒ m.withEntity(f(m.entity)))
|
||||
|
||||
def mapMessage[T, E](entityMapper: DataMapper[E])(mapEntity: (T, E ⇒ E) ⇒ T): DataMapper[T] =
|
||||
new DataMapper[T] {
|
||||
def transformDataBytes(t: T, transformer: Flow[ByteString, ByteString, _]): T =
|
||||
mapEntity(t, entityMapper.transformDataBytes(_, transformer))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.FlowMaterializer
|
||||
import akka.stream.stage.Stage
|
||||
import akka.util.ByteString
|
||||
import headers.HttpEncoding
|
||||
import akka.stream.scaladsl.{ Sink, Source, Flow }
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
trait Decoder {
|
||||
def encoding: HttpEncoding
|
||||
|
||||
def decode[T <: HttpMessage](message: T)(implicit mapper: DataMapper[T]): T#Self =
|
||||
if (message.headers exists Encoder.isContentEncodingHeader)
|
||||
decodeData(message).withHeaders(message.headers filterNot Encoder.isContentEncodingHeader)
|
||||
else message.self
|
||||
|
||||
def decodeData[T](t: T)(implicit mapper: DataMapper[T]): T = mapper.transformDataBytes(t, decoderFlow)
|
||||
|
||||
def maxBytesPerChunk: Int
|
||||
def withMaxBytesPerChunk(maxBytesPerChunk: Int): Decoder
|
||||
|
||||
def decoderFlow: Flow[ByteString, ByteString, Unit]
|
||||
def decode(input: ByteString)(implicit mat: FlowMaterializer): Future[ByteString] =
|
||||
Source.single(input).via(decoderFlow).runWith(Sink.head)
|
||||
}
|
||||
object Decoder {
|
||||
val MaxBytesPerChunkDefault: Int = 65536
|
||||
}
|
||||
|
||||
/** A decoder that is implemented in terms of a [[Stage]] */
|
||||
trait StreamDecoder extends Decoder { outer ⇒
|
||||
protected def newDecompressorStage(maxBytesPerChunk: Int): () ⇒ Stage[ByteString, ByteString]
|
||||
|
||||
def maxBytesPerChunk: Int = Decoder.MaxBytesPerChunkDefault
|
||||
def withMaxBytesPerChunk(newMaxBytesPerChunk: Int): Decoder =
|
||||
new StreamDecoder {
|
||||
def encoding: HttpEncoding = outer.encoding
|
||||
override def maxBytesPerChunk: Int = newMaxBytesPerChunk
|
||||
|
||||
def newDecompressorStage(maxBytesPerChunk: Int): () ⇒ Stage[ByteString, ByteString] =
|
||||
outer.newDecompressorStage(maxBytesPerChunk)
|
||||
}
|
||||
|
||||
def decoderFlow: Flow[ByteString, ByteString, Unit] =
|
||||
Flow[ByteString].transform(newDecompressorStage(maxBytesPerChunk))
|
||||
|
||||
}
|
||||
138
akka-http/src/main/scala/akka/http/scaladsl/coding/Deflate.scala
Normal file
138
akka-http/src/main/scala/akka/http/scaladsl/coding/Deflate.scala
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import java.util.zip.{ Inflater, Deflater }
|
||||
import akka.stream.stage._
|
||||
import akka.util.{ ByteStringBuilder, ByteString }
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import akka.http.impl.util._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers.HttpEncodings
|
||||
|
||||
class Deflate(val messageFilter: HttpMessage ⇒ Boolean) extends Coder with StreamDecoder {
|
||||
val encoding = HttpEncodings.deflate
|
||||
def newCompressor = new DeflateCompressor
|
||||
def newDecompressorStage(maxBytesPerChunk: Int) = () ⇒ new DeflateDecompressor(maxBytesPerChunk)
|
||||
}
|
||||
object Deflate extends Deflate(Encoder.DefaultFilter)
|
||||
|
||||
class DeflateCompressor extends Compressor {
|
||||
protected lazy val deflater = new Deflater(Deflater.BEST_COMPRESSION, false)
|
||||
|
||||
override final def compressAndFlush(input: ByteString): ByteString = {
|
||||
val buffer = newTempBuffer(input.size)
|
||||
|
||||
compressWithBuffer(input, buffer) ++ flushWithBuffer(buffer)
|
||||
}
|
||||
override final def compressAndFinish(input: ByteString): ByteString = {
|
||||
val buffer = newTempBuffer(input.size)
|
||||
|
||||
compressWithBuffer(input, buffer) ++ finishWithBuffer(buffer)
|
||||
}
|
||||
override final def compress(input: ByteString): ByteString = compressWithBuffer(input, newTempBuffer())
|
||||
override final def flush(): ByteString = flushWithBuffer(newTempBuffer())
|
||||
override final def finish(): ByteString = finishWithBuffer(newTempBuffer())
|
||||
|
||||
protected def compressWithBuffer(input: ByteString, buffer: Array[Byte]): ByteString = {
|
||||
assert(deflater.needsInput())
|
||||
deflater.setInput(input.toArray)
|
||||
drain(buffer)
|
||||
}
|
||||
protected def flushWithBuffer(buffer: Array[Byte]): ByteString = {
|
||||
// trick the deflater into flushing: switch compression level
|
||||
// FIXME: use proper APIs and SYNC_FLUSH when Java 6 support is dropped
|
||||
deflater.deflate(EmptyByteArray, 0, 0)
|
||||
deflater.setLevel(Deflater.NO_COMPRESSION)
|
||||
val res1 = drain(buffer)
|
||||
deflater.setLevel(Deflater.BEST_COMPRESSION)
|
||||
val res2 = drain(buffer)
|
||||
res1 ++ res2
|
||||
}
|
||||
protected def finishWithBuffer(buffer: Array[Byte]): ByteString = {
|
||||
deflater.finish()
|
||||
val res = drain(buffer)
|
||||
deflater.end()
|
||||
res
|
||||
}
|
||||
|
||||
@tailrec
|
||||
protected final def drain(buffer: Array[Byte], result: ByteStringBuilder = new ByteStringBuilder()): ByteString = {
|
||||
val len = deflater.deflate(buffer)
|
||||
if (len > 0) {
|
||||
result ++= ByteString.fromArray(buffer, 0, len)
|
||||
drain(buffer, result)
|
||||
} else {
|
||||
assert(deflater.needsInput())
|
||||
result.result()
|
||||
}
|
||||
}
|
||||
|
||||
private def newTempBuffer(size: Int = 65536): Array[Byte] =
|
||||
// The default size is somewhat arbitrary, we'd like to guess a better value but Deflater/zlib
|
||||
// is buffering in an unpredictable manner.
|
||||
// `compress` will only return any data if the buffered compressed data has some size in
|
||||
// the region of 10000-50000 bytes.
|
||||
// `flush` and `finish` will return any size depending on the previous input.
|
||||
// This value will hopefully provide a good compromise between memory churn and
|
||||
// excessive fragmentation of ByteStrings.
|
||||
new Array[Byte](size)
|
||||
}
|
||||
|
||||
class DeflateDecompressor(maxBytesPerChunk: Int = Decoder.MaxBytesPerChunkDefault) extends DeflateDecompressorBase(maxBytesPerChunk) {
|
||||
protected def createInflater() = new Inflater()
|
||||
|
||||
def initial: State = StartInflate
|
||||
def afterInflate: State = StartInflate
|
||||
|
||||
protected def afterBytesRead(buffer: Array[Byte], offset: Int, length: Int): Unit = {}
|
||||
protected def onTruncation(ctx: Context[ByteString]): SyncDirective = ctx.finish()
|
||||
}
|
||||
|
||||
abstract class DeflateDecompressorBase(maxBytesPerChunk: Int = Decoder.MaxBytesPerChunkDefault) extends ByteStringParserStage[ByteString] {
|
||||
protected def createInflater(): Inflater
|
||||
val inflater = createInflater()
|
||||
|
||||
protected def afterInflate: State
|
||||
protected def afterBytesRead(buffer: Array[Byte], offset: Int, length: Int): Unit
|
||||
|
||||
/** Start inflating */
|
||||
case object StartInflate extends IntermediateState {
|
||||
def onPush(data: ByteString, ctx: Context[ByteString]): SyncDirective = {
|
||||
require(inflater.needsInput())
|
||||
inflater.setInput(data.toArray)
|
||||
|
||||
becomeWithRemaining(Inflate()(data), ByteString.empty, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/** Inflate */
|
||||
case class Inflate()(data: ByteString) extends IntermediateState {
|
||||
override def onPull(ctx: Context[ByteString]): SyncDirective = {
|
||||
val buffer = new Array[Byte](maxBytesPerChunk)
|
||||
val read = inflater.inflate(buffer)
|
||||
if (read > 0) {
|
||||
afterBytesRead(buffer, 0, read)
|
||||
ctx.push(ByteString.fromArray(buffer, 0, read))
|
||||
} else {
|
||||
val remaining = data.takeRight(inflater.getRemaining)
|
||||
val next =
|
||||
if (inflater.finished()) afterInflate
|
||||
else StartInflate
|
||||
|
||||
becomeWithRemaining(next, remaining, ctx)
|
||||
}
|
||||
}
|
||||
def onPush(elem: ByteString, ctx: Context[ByteString]): SyncDirective =
|
||||
throw new IllegalStateException("Don't expect a new Element")
|
||||
}
|
||||
|
||||
def becomeWithRemaining(next: State, remaining: ByteString, ctx: Context[ByteString]) = {
|
||||
become(next)
|
||||
if (remaining.isEmpty) current.onPull(ctx)
|
||||
else current.onPush(remaining, ctx)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.impl.util.StreamUtils
|
||||
import akka.stream.stage.Stage
|
||||
import akka.util.ByteString
|
||||
import headers._
|
||||
import akka.stream.scaladsl.Flow
|
||||
|
||||
trait Encoder {
|
||||
def encoding: HttpEncoding
|
||||
|
||||
def messageFilter: HttpMessage ⇒ Boolean
|
||||
|
||||
def encode[T <: HttpMessage](message: T)(implicit mapper: DataMapper[T]): T#Self =
|
||||
if (messageFilter(message) && !message.headers.exists(Encoder.isContentEncodingHeader))
|
||||
encodeData(message).withHeaders(`Content-Encoding`(encoding) +: message.headers)
|
||||
else message.self
|
||||
|
||||
def encodeData[T](t: T)(implicit mapper: DataMapper[T]): T =
|
||||
mapper.transformDataBytes(t, Flow[ByteString].transform(newEncodeTransformer))
|
||||
|
||||
def encode(input: ByteString): ByteString = newCompressor.compressAndFinish(input)
|
||||
|
||||
def newCompressor: Compressor
|
||||
|
||||
def newEncodeTransformer(): Stage[ByteString, ByteString] = {
|
||||
val compressor = newCompressor
|
||||
|
||||
def encodeChunk(bytes: ByteString): ByteString = compressor.compressAndFlush(bytes)
|
||||
def finish(): ByteString = compressor.finish()
|
||||
|
||||
StreamUtils.byteStringTransformer(encodeChunk, finish)
|
||||
}
|
||||
}
|
||||
|
||||
object Encoder {
|
||||
val DefaultFilter: HttpMessage ⇒ Boolean = {
|
||||
case req: HttpRequest ⇒ isCompressible(req)
|
||||
case res @ HttpResponse(status, _, _, _) ⇒ isCompressible(res) && status.isSuccess
|
||||
}
|
||||
private[coding] def isCompressible(msg: HttpMessage): Boolean =
|
||||
msg.entity.contentType.mediaType.compressible
|
||||
|
||||
private[coding] val isContentEncodingHeader: HttpHeader ⇒ Boolean = _.isInstanceOf[`Content-Encoding`]
|
||||
}
|
||||
|
||||
/** A stateful object representing ongoing compression. */
|
||||
abstract class Compressor {
|
||||
/**
|
||||
* Compresses the given input and returns compressed data. The implementation
|
||||
* can and will choose to buffer output data to improve compression. Use
|
||||
* `flush` or `compressAndFlush` to make sure that all input data has been
|
||||
* compressed and pending output data has been returned.
|
||||
*/
|
||||
def compress(input: ByteString): ByteString
|
||||
|
||||
/**
|
||||
* Flushes any output data and returns the currently remaining compressed data.
|
||||
*/
|
||||
def flush(): ByteString
|
||||
|
||||
/**
|
||||
* Closes this compressed stream and return the remaining compressed data. After
|
||||
* calling this method, this Compressor cannot be used any further.
|
||||
*/
|
||||
def finish(): ByteString
|
||||
|
||||
/** Combines `compress` + `flush` */
|
||||
def compressAndFlush(input: ByteString): ByteString
|
||||
/** Combines `compress` + `finish` */
|
||||
def compressAndFinish(input: ByteString): ByteString
|
||||
}
|
||||
145
akka-http/src/main/scala/akka/http/scaladsl/coding/Gzip.scala
Normal file
145
akka-http/src/main/scala/akka/http/scaladsl/coding/Gzip.scala
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import akka.util.ByteString
|
||||
import akka.stream.stage._
|
||||
|
||||
import akka.http.impl.util.ByteReader
|
||||
import java.util.zip.{ Inflater, CRC32, ZipException, Deflater }
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import headers.HttpEncodings
|
||||
|
||||
class Gzip(val messageFilter: HttpMessage ⇒ Boolean) extends Coder with StreamDecoder {
|
||||
val encoding = HttpEncodings.gzip
|
||||
def newCompressor = new GzipCompressor
|
||||
def newDecompressorStage(maxBytesPerChunk: Int) = () ⇒ new GzipDecompressor(maxBytesPerChunk)
|
||||
}
|
||||
|
||||
/**
|
||||
* An encoder and decoder for the HTTP 'gzip' encoding.
|
||||
*/
|
||||
object Gzip extends Gzip(Encoder.DefaultFilter) {
|
||||
def apply(messageFilter: HttpMessage ⇒ Boolean) = new Gzip(messageFilter)
|
||||
}
|
||||
|
||||
class GzipCompressor extends DeflateCompressor {
|
||||
override protected lazy val deflater = new Deflater(Deflater.BEST_COMPRESSION, true)
|
||||
private val checkSum = new CRC32 // CRC32 of uncompressed data
|
||||
private var headerSent = false
|
||||
private var bytesRead = 0L
|
||||
|
||||
override protected def compressWithBuffer(input: ByteString, buffer: Array[Byte]): ByteString = {
|
||||
updateCrc(input)
|
||||
header() ++ super.compressWithBuffer(input, buffer)
|
||||
}
|
||||
override protected def flushWithBuffer(buffer: Array[Byte]): ByteString = header() ++ super.flushWithBuffer(buffer)
|
||||
override protected def finishWithBuffer(buffer: Array[Byte]): ByteString = super.finishWithBuffer(buffer) ++ trailer()
|
||||
|
||||
private def updateCrc(input: ByteString): Unit = {
|
||||
checkSum.update(input.toArray)
|
||||
bytesRead += input.length
|
||||
}
|
||||
private def header(): ByteString =
|
||||
if (!headerSent) {
|
||||
headerSent = true
|
||||
GzipDecompressor.Header
|
||||
} else ByteString.empty
|
||||
|
||||
private def trailer(): ByteString = {
|
||||
def int32(i: Int): ByteString = ByteString(i, i >> 8, i >> 16, i >> 24)
|
||||
val crc = checkSum.getValue.toInt
|
||||
val tot = bytesRead.toInt // truncated to 32bit as specified in https://tools.ietf.org/html/rfc1952#section-2
|
||||
val trailer = int32(crc) ++ int32(tot)
|
||||
|
||||
trailer
|
||||
}
|
||||
}
|
||||
|
||||
class GzipDecompressor(maxBytesPerChunk: Int = Decoder.MaxBytesPerChunkDefault) extends DeflateDecompressorBase(maxBytesPerChunk) {
|
||||
protected def createInflater(): Inflater = new Inflater(true)
|
||||
|
||||
def initial: State = Initial
|
||||
|
||||
/** No bytes were received yet */
|
||||
case object Initial extends State {
|
||||
def onPush(data: ByteString, ctx: Context[ByteString]): SyncDirective =
|
||||
if (data.isEmpty) ctx.pull()
|
||||
else becomeWithRemaining(ReadHeaders, data, ctx)
|
||||
|
||||
override def onPull(ctx: Context[ByteString]): SyncDirective =
|
||||
if (ctx.isFinishing) {
|
||||
ctx.finish()
|
||||
} else super.onPull(ctx)
|
||||
}
|
||||
|
||||
var crc32: CRC32 = new CRC32
|
||||
protected def afterInflate: State = ReadTrailer
|
||||
|
||||
/** Reading the header bytes */
|
||||
case object ReadHeaders extends ByteReadingState {
|
||||
def read(reader: ByteReader, ctx: Context[ByteString]): SyncDirective = {
|
||||
import reader._
|
||||
|
||||
if (readByte() != 0x1F || readByte() != 0x8B) fail("Not in GZIP format") // check magic header
|
||||
if (readByte() != 8) fail("Unsupported GZIP compression method") // check compression method
|
||||
val flags = readByte()
|
||||
skip(6) // skip MTIME, XFL and OS fields
|
||||
if ((flags & 4) > 0) skip(readShortLE()) // skip optional extra fields
|
||||
if ((flags & 8) > 0) skipZeroTerminatedString() // skip optional file name
|
||||
if ((flags & 16) > 0) skipZeroTerminatedString() // skip optional file comment
|
||||
if ((flags & 2) > 0 && crc16(fromStartToHere) != readShortLE()) fail("Corrupt GZIP header")
|
||||
|
||||
inflater.reset()
|
||||
crc32.reset()
|
||||
becomeWithRemaining(StartInflate, remainingData, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
protected def afterBytesRead(buffer: Array[Byte], offset: Int, length: Int): Unit =
|
||||
crc32.update(buffer, offset, length)
|
||||
|
||||
/** Reading the trailer */
|
||||
case object ReadTrailer extends ByteReadingState {
|
||||
def read(reader: ByteReader, ctx: Context[ByteString]): SyncDirective = {
|
||||
import reader._
|
||||
|
||||
if (readIntLE() != crc32.getValue.toInt) fail("Corrupt data (CRC32 checksum error)")
|
||||
if (readIntLE() != inflater.getBytesWritten.toInt /* truncated to 32bit */ ) fail("Corrupt GZIP trailer ISIZE")
|
||||
|
||||
becomeWithRemaining(Initial, remainingData, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
override def onUpstreamFinish(ctx: Context[ByteString]): TerminationDirective = ctx.absorbTermination()
|
||||
|
||||
private def crc16(data: ByteString) = {
|
||||
val crc = new CRC32
|
||||
crc.update(data.toArray)
|
||||
crc.getValue.toInt & 0xFFFF
|
||||
}
|
||||
|
||||
override protected def onTruncation(ctx: Context[ByteString]): SyncDirective = ctx.fail(new ZipException("Truncated GZIP stream"))
|
||||
|
||||
private def fail(msg: String) = throw new ZipException(msg)
|
||||
}
|
||||
|
||||
/** INTERNAL API */
|
||||
private[http] object GzipDecompressor {
|
||||
// RFC 1952: http://tools.ietf.org/html/rfc1952 section 2.2
|
||||
val Header = ByteString(
|
||||
0x1F, // ID1
|
||||
0x8B, // ID2
|
||||
8, // CM = Deflate
|
||||
0, // FLG
|
||||
0, // MTIME 1
|
||||
0, // MTIME 2
|
||||
0, // MTIME 3
|
||||
0, // MTIME 4
|
||||
0, // XFL
|
||||
0 // OS
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.impl.util.StreamUtils
|
||||
import akka.stream.stage.Stage
|
||||
import akka.util.ByteString
|
||||
import headers.HttpEncodings
|
||||
|
||||
/**
|
||||
* An encoder and decoder for the HTTP 'identity' encoding.
|
||||
*/
|
||||
object NoCoding extends Coder with StreamDecoder {
|
||||
val encoding = HttpEncodings.identity
|
||||
|
||||
override def encode[T <: HttpMessage](message: T)(implicit mapper: DataMapper[T]): T#Self = message.self
|
||||
override def encodeData[T](t: T)(implicit mapper: DataMapper[T]): T = t
|
||||
override def decode[T <: HttpMessage](message: T)(implicit mapper: DataMapper[T]): T#Self = message.self
|
||||
override def decodeData[T](t: T)(implicit mapper: DataMapper[T]): T = t
|
||||
|
||||
val messageFilter: HttpMessage ⇒ Boolean = _ ⇒ false
|
||||
|
||||
def newCompressor = NoCodingCompressor
|
||||
|
||||
def newDecompressorStage(maxBytesPerChunk: Int): () ⇒ Stage[ByteString, ByteString] =
|
||||
() ⇒ StreamUtils.limitByteChunksStage(maxBytesPerChunk)
|
||||
}
|
||||
|
||||
object NoCodingCompressor extends Compressor {
|
||||
def compress(input: ByteString): ByteString = input
|
||||
def flush() = ByteString.empty
|
||||
def finish() = ByteString.empty
|
||||
|
||||
def compressAndFlush(input: ByteString): ByteString = input
|
||||
def compressAndFinish(input: ByteString): ByteString = input
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.common
|
||||
|
||||
import akka.http.scaladsl.unmarshalling.{ FromStringUnmarshaller ⇒ FSU }
|
||||
|
||||
private[http] trait ToNameReceptacleEnhancements {
|
||||
implicit def symbol2NR(symbol: Symbol) = new NameReceptacle[String](symbol.name)
|
||||
implicit def string2NR(string: String) = new NameReceptacle[String](string)
|
||||
}
|
||||
object ToNameReceptacleEnhancements extends ToNameReceptacleEnhancements
|
||||
|
||||
class NameReceptacle[T](val name: String) {
|
||||
def as[B] = new NameReceptacle[B](name)
|
||||
def as[B](unmarshaller: FSU[B]) = new NameUnmarshallerReceptacle(name, unmarshaller)
|
||||
def ? = new NameOptionReceptacle[T](name)
|
||||
def ?[B](default: B) = new NameDefaultReceptacle(name, default)
|
||||
def  = new RequiredValueReceptacle(name, requiredValue)
|
||||
}
|
||||
|
||||
class NameUnmarshallerReceptacle[T](val name: String, val um: FSU[T]) {
|
||||
def ? = new NameOptionUnmarshallerReceptacle[T](name, um)
|
||||
def ?(default: T) = new NameDefaultUnmarshallerReceptacle(name, default, um)
|
||||
def !(requiredValue: T) = new RequiredValueUnmarshallerReceptacle(name, requiredValue, um)
|
||||
}
|
||||
|
||||
class NameOptionReceptacle[T](val name: String)
|
||||
|
||||
class NameDefaultReceptacle[T](val name: String, val default: T)
|
||||
|
||||
class RequiredValueReceptacle[T](val name: String, val requiredValue: T)
|
||||
|
||||
class NameOptionUnmarshallerReceptacle[T](val name: String, val um: FSU[T])
|
||||
|
||||
class NameDefaultUnmarshallerReceptacle[T](val name: String, val default: T, val um: FSU[T])
|
||||
|
||||
class RequiredValueUnmarshallerReceptacle[T](val name: String, val requiredValue: T, val um: FSU[T])
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.common
|
||||
|
||||
import scala.annotation.implicitNotFound
|
||||
import scala.collection.immutable
|
||||
import scala.concurrent.{ ExecutionContext, Future }
|
||||
import scala.concurrent.duration._
|
||||
import akka.stream.FlowMaterializer
|
||||
import akka.http.scaladsl.unmarshalling._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
import FastFuture._
|
||||
|
||||
/**
|
||||
* Read-only abstraction on top of `application/x-www-form-urlencoded` and multipart form data,
|
||||
* allowing joint unmarshalling access to either kind, **if** you supply both, a [[FromStringUnmarshaller]]
|
||||
* as well as a [[FromEntityUnmarshaller]] for the target type `T`.
|
||||
* Note: In order to allow for random access to the field values streamed multipart form data are strictified!
|
||||
* Don't use this abstraction on potentially unbounded forms (e.g. large file uploads).
|
||||
*
|
||||
* If you only need to consume one type of form (`application/x-www-form-urlencoded` *or* multipart) then
|
||||
* simply unmarshal directly to the respective form abstraction ([[FormData]] or [[Multipart.FormData]])
|
||||
* rather than going through [[StrictForm]].
|
||||
*
|
||||
* Simple usage example:
|
||||
* {{{
|
||||
* val strictFormFuture = Unmarshal(entity).to[StrictForm]
|
||||
* val fooFieldUnmarshalled: Future[T] =
|
||||
* strictFormFuture flatMap { form =>
|
||||
* Unmarshal(form field "foo").to[T]
|
||||
* }
|
||||
* }}}
|
||||
*/
|
||||
sealed abstract class StrictForm {
|
||||
def fields: immutable.Seq[(String, StrictForm.Field)]
|
||||
def field(name: String): Option[StrictForm.Field] = fields collectFirst { case (`name`, field) ⇒ field }
|
||||
}
|
||||
|
||||
object StrictForm {
|
||||
sealed trait Field
|
||||
object Field {
|
||||
private[StrictForm] final case class FromString(value: String) extends Field
|
||||
private[StrictForm] final case class FromPart(value: Multipart.FormData.BodyPart.Strict) extends Field
|
||||
|
||||
implicit def unmarshaller[T](implicit um: FieldUnmarshaller[T]): FromStrictFormFieldUnmarshaller[T] =
|
||||
Unmarshaller(implicit ec ⇒ {
|
||||
case FromString(value) ⇒ um.unmarshalString(value)
|
||||
case FromPart(value) ⇒ um.unmarshalPart(value)
|
||||
})
|
||||
|
||||
def unmarshallerFromFSU[T](fsu: FromStringUnmarshaller[T]): FromStrictFormFieldUnmarshaller[T] =
|
||||
Unmarshaller(implicit ec ⇒ {
|
||||
case FromString(value) ⇒ fsu(value)
|
||||
case FromPart(value) ⇒ fsu(value.entity.data.decodeString(value.entity.contentType.charset.nioCharset.name))
|
||||
})
|
||||
|
||||
@implicitNotFound("In order to unmarshal a `StrictForm.Field` to type `${T}` you need to supply a " +
|
||||
"`FromStringUnmarshaller[${T}]` and/or a `FromEntityUnmarshaller[${T}]`")
|
||||
sealed trait FieldUnmarshaller[T] {
|
||||
def unmarshalString(value: String)(implicit ec: ExecutionContext): Future[T]
|
||||
def unmarshalPart(value: Multipart.FormData.BodyPart.Strict)(implicit ec: ExecutionContext): Future[T]
|
||||
}
|
||||
object FieldUnmarshaller extends LowPrioImplicits {
|
||||
implicit def fromBoth[T](implicit fsu: FromStringUnmarshaller[T], feu: FromEntityUnmarshaller[T]) =
|
||||
new FieldUnmarshaller[T] {
|
||||
def unmarshalString(value: String)(implicit ec: ExecutionContext) = fsu(value)
|
||||
def unmarshalPart(value: Multipart.FormData.BodyPart.Strict)(implicit ec: ExecutionContext) = feu(value.entity)
|
||||
}
|
||||
}
|
||||
sealed abstract class LowPrioImplicits {
|
||||
implicit def fromFSU[T](implicit fsu: FromStringUnmarshaller[T]) =
|
||||
new FieldUnmarshaller[T] {
|
||||
def unmarshalString(value: String)(implicit ec: ExecutionContext) = fsu(value)
|
||||
def unmarshalPart(value: Multipart.FormData.BodyPart.Strict)(implicit ec: ExecutionContext) =
|
||||
fsu(value.entity.data.decodeString(value.entity.contentType.charset.nioCharset.name))
|
||||
}
|
||||
implicit def fromFEU[T](implicit feu: FromEntityUnmarshaller[T]) =
|
||||
new FieldUnmarshaller[T] {
|
||||
def unmarshalString(value: String)(implicit ec: ExecutionContext) = feu(HttpEntity(value))
|
||||
def unmarshalPart(value: Multipart.FormData.BodyPart.Strict)(implicit ec: ExecutionContext) = feu(value.entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicit def unmarshaller(implicit formDataUM: FromEntityUnmarshaller[FormData],
|
||||
multipartUM: FromEntityUnmarshaller[Multipart.FormData],
|
||||
fm: FlowMaterializer): FromEntityUnmarshaller[StrictForm] =
|
||||
Unmarshaller { implicit ec ⇒
|
||||
entity ⇒
|
||||
|
||||
def tryUnmarshalToQueryForm: Future[StrictForm] =
|
||||
for (formData ← formDataUM(entity).fast) yield {
|
||||
new StrictForm {
|
||||
val fields = formData.fields.map { case (name, value) ⇒ name -> Field.FromString(value) }(collection.breakOut)
|
||||
}
|
||||
}
|
||||
|
||||
def tryUnmarshalToMultipartForm: Future[StrictForm] =
|
||||
for {
|
||||
multiPartFD ← multipartUM(entity).fast
|
||||
strictMultiPartFD ← multiPartFD.toStrict(10.seconds).fast // TODO: make timeout configurable
|
||||
} yield {
|
||||
new StrictForm {
|
||||
val fields = strictMultiPartFD.strictParts.map {
|
||||
case x: Multipart.FormData.BodyPart.Strict ⇒ x.name -> Field.FromPart(x)
|
||||
}(collection.breakOut)
|
||||
}
|
||||
}
|
||||
|
||||
tryUnmarshalToQueryForm.fast.recoverWith {
|
||||
case Unmarshaller.UnsupportedContentTypeException(supported1) ⇒
|
||||
tryUnmarshalToMultipartForm.fast.recoverWith {
|
||||
case Unmarshaller.UnsupportedContentTypeException(supported2) ⇒
|
||||
FastFuture.failed(Unmarshaller.UnsupportedContentTypeException(supported1 ++ supported2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple model for strict file content in a multipart form data part.
|
||||
*/
|
||||
final case class FileData(filename: Option[String], entity: HttpEntity.Strict)
|
||||
|
||||
object FileData {
|
||||
implicit val unmarshaller: FromStrictFormFieldUnmarshaller[FileData] =
|
||||
Unmarshaller strict {
|
||||
case Field.FromString(_) ⇒ throw Unmarshaller.UnsupportedContentTypeException(MediaTypes.`application/x-www-form-urlencoded`)
|
||||
case Field.FromPart(part) ⇒ FileData(part.filename, part.entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import scala.collection.immutable
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
class EmptyValue[+T] private (val emptyValue: T)
|
||||
|
||||
object EmptyValue {
|
||||
implicit def emptyEntity = new EmptyValue[UniversalEntity](HttpEntity.Empty)
|
||||
implicit val emptyHeadersAndEntity = new EmptyValue[(immutable.Seq[HttpHeader], UniversalEntity)](Nil -> HttpEntity.Empty)
|
||||
implicit val emptyResponse = new EmptyValue[HttpResponse](HttpResponse(entity = emptyEntity.emptyValue))
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{ Try, Failure, Success }
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
import FastFuture._
|
||||
|
||||
trait GenericMarshallers extends LowPriorityToResponseMarshallerImplicits {
|
||||
|
||||
implicit def throwableMarshaller[T]: Marshaller[Throwable, T] = Marshaller(_ ⇒ FastFuture.failed)
|
||||
|
||||
implicit def optionMarshaller[A, B](implicit m: Marshaller[A, B], empty: EmptyValue[B]): Marshaller[Option[A], B] =
|
||||
Marshaller { implicit ec ⇒
|
||||
{
|
||||
case Some(value) ⇒ m(value)
|
||||
case None ⇒ FastFuture.successful(Marshalling.Opaque(() ⇒ empty.emptyValue) :: Nil)
|
||||
}
|
||||
}
|
||||
|
||||
implicit def eitherMarshaller[A1, A2, B](implicit m1: Marshaller[A1, B], m2: Marshaller[A2, B]): Marshaller[Either[A1, A2], B] =
|
||||
Marshaller { implicit ec ⇒
|
||||
{
|
||||
case Left(a1) ⇒ m1(a1)
|
||||
case Right(a2) ⇒ m2(a2)
|
||||
}
|
||||
}
|
||||
|
||||
implicit def futureMarshaller[A, B](implicit m: Marshaller[A, B]): Marshaller[Future[A], B] =
|
||||
Marshaller(implicit ec ⇒ _.fast.flatMap(m(_)))
|
||||
|
||||
implicit def tryMarshaller[A, B](implicit m: Marshaller[A, B]): Marshaller[Try[A], B] =
|
||||
Marshaller { implicit ec ⇒
|
||||
{
|
||||
case Success(value) ⇒ m(value)
|
||||
case Failure(error) ⇒ FastFuture.failed(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object GenericMarshallers extends GenericMarshallers
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import scala.concurrent.{ ExecutionContext, Future }
|
||||
import akka.http.scaladsl.model.HttpCharsets._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
|
||||
object Marshal {
|
||||
def apply[T](value: T): Marshal[T] = new Marshal(value)
|
||||
|
||||
case class UnacceptableResponseContentTypeException(supported: Set[ContentType]) extends RuntimeException
|
||||
|
||||
private class MarshallingWeight(val weight: Float, val marshal: () ⇒ HttpResponse)
|
||||
}
|
||||
|
||||
class Marshal[A](val value: A) {
|
||||
/**
|
||||
* Marshals `value` using the first available [[Marshalling]] for `A` and `B` provided by the given [[Marshaller]].
|
||||
* If the marshalling is flexible with regard to the used charset `UTF-8` is chosen.
|
||||
*/
|
||||
def to[B](implicit m: Marshaller[A, B], ec: ExecutionContext): Future[B] =
|
||||
m(value).fast.map {
|
||||
_.head match {
|
||||
case Marshalling.WithFixedCharset(_, _, marshal) ⇒ marshal()
|
||||
case Marshalling.WithOpenCharset(_, marshal) ⇒ marshal(HttpCharsets.`UTF-8`)
|
||||
case Marshalling.Opaque(marshal) ⇒ marshal()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marshals `value` to an `HttpResponse` for the given `HttpRequest` with full content-negotiation.
|
||||
*/
|
||||
def toResponseFor(request: HttpRequest)(implicit m: ToResponseMarshaller[A], ec: ExecutionContext): Future[HttpResponse] = {
|
||||
import akka.http.scaladsl.marshalling.Marshal._
|
||||
val mediaRanges = request.acceptedMediaRanges // cache for performance
|
||||
val charsetRanges = request.acceptedCharsetRanges // cache for performance
|
||||
def qValueMT(mediaType: MediaType) = request.qValueForMediaType(mediaType, mediaRanges)
|
||||
def qValueCS(charset: HttpCharset) = request.qValueForCharset(charset, charsetRanges)
|
||||
|
||||
m(value).fast.map { marshallings ⇒
|
||||
val defaultMarshallingWeight = new MarshallingWeight(0f, { () ⇒
|
||||
val supportedContentTypes = marshallings collect {
|
||||
case Marshalling.WithFixedCharset(mt, cs, _) ⇒ ContentType(mt, cs)
|
||||
case Marshalling.WithOpenCharset(mt, _) ⇒ ContentType(mt)
|
||||
}
|
||||
throw UnacceptableResponseContentTypeException(supportedContentTypes.toSet)
|
||||
})
|
||||
def choose(acc: MarshallingWeight, mt: MediaType, cs: HttpCharset, marshal: () ⇒ HttpResponse) = {
|
||||
val weight = math.min(qValueMT(mt), qValueCS(cs))
|
||||
if (weight > acc.weight) new MarshallingWeight(weight, marshal) else acc
|
||||
}
|
||||
val best = marshallings.foldLeft(defaultMarshallingWeight) {
|
||||
case (acc, Marshalling.WithFixedCharset(mt, cs, marshal)) ⇒
|
||||
choose(acc, mt, cs, marshal)
|
||||
case (acc, Marshalling.WithOpenCharset(mt, marshal)) ⇒
|
||||
def withCharset(cs: HttpCharset) = choose(acc, mt, cs, () ⇒ marshal(cs))
|
||||
// logic for choosing the charset adapted from http://tools.ietf.org/html/rfc7231#section-5.3.3
|
||||
if (qValueCS(`UTF-8`) == 1f) withCharset(`UTF-8`) // prefer UTF-8 if fully accepted
|
||||
else charsetRanges match {
|
||||
// pick the charset which the highest q-value (head of charsetRanges) if it isn't explicitly rejected
|
||||
case (HttpCharsetRange.One(cs, qValue)) :: _ if qValue > 0f ⇒ withCharset(cs)
|
||||
case _ ⇒ acc
|
||||
}
|
||||
|
||||
case (acc, Marshalling.Opaque(marshal)) ⇒
|
||||
if (acc.weight == 0f) new MarshallingWeight(Float.MinPositiveValue, marshal) else acc
|
||||
}
|
||||
best.marshal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import scala.concurrent.{ Future, ExecutionContext }
|
||||
import scala.util.control.NonFatal
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
|
||||
sealed abstract class Marshaller[-A, +B] {
|
||||
|
||||
def apply(value: A)(implicit ec: ExecutionContext): Future[List[Marshalling[B]]]
|
||||
|
||||
def map[C](f: B ⇒ C): Marshaller[A, C] =
|
||||
Marshaller(implicit ec ⇒ value ⇒ this(value).fast map (_ map (_ map f)))
|
||||
|
||||
/**
|
||||
* Reuses this Marshaller's logic to produce a new Marshaller from another type `C` which overrides
|
||||
* the produced [[ContentType]] with another one.
|
||||
* Depending on whether the given [[ContentType]] has a defined charset or not and whether the underlying
|
||||
* marshaller marshals with a fixed charset it can happen, that the wrapping becomes illegal.
|
||||
* For example, a marshaller producing content encoded with UTF-16 cannot be wrapped with a [[ContentType]]
|
||||
* that has a defined charset of UTF-8, since akka-http will never recode entities.
|
||||
* If the wrapping is illegal the [[Future]] produced by the resulting marshaller will contain a [[RuntimeException]].
|
||||
*/
|
||||
def wrap[C, D >: B](contentType: ContentType)(f: C ⇒ A)(implicit mto: MediaTypeOverrider[D]): Marshaller[C, D] =
|
||||
wrapWithEC[C, D](contentType)(_ ⇒ f)
|
||||
|
||||
/**
|
||||
* Reuses this Marshaller's logic to produce a new Marshaller from another type `C` which overrides
|
||||
* the produced [[ContentType]] with another one.
|
||||
* Depending on whether the given [[ContentType]] has a defined charset or not and whether the underlying
|
||||
* marshaller marshals with a fixed charset it can happen, that the wrapping becomes illegal.
|
||||
* For example, a marshaller producing content encoded with UTF-16 cannot be wrapped with a [[ContentType]]
|
||||
* that has a defined charset of UTF-8, since akka-http will never recode entities.
|
||||
* If the wrapping is illegal the [[Future]] produced by the resulting marshaller will contain a [[RuntimeException]].
|
||||
*/
|
||||
def wrapWithEC[C, D >: B](contentType: ContentType)(f: ExecutionContext ⇒ C ⇒ A)(implicit mto: MediaTypeOverrider[D]): Marshaller[C, D] =
|
||||
Marshaller { implicit ec ⇒
|
||||
value ⇒
|
||||
import Marshalling._
|
||||
this(f(ec)(value)).fast map {
|
||||
_ map {
|
||||
case WithFixedCharset(_, cs, marshal) if contentType.hasOpenCharset || contentType.charset == cs ⇒
|
||||
WithFixedCharset(contentType.mediaType, cs, () ⇒ mto(marshal(), contentType.mediaType))
|
||||
case WithOpenCharset(_, marshal) if contentType.hasOpenCharset ⇒
|
||||
WithOpenCharset(contentType.mediaType, cs ⇒ mto(marshal(cs), contentType.mediaType))
|
||||
case WithOpenCharset(_, marshal) ⇒
|
||||
WithFixedCharset(contentType.mediaType, contentType.charset, () ⇒ mto(marshal(contentType.charset), contentType.mediaType))
|
||||
case Opaque(marshal) if contentType.definedCharset.isEmpty ⇒ Opaque(() ⇒ mto(marshal(), contentType.mediaType))
|
||||
case x ⇒ sys.error(s"Illegal marshaller wrapping. Marshalling `$x` cannot be wrapped with ContentType `$contentType`")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def compose[C](f: C ⇒ A): Marshaller[C, B] =
|
||||
Marshaller(implicit ec ⇒ c ⇒ apply(f(c)))
|
||||
|
||||
def composeWithEC[C](f: ExecutionContext ⇒ C ⇒ A): Marshaller[C, B] =
|
||||
Marshaller(implicit ec ⇒ c ⇒ apply(f(ec)(c)))
|
||||
}
|
||||
|
||||
object Marshaller
|
||||
extends GenericMarshallers
|
||||
with PredefinedToEntityMarshallers
|
||||
with PredefinedToResponseMarshallers
|
||||
with PredefinedToRequestMarshallers {
|
||||
|
||||
/**
|
||||
* Creates a [[Marshaller]] from the given function.
|
||||
*/
|
||||
def apply[A, B](f: ExecutionContext ⇒ A ⇒ Future[List[Marshalling[B]]]): Marshaller[A, B] =
|
||||
new Marshaller[A, B] {
|
||||
def apply(value: A)(implicit ec: ExecutionContext) =
|
||||
try f(ec)(value)
|
||||
catch { case NonFatal(e) ⇒ FastFuture.failed(e) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for creating a [[Marshaller]] using the given function.
|
||||
*/
|
||||
def strict[A, B](f: A ⇒ Marshalling[B]): Marshaller[A, B] =
|
||||
Marshaller { _ ⇒ a ⇒ FastFuture.successful(f(a) :: Nil) }
|
||||
|
||||
/**
|
||||
* Helper for creating a "super-marshaller" from a number of "sub-marshallers".
|
||||
* Content-negotiation determines, which "sub-marshaller" eventually gets to do the job.
|
||||
*/
|
||||
def oneOf[A, B](marshallers: Marshaller[A, B]*): Marshaller[A, B] =
|
||||
Marshaller { implicit ec ⇒ a ⇒ FastFuture.sequence(marshallers.map(_(a))).fast.map(_.flatten.toList) }
|
||||
|
||||
/**
|
||||
* Helper for creating a "super-marshaller" from a number of values and a function producing "sub-marshallers"
|
||||
* from these values. Content-negotiation determines, which "sub-marshaller" eventually gets to do the job.
|
||||
*/
|
||||
def oneOf[T, A, B](values: T*)(f: T ⇒ Marshaller[A, B]): Marshaller[A, B] =
|
||||
oneOf(values map f: _*)
|
||||
|
||||
/**
|
||||
* Helper for creating a synchronous [[Marshaller]] to content with a fixed charset from the given function.
|
||||
*/
|
||||
def withFixedCharset[A, B](mediaType: MediaType, charset: HttpCharset)(marshal: A ⇒ B): Marshaller[A, B] =
|
||||
strict { value ⇒ Marshalling.WithFixedCharset(mediaType, charset, () ⇒ marshal(value)) }
|
||||
|
||||
/**
|
||||
* Helper for creating a synchronous [[Marshaller]] to content with a negotiable charset from the given function.
|
||||
*/
|
||||
def withOpenCharset[A, B](mediaType: MediaType)(marshal: (A, HttpCharset) ⇒ B): Marshaller[A, B] =
|
||||
strict { value ⇒ Marshalling.WithOpenCharset(mediaType, charset ⇒ marshal(value, charset)) }
|
||||
|
||||
/**
|
||||
* Helper for creating a synchronous [[Marshaller]] to non-negotiable content from the given function.
|
||||
*/
|
||||
def opaque[A, B](marshal: A ⇒ B): Marshaller[A, B] =
|
||||
strict { value ⇒ Marshalling.Opaque(() ⇒ marshal(value)) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes one possible option for marshalling a given value.
|
||||
*/
|
||||
sealed trait Marshalling[+A] {
|
||||
def map[B](f: A ⇒ B): Marshalling[B]
|
||||
}
|
||||
|
||||
object Marshalling {
|
||||
/**
|
||||
* A Marshalling to a specific MediaType and charset.
|
||||
*/
|
||||
final case class WithFixedCharset[A](mediaType: MediaType,
|
||||
charset: HttpCharset,
|
||||
marshal: () ⇒ A) extends Marshalling[A] {
|
||||
def map[B](f: A ⇒ B): WithFixedCharset[B] = copy(marshal = () ⇒ f(marshal()))
|
||||
}
|
||||
|
||||
/**
|
||||
* A Marshalling to a specific MediaType and a potentially flexible charset.
|
||||
*/
|
||||
final case class WithOpenCharset[A](mediaType: MediaType,
|
||||
marshal: HttpCharset ⇒ A) extends Marshalling[A] {
|
||||
def map[B](f: A ⇒ B): WithOpenCharset[B] = copy(marshal = cs ⇒ f(marshal(cs)))
|
||||
}
|
||||
|
||||
/**
|
||||
* A Marshalling to an unknown MediaType and charset.
|
||||
* Circumvents content negotiation.
|
||||
*/
|
||||
final case class Opaque[A](marshal: () ⇒ A) extends Marshalling[A] {
|
||||
def map[B](f: A ⇒ B): Opaque[B] = copy(marshal = () ⇒ f(marshal()))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import scala.collection.immutable
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
sealed trait MediaTypeOverrider[T] {
|
||||
def apply(value: T, mediaType: MediaType): T
|
||||
}
|
||||
object MediaTypeOverrider {
|
||||
implicit def forEntity[T <: HttpEntity]: MediaTypeOverrider[T] = new MediaTypeOverrider[T] {
|
||||
def apply(value: T, mediaType: MediaType) =
|
||||
value.withContentType(value.contentType withMediaType mediaType).asInstanceOf[T] // can't be expressed in types
|
||||
}
|
||||
implicit def forHeadersAndEntity[T <: HttpEntity] = new MediaTypeOverrider[(immutable.Seq[HttpHeader], T)] {
|
||||
def apply(value: (immutable.Seq[HttpHeader], T), mediaType: MediaType) =
|
||||
value._1 -> value._2.withContentType(value._2.contentType withMediaType mediaType).asInstanceOf[T]
|
||||
}
|
||||
implicit val forResponse = new MediaTypeOverrider[HttpResponse] {
|
||||
def apply(value: HttpResponse, mediaType: MediaType) =
|
||||
value.mapEntity(forEntity(_: ResponseEntity, mediaType))
|
||||
}
|
||||
implicit val forRequest = new MediaTypeOverrider[HttpRequest] {
|
||||
def apply(value: HttpRequest, mediaType: MediaType) =
|
||||
value.mapEntity(forEntity(_: RequestEntity, mediaType))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import scala.concurrent.forkjoin.ThreadLocalRandom
|
||||
import akka.parboiled2.util.Base64
|
||||
import akka.event.{ NoLogging, LoggingAdapter }
|
||||
import akka.stream.scaladsl.FlattenStrategy
|
||||
import akka.http.impl.engine.rendering.BodyPartRenderer
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
trait MultipartMarshallers {
|
||||
protected val multipartBoundaryRandom: java.util.Random = ThreadLocalRandom.current()
|
||||
|
||||
/**
|
||||
* Creates a new random 144-bit number and base64 encodes it (using a custom "safe" alphabet, yielding 24 characters).
|
||||
*/
|
||||
def randomBoundary: String = {
|
||||
val array = new Array[Byte](18)
|
||||
multipartBoundaryRandom.nextBytes(array)
|
||||
Base64.custom.encodeToString(array, false)
|
||||
}
|
||||
|
||||
implicit def multipartMarshaller[T <: Multipart](implicit log: LoggingAdapter = NoLogging): ToEntityMarshaller[T] =
|
||||
Marshaller strict { value ⇒
|
||||
val boundary = randomBoundary
|
||||
val contentType = ContentType(value.mediaType withBoundary boundary)
|
||||
Marshalling.WithOpenCharset(contentType.mediaType, { charset ⇒
|
||||
value match {
|
||||
case x: Multipart.Strict ⇒
|
||||
val data = BodyPartRenderer.strict(x.strictParts, boundary, charset.nioCharset, partHeadersSizeHint = 128, log)
|
||||
HttpEntity(contentType, data)
|
||||
case _ ⇒
|
||||
val chunks = value.parts
|
||||
.transform(() ⇒ BodyPartRenderer.streamed(boundary, charset.nioCharset, partHeadersSizeHint = 128, log))
|
||||
.flatten(FlattenStrategy.concat)
|
||||
HttpEntity.Chunked(contentType, chunks)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
object MultipartMarshallers extends MultipartMarshallers
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import java.nio.CharBuffer
|
||||
import akka.http.impl.model.parser.CharacterClasses
|
||||
import akka.http.scaladsl.model.MediaTypes._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.impl.util.StringRendering
|
||||
import akka.util.ByteString
|
||||
|
||||
trait PredefinedToEntityMarshallers extends MultipartMarshallers {
|
||||
|
||||
implicit val ByteArrayMarshaller: ToEntityMarshaller[Array[Byte]] = byteArrayMarshaller(`application/octet-stream`)
|
||||
def byteArrayMarshaller(mediaType: MediaType, charset: HttpCharset): ToEntityMarshaller[Array[Byte]] = {
|
||||
val ct = ContentType(mediaType, charset)
|
||||
Marshaller.withFixedCharset(ct.mediaType, ct.definedCharset.get) { bytes ⇒ HttpEntity(ct, bytes) }
|
||||
}
|
||||
def byteArrayMarshaller(mediaType: MediaType): ToEntityMarshaller[Array[Byte]] = {
|
||||
val ct = ContentType(mediaType)
|
||||
// since we don't want to recode we simply ignore the charset determined by content negotiation here
|
||||
Marshaller.withOpenCharset(ct.mediaType) { (bytes, _) ⇒ HttpEntity(ct, bytes) }
|
||||
}
|
||||
|
||||
implicit val ByteStringMarshaller: ToEntityMarshaller[ByteString] = byteStringMarshaller(`application/octet-stream`)
|
||||
def byteStringMarshaller(mediaType: MediaType, charset: HttpCharset): ToEntityMarshaller[ByteString] = {
|
||||
val ct = ContentType(mediaType, charset)
|
||||
Marshaller.withFixedCharset(ct.mediaType, ct.definedCharset.get) { bytes ⇒ HttpEntity(ct, bytes) }
|
||||
}
|
||||
def byteStringMarshaller(mediaType: MediaType): ToEntityMarshaller[ByteString] = {
|
||||
val ct = ContentType(mediaType)
|
||||
// since we don't want to recode we simply ignore the charset determined by content negotiation here
|
||||
Marshaller.withOpenCharset(ct.mediaType) { (bytes, _) ⇒ HttpEntity(ct, bytes) }
|
||||
}
|
||||
|
||||
implicit val CharArrayMarshaller: ToEntityMarshaller[Array[Char]] = charArrayMarshaller(`text/plain`)
|
||||
def charArrayMarshaller(mediaType: MediaType): ToEntityMarshaller[Array[Char]] =
|
||||
Marshaller.withOpenCharset(mediaType) { (value, charset) ⇒
|
||||
if (value.length > 0) {
|
||||
val charBuffer = CharBuffer.wrap(value)
|
||||
val byteBuffer = charset.nioCharset.encode(charBuffer)
|
||||
val array = new Array[Byte](byteBuffer.remaining())
|
||||
byteBuffer.get(array)
|
||||
HttpEntity(ContentType(mediaType, charset), array)
|
||||
} else HttpEntity.Empty
|
||||
}
|
||||
|
||||
implicit val StringMarshaller: ToEntityMarshaller[String] = stringMarshaller(`text/plain`)
|
||||
def stringMarshaller(mediaType: MediaType): ToEntityMarshaller[String] =
|
||||
Marshaller.withOpenCharset(mediaType) { (s, cs) ⇒ HttpEntity(ContentType(mediaType, cs), s) }
|
||||
|
||||
implicit val FormDataMarshaller: ToEntityMarshaller[FormData] =
|
||||
Marshaller.withOpenCharset(`application/x-www-form-urlencoded`) { (formData, charset) ⇒
|
||||
val query = Uri.Query(formData.fields: _*)
|
||||
val string = UriRendering.renderQuery(new StringRendering, query, charset.nioCharset, CharacterClasses.unreserved).get
|
||||
HttpEntity(ContentType(`application/x-www-form-urlencoded`, charset), string)
|
||||
}
|
||||
|
||||
implicit val HttpEntityMarshaller: ToEntityMarshaller[MessageEntity] = Marshaller strict { value ⇒
|
||||
Marshalling.WithFixedCharset(value.contentType.mediaType, value.contentType.charset, () ⇒ value)
|
||||
}
|
||||
}
|
||||
|
||||
object PredefinedToEntityMarshallers extends PredefinedToEntityMarshallers
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import scala.collection.immutable
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
|
||||
trait PredefinedToRequestMarshallers {
|
||||
private type TRM[T] = ToRequestMarshaller[T] // brevity alias
|
||||
|
||||
implicit val fromRequest: TRM[HttpRequest] = Marshaller.opaque(identity)
|
||||
|
||||
implicit def fromUri: TRM[Uri] =
|
||||
Marshaller strict { uri ⇒ Marshalling.Opaque(() ⇒ HttpRequest(uri = uri)) }
|
||||
|
||||
implicit def fromMethodAndUriAndValue[S, T](implicit mt: ToEntityMarshaller[T]): TRM[(HttpMethod, Uri, T)] =
|
||||
fromMethodAndUriAndHeadersAndValue[T] compose { case (m, u, v) ⇒ (m, u, Nil, v) }
|
||||
|
||||
implicit def fromMethodAndUriAndHeadersAndValue[T](implicit mt: ToEntityMarshaller[T]): TRM[(HttpMethod, Uri, immutable.Seq[HttpHeader], T)] =
|
||||
Marshaller(implicit ec ⇒ {
|
||||
case (m, u, h, v) ⇒ mt(v).fast map (_ map (_ map (HttpRequest(m, u, h, _))))
|
||||
})
|
||||
}
|
||||
|
||||
object PredefinedToRequestMarshallers extends PredefinedToRequestMarshallers
|
||||
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import scala.collection.immutable
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
import akka.http.scaladsl.model.MediaTypes._
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
trait PredefinedToResponseMarshallers extends LowPriorityToResponseMarshallerImplicits {
|
||||
|
||||
private type TRM[T] = ToResponseMarshaller[T] // brevity alias
|
||||
|
||||
def fromToEntityMarshaller[T](status: StatusCode = StatusCodes.OK,
|
||||
headers: immutable.Seq[HttpHeader] = Nil)(
|
||||
implicit m: ToEntityMarshaller[T]): ToResponseMarshaller[T] =
|
||||
fromStatusCodeAndHeadersAndValue compose (t ⇒ (status, headers, t))
|
||||
|
||||
implicit val fromResponse: TRM[HttpResponse] = Marshaller.opaque(identity)
|
||||
|
||||
implicit val fromStatusCode: TRM[StatusCode] =
|
||||
Marshaller.withOpenCharset(`text/plain`) { (status, charset) ⇒
|
||||
HttpResponse(status, entity = HttpEntity(ContentType(`text/plain`, charset), status.defaultMessage))
|
||||
}
|
||||
|
||||
implicit def fromStatusCodeAndValue[S, T](implicit sConv: S ⇒ StatusCode, mt: ToEntityMarshaller[T]): TRM[(S, T)] =
|
||||
fromStatusCodeAndHeadersAndValue[T] compose { case (status, value) ⇒ (sConv(status), Nil, value) }
|
||||
|
||||
implicit def fromStatusCodeConvertibleAndHeadersAndT[S, T](implicit sConv: S ⇒ StatusCode,
|
||||
mt: ToEntityMarshaller[T]): TRM[(S, immutable.Seq[HttpHeader], T)] =
|
||||
fromStatusCodeAndHeadersAndValue[T] compose { case (status, headers, value) ⇒ (sConv(status), headers, value) }
|
||||
|
||||
implicit def fromStatusCodeAndHeadersAndValue[T](implicit mt: ToEntityMarshaller[T]): TRM[(StatusCode, immutable.Seq[HttpHeader], T)] =
|
||||
Marshaller(implicit ec ⇒ {
|
||||
case (status, headers, value) ⇒ mt(value).fast map (_ map (_ map (HttpResponse(status, headers, _))))
|
||||
})
|
||||
}
|
||||
|
||||
trait LowPriorityToResponseMarshallerImplicits {
|
||||
implicit def liftMarshallerConversion[T](m: ToEntityMarshaller[T]): ToResponseMarshaller[T] =
|
||||
liftMarshaller(m)
|
||||
implicit def liftMarshaller[T](implicit m: ToEntityMarshaller[T]): ToResponseMarshaller[T] =
|
||||
PredefinedToResponseMarshallers.fromToEntityMarshaller()
|
||||
}
|
||||
|
||||
object PredefinedToResponseMarshallers extends PredefinedToResponseMarshallers
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import scala.concurrent.{ Future, ExecutionContext }
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
/** Something that can later be marshalled into a response */
|
||||
trait ToResponseMarshallable {
|
||||
type T
|
||||
def value: T
|
||||
implicit def marshaller: ToResponseMarshaller[T]
|
||||
|
||||
def apply(request: HttpRequest)(implicit ec: ExecutionContext): Future[HttpResponse] =
|
||||
Marshal(value).toResponseFor(request)
|
||||
}
|
||||
|
||||
object ToResponseMarshallable {
|
||||
implicit def apply[A](_value: A)(implicit _marshaller: ToResponseMarshaller[A]): ToResponseMarshallable =
|
||||
new ToResponseMarshallable {
|
||||
type T = A
|
||||
def value: T = _value
|
||||
def marshaller: ToResponseMarshaller[T] = _marshaller
|
||||
}
|
||||
|
||||
implicit val marshaller: ToResponseMarshaller[ToResponseMarshallable] =
|
||||
Marshaller { implicit ec ⇒ marshallable ⇒ marshallable.marshaller(marshallable.value) }
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl
|
||||
|
||||
import scala.collection.immutable
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
package object marshalling {
|
||||
type ToEntityMarshaller[T] = Marshaller[T, MessageEntity]
|
||||
type ToHeadersAndEntityMarshaller[T] = Marshaller[T, (immutable.Seq[HttpHeader], MessageEntity)]
|
||||
type ToResponseMarshaller[T] = Marshaller[T, HttpResponse]
|
||||
type ToRequestMarshaller[T] = Marshaller[T, HttpRequest]
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import scala.collection.immutable
|
||||
import akka.http.scaladsl.server.directives.RouteDirectives
|
||||
import akka.http.scaladsl.server.util._
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
|
||||
/**
|
||||
* A directive that provides a tuple of values of type `L` to create an inner route.
|
||||
*/
|
||||
abstract class Directive[L](implicit val ev: Tuple[L]) {
|
||||
|
||||
/**
|
||||
* Calls the inner route with a tuple of extracted values of type `L`.
|
||||
*
|
||||
* `tapply` is short for "tuple-apply". Usually, you will use the regular `apply` method instead,
|
||||
* which is added by an implicit conversion (see `Directive.addDirectiveApply`).
|
||||
*/
|
||||
def tapply(f: L ⇒ Route): Route
|
||||
|
||||
/**
|
||||
* Joins two directives into one which runs the second directive if the first one rejects.
|
||||
*/
|
||||
def |[R >: L](that: Directive[R]): Directive[R] =
|
||||
recover(rejections ⇒ directives.BasicDirectives.mapRejections(rejections ++ _) & that)(that.ev)
|
||||
|
||||
/**
|
||||
* Joins two directives into one which extracts the concatenation of its base directive extractions.
|
||||
* NOTE: Extraction joining is an O(N) operation with N being the number of extractions on the right-side.
|
||||
*/
|
||||
def &(magnet: ConjunctionMagnet[L]): magnet.Out = magnet(this)
|
||||
|
||||
/**
|
||||
* Converts this directive into one which, instead of a tuple of type ``L``, creates an
|
||||
* instance of type ``A`` (which is usually a case class).
|
||||
*/
|
||||
def as[A](constructor: ConstructFromTuple[L, A]): Directive1[A] = tmap(constructor)
|
||||
|
||||
/**
|
||||
* Maps over this directive using the given function, which can produce either a tuple or any other value
|
||||
* (which will then we wrapped into a [[Tuple1]]).
|
||||
*/
|
||||
def tmap[R](f: L ⇒ R)(implicit tupler: Tupler[R]): Directive[tupler.Out] =
|
||||
Directive[tupler.Out] { inner ⇒ tapply { values ⇒ inner(tupler(f(values))) } }(tupler.OutIsTuple)
|
||||
|
||||
/**
|
||||
* Flatmaps this directive using the given function.
|
||||
*/
|
||||
def tflatMap[R: Tuple](f: L ⇒ Directive[R]): Directive[R] =
|
||||
Directive[R] { inner ⇒ tapply { values ⇒ f(values) tapply inner } }
|
||||
|
||||
/**
|
||||
* Creates a new [[Directive0]], which passes if the given predicate matches the current
|
||||
* extractions or rejects with the given rejections.
|
||||
*/
|
||||
def trequire(predicate: L ⇒ Boolean, rejections: Rejection*): Directive0 =
|
||||
tfilter(predicate, rejections: _*).tflatMap(_ ⇒ Directive.Empty)
|
||||
|
||||
/**
|
||||
* Creates a new directive of the same type, which passes if the given predicate matches the current
|
||||
* extractions or rejects with the given rejections.
|
||||
*/
|
||||
def tfilter(predicate: L ⇒ Boolean, rejections: Rejection*): Directive[L] =
|
||||
Directive[L] { inner ⇒ tapply { values ⇒ ctx ⇒ if (predicate(values)) inner(values)(ctx) else ctx.reject(rejections: _*) } }
|
||||
|
||||
/**
|
||||
* Creates a new directive that is able to recover from rejections that were produced by `this` Directive
|
||||
* **before the inner route was applied**.
|
||||
*/
|
||||
def recover[R >: L: Tuple](recovery: immutable.Seq[Rejection] ⇒ Directive[R]): Directive[R] =
|
||||
Directive[R] { inner ⇒
|
||||
ctx ⇒
|
||||
import ctx.executionContext
|
||||
@volatile var rejectedFromInnerRoute = false
|
||||
tapply({ list ⇒ c ⇒ rejectedFromInnerRoute = true; inner(list)(c) })(ctx).fast.flatMap {
|
||||
case RouteResult.Rejected(rejections) if !rejectedFromInnerRoute ⇒ recovery(rejections).tapply(inner)(ctx)
|
||||
case x ⇒ FastFuture.successful(x)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of `recover` that only recovers from rejections handled by the given PartialFunction.
|
||||
*/
|
||||
def recoverPF[R >: L: Tuple](recovery: PartialFunction[immutable.Seq[Rejection], Directive[R]]): Directive[R] =
|
||||
recover { rejections ⇒ recovery.applyOrElse(rejections, (rejs: Seq[Rejection]) ⇒ RouteDirectives.reject(rejs: _*)) }
|
||||
}
|
||||
|
||||
object Directive {
|
||||
|
||||
/**
|
||||
* Constructs a directive from a function literal.
|
||||
*/
|
||||
def apply[T: Tuple](f: (T ⇒ Route) ⇒ Route): Directive[T] =
|
||||
new Directive[T] { def tapply(inner: T ⇒ Route) = f(inner) }
|
||||
|
||||
/**
|
||||
* A Directive that always passes the request on to its inner route (i.e. does nothing).
|
||||
*/
|
||||
val Empty: Directive0 = Directive(_())
|
||||
|
||||
/**
|
||||
* Adds `apply` to all Directives with 1 or more extractions,
|
||||
* which allows specifying an n-ary function to receive the extractions instead of a Function1[TupleX, Route].
|
||||
*/
|
||||
implicit def addDirectiveApply[L](directive: Directive[L])(implicit hac: ApplyConverter[L]): hac.In ⇒ Route =
|
||||
f ⇒ directive.tapply(hac(f))
|
||||
|
||||
/**
|
||||
* Adds `apply` to Directive0. Note: The `apply` parameter is call-by-name to ensure consistent execution behavior
|
||||
* with the directives producing extractions.
|
||||
*/
|
||||
implicit def addByNameNullaryApply(directive: Directive0): (⇒ Route) ⇒ Route =
|
||||
r ⇒ directive.tapply(_ ⇒ r)
|
||||
|
||||
implicit class SingleValueModifiers[T](underlying: Directive1[T]) extends AnyRef {
|
||||
def map[R](f: T ⇒ R)(implicit tupler: Tupler[R]): Directive[tupler.Out] =
|
||||
underlying.tmap { case Tuple1(value) ⇒ f(value) }
|
||||
|
||||
def flatMap[R: Tuple](f: T ⇒ Directive[R]): Directive[R] =
|
||||
underlying.tflatMap { case Tuple1(value) ⇒ f(value) }
|
||||
|
||||
def require(predicate: T ⇒ Boolean, rejections: Rejection*): Directive0 =
|
||||
underlying.filter(predicate, rejections: _*).tflatMap(_ ⇒ Empty)
|
||||
|
||||
def filter(predicate: T ⇒ Boolean, rejections: Rejection*): Directive1[T] =
|
||||
underlying.tfilter({ case Tuple1(value) ⇒ predicate(value) }, rejections: _*)
|
||||
}
|
||||
}
|
||||
|
||||
trait ConjunctionMagnet[L] {
|
||||
type Out
|
||||
def apply(underlying: Directive[L]): Out
|
||||
}
|
||||
|
||||
object ConjunctionMagnet {
|
||||
implicit def fromDirective[L, R](other: Directive[R])(implicit join: TupleOps.Join[L, R]): ConjunctionMagnet[L] { type Out = Directive[join.Out] } =
|
||||
new ConjunctionMagnet[L] {
|
||||
type Out = Directive[join.Out]
|
||||
def apply(underlying: Directive[L]) =
|
||||
Directive[join.Out] { inner ⇒
|
||||
underlying.tapply { prefix ⇒ other.tapply { suffix ⇒ inner(join(prefix, suffix)) } }
|
||||
}(Tuple.yes) // we know that join will only ever produce tuples
|
||||
}
|
||||
|
||||
implicit def fromStandardRoute[L](route: StandardRoute) =
|
||||
new ConjunctionMagnet[L] {
|
||||
type Out = StandardRoute
|
||||
def apply(underlying: Directive[L]) = StandardRoute(underlying.tapply(_ ⇒ route))
|
||||
}
|
||||
|
||||
implicit def fromRouteGenerator[T, R <: Route](generator: T ⇒ R) =
|
||||
new ConjunctionMagnet[Unit] {
|
||||
type Out = RouteGenerator[T]
|
||||
def apply(underlying: Directive0) = value ⇒ underlying.tapply(_ ⇒ generator(value))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import directives._
|
||||
|
||||
trait Directives extends RouteConcatenation
|
||||
with BasicDirectives
|
||||
with CacheConditionDirectives
|
||||
with CookieDirectives
|
||||
with DebuggingDirectives
|
||||
with CodingDirectives
|
||||
with ExecutionDirectives
|
||||
with FileAndResourceDirectives
|
||||
with FormFieldDirectives
|
||||
with FutureDirectives
|
||||
with HeaderDirectives
|
||||
with HostDirectives
|
||||
with MarshallingDirectives
|
||||
with MethodDirectives
|
||||
with MiscDirectives
|
||||
with ParameterDirectives
|
||||
with PathDirectives
|
||||
with RangeDirectives
|
||||
with RespondWithDirectives
|
||||
with RouteDirectives
|
||||
with SchemeDirectives
|
||||
with SecurityDirectives
|
||||
with WebsocketDirectives
|
||||
|
||||
object Directives extends Directives
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
import akka.http.scaladsl.model._
|
||||
import StatusCodes._
|
||||
|
||||
trait ExceptionHandler extends ExceptionHandler.PF {
|
||||
|
||||
/**
|
||||
* Creates a new [[ExceptionHandler]] which uses the given one as fallback for this one.
|
||||
*/
|
||||
def withFallback(that: ExceptionHandler): ExceptionHandler
|
||||
|
||||
/**
|
||||
* "Seals" this handler by attaching a default handler as fallback if necessary.
|
||||
*/
|
||||
def seal(settings: RoutingSettings): ExceptionHandler
|
||||
}
|
||||
|
||||
object ExceptionHandler {
|
||||
type PF = PartialFunction[Throwable, Route]
|
||||
|
||||
implicit def apply(pf: PF): ExceptionHandler = apply(knownToBeSealed = false)(pf)
|
||||
|
||||
private def apply(knownToBeSealed: Boolean)(pf: PF): ExceptionHandler =
|
||||
new ExceptionHandler {
|
||||
def isDefinedAt(error: Throwable) = pf.isDefinedAt(error)
|
||||
def apply(error: Throwable) = pf(error)
|
||||
def withFallback(that: ExceptionHandler): ExceptionHandler =
|
||||
if (!knownToBeSealed) ExceptionHandler(knownToBeSealed = false)(this orElse that) else this
|
||||
def seal(settings: RoutingSettings): ExceptionHandler =
|
||||
if (!knownToBeSealed) ExceptionHandler(knownToBeSealed = true)(this orElse default(settings)) else this
|
||||
}
|
||||
|
||||
def default(settings: RoutingSettings): ExceptionHandler =
|
||||
apply(knownToBeSealed = true) {
|
||||
case IllegalRequestException(info, status) ⇒ ctx ⇒ {
|
||||
ctx.log.warning("Illegal request {}\n\t{}\n\tCompleting with '{}' response",
|
||||
ctx.request, info.formatPretty, status)
|
||||
ctx.complete(status, info.format(settings.verboseErrorMessages))
|
||||
}
|
||||
case NonFatal(e) ⇒ ctx ⇒ {
|
||||
ctx.log.error(e, "Error during processing of request {}", ctx.request)
|
||||
ctx.complete(InternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,475 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import java.util.UUID
|
||||
import scala.util.matching.Regex
|
||||
import scala.annotation.tailrec
|
||||
import akka.http.scaladsl.server.util.Tuple
|
||||
import akka.http.scaladsl.server.util.TupleOps._
|
||||
import akka.http.scaladsl.common.NameOptionReceptacle
|
||||
import akka.http.scaladsl.model.Uri.Path
|
||||
import akka.http.impl.util._
|
||||
|
||||
/**
|
||||
* A PathMatcher tries to match a prefix of a given string and returns either a PathMatcher.Matched instance
|
||||
* if matched, otherwise PathMatcher.Unmatched.
|
||||
*/
|
||||
abstract class PathMatcher[L](implicit val ev: Tuple[L]) extends (Path ⇒ PathMatcher.Matching[L]) { self ⇒
|
||||
import PathMatcher._
|
||||
|
||||
def / : PathMatcher[L] = this ~ PathMatchers.Slash
|
||||
|
||||
def /[R](other: PathMatcher[R])(implicit join: Join[L, R]): PathMatcher[join.Out] =
|
||||
this ~ PathMatchers.Slash ~ other
|
||||
|
||||
def |[R >: L: Tuple](other: PathMatcher[_ <: R]): PathMatcher[R] =
|
||||
new PathMatcher[R] {
|
||||
def apply(path: Path) = self(path) orElse other(path)
|
||||
}
|
||||
|
||||
def ~[R](other: PathMatcher[R])(implicit join: Join[L, R]): PathMatcher[join.Out] = {
|
||||
implicit def joinProducesTuple = Tuple.yes[join.Out]
|
||||
transform(_.andThen((restL, valuesL) ⇒ other(restL).map(join(valuesL, _))))
|
||||
}
|
||||
|
||||
def unary_!(): PathMatcher0 =
|
||||
new PathMatcher[Unit] {
|
||||
def apply(path: Path) = if (self(path) eq Unmatched) Matched(path, ()) else Unmatched
|
||||
}
|
||||
|
||||
def transform[R: Tuple](f: Matching[L] ⇒ Matching[R]): PathMatcher[R] =
|
||||
new PathMatcher[R] { def apply(path: Path) = f(self(path)) }
|
||||
|
||||
def tmap[R: Tuple](f: L ⇒ R): PathMatcher[R] = transform(_.map(f))
|
||||
|
||||
def tflatMap[R: Tuple](f: L ⇒ Option[R]): PathMatcher[R] = transform(_.flatMap(f))
|
||||
|
||||
/**
|
||||
* Same as ``repeat(min = count, max = count)``.
|
||||
*/
|
||||
def repeat(count: Int)(implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] =
|
||||
repeat(min = count, max = count)
|
||||
|
||||
/**
|
||||
* Same as ``repeat(min = count, max = count, separator = separator)``.
|
||||
*/
|
||||
def repeat(count: Int, separator: PathMatcher0)(implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] =
|
||||
repeat(min = count, max = count, separator = separator)
|
||||
|
||||
/**
|
||||
* Turns this ``PathMatcher`` into one that matches a number of times (with the given separator)
|
||||
* and potentially extracts a ``List`` of the underlying matcher's extractions.
|
||||
* If less than ``min`` applications of the underlying matcher have succeeded the produced matcher fails,
|
||||
* otherwise it matches up to the given ``max`` number of applications.
|
||||
* Note that it won't fail even if more than ``max`` applications could succeed!
|
||||
* The "surplus" path elements will simply be left unmatched.
|
||||
*
|
||||
* The result type depends on the type of the underlying matcher:
|
||||
*
|
||||
* <table>
|
||||
* <th><td>If a ``matcher`` is of type</td><td>then ``matcher.repeat(...)`` is of type</td></th>
|
||||
* <tr><td>``PathMatcher0``</td><td>``PathMatcher0``</td></tr>
|
||||
* <tr><td>``PathMatcher1[T]``</td><td>``PathMatcher1[List[T]``</td></tr>
|
||||
* <tr><td>``PathMatcher[L :Tuple]``</td><td>``PathMatcher[List[L]]``</td></tr>
|
||||
* </table>
|
||||
*/
|
||||
def repeat(min: Int, max: Int, separator: PathMatcher0 = PathMatchers.Neutral)(implicit lift: PathMatcher.Lift[L, List]): PathMatcher[lift.Out] =
|
||||
new PathMatcher[lift.Out]()(lift.OutIsTuple) {
|
||||
require(min >= 0, "`min` must be >= 0")
|
||||
require(max >= min, "`max` must be >= `min`")
|
||||
def apply(path: Path) = rec(path, 1)
|
||||
def rec(path: Path, count: Int): Matching[lift.Out] = {
|
||||
def done = if (count >= min) Matched(path, lift()) else Unmatched
|
||||
if (count <= max) {
|
||||
self(path) match {
|
||||
case Matched(remaining, extractions) ⇒
|
||||
def done1 = if (count >= min) Matched(remaining, lift(extractions)) else Unmatched
|
||||
separator(remaining) match {
|
||||
case Matched(remaining2, _) ⇒ rec(remaining2, count + 1) match {
|
||||
case Matched(`remaining2`, _) ⇒ done1 // we made no progress, so "go back" to before the separator
|
||||
case Matched(rest, result) ⇒ Matched(rest, lift(extractions, result))
|
||||
case Unmatched ⇒ Unmatched
|
||||
}
|
||||
case Unmatched ⇒ done1
|
||||
}
|
||||
case Unmatched ⇒ done
|
||||
}
|
||||
} else done
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object PathMatcher extends ImplicitPathMatcherConstruction {
|
||||
sealed abstract class Matching[+L: Tuple] {
|
||||
def map[R: Tuple](f: L ⇒ R): Matching[R]
|
||||
def flatMap[R: Tuple](f: L ⇒ Option[R]): Matching[R]
|
||||
def andThen[R: Tuple](f: (Path, L) ⇒ Matching[R]): Matching[R]
|
||||
def orElse[R >: L](other: ⇒ Matching[R]): Matching[R]
|
||||
}
|
||||
case class Matched[L: Tuple](pathRest: Path, extractions: L) extends Matching[L] {
|
||||
def map[R: Tuple](f: L ⇒ R) = Matched(pathRest, f(extractions))
|
||||
def flatMap[R: Tuple](f: L ⇒ Option[R]) = f(extractions) match {
|
||||
case Some(valuesR) ⇒ Matched(pathRest, valuesR)
|
||||
case None ⇒ Unmatched
|
||||
}
|
||||
def andThen[R: Tuple](f: (Path, L) ⇒ Matching[R]) = f(pathRest, extractions)
|
||||
def orElse[R >: L](other: ⇒ Matching[R]) = this
|
||||
}
|
||||
object Matched { val Empty = Matched(Path.Empty, ()) }
|
||||
case object Unmatched extends Matching[Nothing] {
|
||||
def map[R: Tuple](f: Nothing ⇒ R) = this
|
||||
def flatMap[R: Tuple](f: Nothing ⇒ Option[R]) = this
|
||||
def andThen[R: Tuple](f: (Path, Nothing) ⇒ Matching[R]) = this
|
||||
def orElse[R](other: ⇒ Matching[R]) = other
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PathMatcher that always matches, consumes nothing and extracts the given Tuple of values.
|
||||
*/
|
||||
def provide[L: Tuple](extractions: L): PathMatcher[L] =
|
||||
new PathMatcher[L] {
|
||||
def apply(path: Path) = Matched(path, extractions)(ev)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PathMatcher that matches and consumes the given path prefix and extracts the given list of extractions.
|
||||
* If the given prefix is empty the returned PathMatcher matches always and consumes nothing.
|
||||
*/
|
||||
def apply[L: Tuple](prefix: Path, extractions: L): PathMatcher[L] =
|
||||
if (prefix.isEmpty) provide(extractions)
|
||||
else new PathMatcher[L] {
|
||||
def apply(path: Path) =
|
||||
if (path startsWith prefix) Matched(path dropChars prefix.charCount, extractions)(ev)
|
||||
else Unmatched
|
||||
}
|
||||
|
||||
def apply[L](magnet: PathMatcher[L]): PathMatcher[L] = magnet
|
||||
|
||||
implicit class PathMatcher1Ops[T](matcher: PathMatcher1[T]) {
|
||||
def map[R](f: T ⇒ R): PathMatcher1[R] = matcher.tmap { case Tuple1(e) ⇒ Tuple1(f(e)) }
|
||||
def flatMap[R](f: T ⇒ Option[R]): PathMatcher1[R] =
|
||||
matcher.tflatMap { case Tuple1(e) ⇒ f(e).map(x ⇒ Tuple1(x)) }
|
||||
}
|
||||
|
||||
implicit class EnhancedPathMatcher[L](underlying: PathMatcher[L]) {
|
||||
def ?(implicit lift: PathMatcher.Lift[L, Option]): PathMatcher[lift.Out] =
|
||||
new PathMatcher[lift.Out]()(lift.OutIsTuple) {
|
||||
def apply(path: Path) = underlying(path) match {
|
||||
case Matched(rest, extractions) ⇒ Matched(rest, lift(extractions))
|
||||
case Unmatched ⇒ Matched(path, lift())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait Lift[L, M[+_]] {
|
||||
type Out
|
||||
def OutIsTuple: Tuple[Out]
|
||||
def apply(): Out
|
||||
def apply(value: L): Out
|
||||
def apply(value: L, more: Out): Out
|
||||
}
|
||||
object Lift extends LowLevelLiftImplicits {
|
||||
trait MOps[M[+_]] {
|
||||
def apply(): M[Nothing]
|
||||
def apply[T](value: T): M[T]
|
||||
def apply[T](value: T, more: M[T]): M[T]
|
||||
}
|
||||
object MOps {
|
||||
implicit object OptionMOps extends MOps[Option] {
|
||||
def apply(): Option[Nothing] = None
|
||||
def apply[T](value: T): Option[T] = Some(value)
|
||||
def apply[T](value: T, more: Option[T]): Option[T] = Some(value)
|
||||
}
|
||||
implicit object ListMOps extends MOps[List] {
|
||||
def apply(): List[Nothing] = Nil
|
||||
def apply[T](value: T): List[T] = value :: Nil
|
||||
def apply[T](value: T, more: List[T]): List[T] = value :: more
|
||||
}
|
||||
}
|
||||
implicit def liftUnit[M[+_]] = new Lift[Unit, M] {
|
||||
type Out = Unit
|
||||
def OutIsTuple = implicitly[Tuple[Out]]
|
||||
def apply() = ()
|
||||
def apply(value: Unit) = value
|
||||
def apply(value: Unit, more: Out) = value
|
||||
}
|
||||
implicit def liftSingleElement[A, M[+_]](implicit mops: MOps[M]) = new Lift[Tuple1[A], M] {
|
||||
type Out = Tuple1[M[A]]
|
||||
def OutIsTuple = implicitly[Tuple[Out]]
|
||||
def apply() = Tuple1(mops())
|
||||
def apply(value: Tuple1[A]) = Tuple1(mops(value._1))
|
||||
def apply(value: Tuple1[A], more: Out) = Tuple1(mops(value._1, more._1))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trait LowLevelLiftImplicits {
|
||||
import Lift._
|
||||
implicit def default[T, M[+_]](implicit mops: MOps[M]) = new Lift[T, M] {
|
||||
type Out = Tuple1[M[T]]
|
||||
def OutIsTuple = implicitly[Tuple[Out]]
|
||||
def apply() = Tuple1(mops())
|
||||
def apply(value: T) = Tuple1(mops(value))
|
||||
def apply(value: T, more: Out) = Tuple1(mops(value, more._1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait ImplicitPathMatcherConstruction {
|
||||
import PathMatcher._
|
||||
|
||||
/**
|
||||
* Creates a PathMatcher that consumes (a prefix of) the first path segment
|
||||
* (if the path begins with a segment) and extracts a given value.
|
||||
*/
|
||||
implicit def stringExtractionPair2PathMatcher[T](tuple: (String, T)): PathMatcher1[T] =
|
||||
PathMatcher(tuple._1 :: Path.Empty, Tuple1(tuple._2))
|
||||
|
||||
/**
|
||||
* Creates a PathMatcher that consumes (a prefix of) the first path segment
|
||||
* (if the path begins with a segment).
|
||||
*/
|
||||
implicit def segmentStringToPathMatcher(segment: String): PathMatcher0 =
|
||||
PathMatcher(segment :: Path.Empty, ())
|
||||
|
||||
implicit def stringNameOptionReceptacle2PathMatcher(nr: NameOptionReceptacle[String]): PathMatcher0 =
|
||||
PathMatcher(nr.name).?
|
||||
|
||||
/**
|
||||
* Creates a PathMatcher that consumes (a prefix of) the first path segment
|
||||
* if the path begins with a segment (a prefix of) which matches the given regex.
|
||||
* Extracts either the complete match (if the regex doesn't contain a capture group) or
|
||||
* the capture group (if the regex contains exactly one).
|
||||
* If the regex contains more than one capture group the method throws an IllegalArgumentException.
|
||||
*/
|
||||
implicit def regex2PathMatcher(regex: Regex): PathMatcher1[String] = regex.groupCount match {
|
||||
case 0 ⇒ new PathMatcher1[String] {
|
||||
def apply(path: Path) = path match {
|
||||
case Path.Segment(segment, tail) ⇒ regex findPrefixOf segment match {
|
||||
case Some(m) ⇒ Matched(segment.substring(m.length) :: tail, Tuple1(m))
|
||||
case None ⇒ Unmatched
|
||||
}
|
||||
case _ ⇒ Unmatched
|
||||
}
|
||||
}
|
||||
case 1 ⇒ new PathMatcher1[String] {
|
||||
def apply(path: Path) = path match {
|
||||
case Path.Segment(segment, tail) ⇒ regex findPrefixMatchOf segment match {
|
||||
case Some(m) ⇒ Matched(segment.substring(m.end) :: tail, Tuple1(m.group(1)))
|
||||
case None ⇒ Unmatched
|
||||
}
|
||||
case _ ⇒ Unmatched
|
||||
}
|
||||
}
|
||||
case _ ⇒ throw new IllegalArgumentException("Path regex '" + regex.pattern.pattern +
|
||||
"' must not contain more than one capturing group")
|
||||
}
|
||||
/**
|
||||
* Creates a PathMatcher from the given Map of path segments (prefixes) to extracted values.
|
||||
* If the unmatched path starts with a segment having one of the maps keys as a prefix
|
||||
* the matcher consumes this path segment (prefix) and extracts the corresponding map value.
|
||||
*/
|
||||
implicit def valueMap2PathMatcher[T](valueMap: Map[String, T]): PathMatcher1[T] =
|
||||
if (valueMap.isEmpty) PathMatchers.nothingMatcher
|
||||
else valueMap.map { case (prefix, value) ⇒ stringExtractionPair2PathMatcher(prefix, value) }.reduceLeft(_ | _)
|
||||
}
|
||||
|
||||
trait PathMatchers {
|
||||
import PathMatcher._
|
||||
|
||||
/**
|
||||
* Converts a path string containing slashes into a PathMatcher that interprets slashes as
|
||||
* path segment separators.
|
||||
*/
|
||||
def separateOnSlashes(string: String): PathMatcher0 = {
|
||||
@tailrec def split(ix: Int = 0, matcher: PathMatcher0 = null): PathMatcher0 = {
|
||||
val nextIx = string.indexOf('/', ix)
|
||||
def append(m: PathMatcher0) = if (matcher eq null) m else matcher / m
|
||||
if (nextIx < 0) append(string.substring(ix))
|
||||
else split(nextIx + 1, append(string.substring(ix, nextIx)))
|
||||
}
|
||||
split()
|
||||
}
|
||||
|
||||
/**
|
||||
* A PathMatcher that matches a single slash character ('/').
|
||||
*/
|
||||
object Slash extends PathMatcher0 {
|
||||
def apply(path: Path) = path match {
|
||||
case Path.Slash(tail) ⇒ Matched(tail, ())
|
||||
case _ ⇒ Unmatched
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A PathMatcher that matches the very end of the requests URI path.
|
||||
*/
|
||||
object PathEnd extends PathMatcher0 {
|
||||
def apply(path: Path) = path match {
|
||||
case Path.Empty ⇒ Matched.Empty
|
||||
case _ ⇒ Unmatched
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A PathMatcher that matches and extracts the complete remaining,
|
||||
* unmatched part of the request's URI path as an (encoded!) String.
|
||||
* If you need access to the remaining unencoded elements of the path
|
||||
* use the `RestPath` matcher!
|
||||
*/
|
||||
object Rest extends PathMatcher1[String] {
|
||||
def apply(path: Path) = Matched(Path.Empty, Tuple1(path.toString))
|
||||
}
|
||||
|
||||
/**
|
||||
* A PathMatcher that matches and extracts the complete remaining,
|
||||
* unmatched part of the request's URI path.
|
||||
*/
|
||||
object RestPath extends PathMatcher1[Path] {
|
||||
def apply(path: Path) = Matched(Path.Empty, Tuple1(path))
|
||||
}
|
||||
|
||||
/**
|
||||
* A PathMatcher that efficiently matches a number of digits and extracts their (non-negative) Int value.
|
||||
* The matcher will not match 0 digits or a sequence of digits that would represent an Int value larger
|
||||
* than Int.MaxValue.
|
||||
*/
|
||||
object IntNumber extends NumberMatcher[Int](Int.MaxValue, 10) {
|
||||
def fromChar(c: Char) = fromDecimalChar(c)
|
||||
}
|
||||
|
||||
/**
|
||||
* A PathMatcher that efficiently matches a number of digits and extracts their (non-negative) Long value.
|
||||
* The matcher will not match 0 digits or a sequence of digits that would represent an Long value larger
|
||||
* than Long.MaxValue.
|
||||
*/
|
||||
object LongNumber extends NumberMatcher[Long](Long.MaxValue, 10) {
|
||||
def fromChar(c: Char) = fromDecimalChar(c)
|
||||
}
|
||||
|
||||
/**
|
||||
* A PathMatcher that efficiently matches a number of hex-digits and extracts their (non-negative) Int value.
|
||||
* The matcher will not match 0 digits or a sequence of digits that would represent an Int value larger
|
||||
* than Int.MaxValue.
|
||||
*/
|
||||
object HexIntNumber extends NumberMatcher[Int](Int.MaxValue, 16) {
|
||||
def fromChar(c: Char) = fromHexChar(c)
|
||||
}
|
||||
|
||||
/**
|
||||
* A PathMatcher that efficiently matches a number of hex-digits and extracts their (non-negative) Long value.
|
||||
* The matcher will not match 0 digits or a sequence of digits that would represent an Long value larger
|
||||
* than Long.MaxValue.
|
||||
*/
|
||||
object HexLongNumber extends NumberMatcher[Long](Long.MaxValue, 16) {
|
||||
def fromChar(c: Char) = fromHexChar(c)
|
||||
}
|
||||
|
||||
// common implementation of Number matchers
|
||||
abstract class NumberMatcher[@specialized(Int, Long) T](max: T, base: T)(implicit x: Integral[T])
|
||||
extends PathMatcher1[T] {
|
||||
|
||||
import x._ // import implicit conversions for numeric operators
|
||||
val minusOne = x.zero - x.one
|
||||
val maxDivBase = max / base
|
||||
|
||||
def apply(path: Path) = path match {
|
||||
case Path.Segment(segment, tail) ⇒
|
||||
@tailrec def digits(ix: Int = 0, value: T = minusOne): Matching[Tuple1[T]] = {
|
||||
val a = if (ix < segment.length) fromChar(segment charAt ix) else minusOne
|
||||
if (a == minusOne) {
|
||||
if (value == minusOne) Unmatched
|
||||
else Matched(if (ix < segment.length) segment.substring(ix) :: tail else tail, Tuple1(value))
|
||||
} else {
|
||||
if (value == minusOne) digits(ix + 1, a)
|
||||
else if (value <= maxDivBase && value * base <= max - a) // protect from overflow
|
||||
digits(ix + 1, value * base + a)
|
||||
else Unmatched
|
||||
}
|
||||
}
|
||||
digits()
|
||||
|
||||
case _ ⇒ Unmatched
|
||||
}
|
||||
|
||||
def fromChar(c: Char): T
|
||||
|
||||
def fromDecimalChar(c: Char): T = if ('0' <= c && c <= '9') x.fromInt(c - '0') else minusOne
|
||||
|
||||
def fromHexChar(c: Char): T =
|
||||
if ('0' <= c && c <= '9') x.fromInt(c - '0') else {
|
||||
val cn = c | 0x20 // normalize to lowercase
|
||||
if ('a' <= cn && cn <= 'f') x.fromInt(cn - 'a' + 10) else minusOne
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A PathMatcher that matches and extracts a Double value. The matched string representation is the pure decimal,
|
||||
* optionally signed form of a double value, i.e. without exponent.
|
||||
*/
|
||||
val DoubleNumber: PathMatcher1[Double] =
|
||||
PathMatcher("""[+-]?\d*\.?\d*""".r) flatMap { string ⇒
|
||||
try Some(java.lang.Double.parseDouble(string))
|
||||
catch { case _: NumberFormatException ⇒ None }
|
||||
}
|
||||
|
||||
/**
|
||||
* A PathMatcher that matches and extracts a java.util.UUID instance.
|
||||
*/
|
||||
val JavaUUID: PathMatcher1[UUID] =
|
||||
PathMatcher("""[\da-fA-F]{8}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{4}-[\da-fA-F]{12}""".r) flatMap { string ⇒
|
||||
try Some(UUID.fromString(string))
|
||||
catch { case _: IllegalArgumentException ⇒ None }
|
||||
}
|
||||
|
||||
/**
|
||||
* A PathMatcher that always matches, doesn't consume anything and extracts nothing.
|
||||
* Serves mainly as a neutral element in PathMatcher composition.
|
||||
*/
|
||||
val Neutral: PathMatcher0 = PathMatcher.provide(())
|
||||
|
||||
/**
|
||||
* A PathMatcher that matches if the unmatched path starts with a path segment.
|
||||
* If so the path segment is extracted as a String.
|
||||
*/
|
||||
object Segment extends PathMatcher1[String] {
|
||||
def apply(path: Path) = path match {
|
||||
case Path.Segment(segment, tail) ⇒ Matched(tail, Tuple1(segment))
|
||||
case _ ⇒ Unmatched
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A PathMatcher that matches up to 128 remaining segments as a List[String].
|
||||
* This can also be no segments resulting in the empty list.
|
||||
* If the path has a trailing slash this slash will *not* be matched.
|
||||
*/
|
||||
val Segments: PathMatcher1[List[String]] = Segments(min = 0, max = 128)
|
||||
|
||||
/**
|
||||
* A PathMatcher that matches the given number of path segments (separated by slashes) as a List[String].
|
||||
* If there are more than ``count`` segments present the remaining ones will be left unmatched.
|
||||
* If the path has a trailing slash this slash will *not* be matched.
|
||||
*/
|
||||
def Segments(count: Int): PathMatcher1[List[String]] = Segment.repeat(count, separator = Slash)
|
||||
|
||||
/**
|
||||
* A PathMatcher that matches between ``min`` and ``max`` (both inclusively) path segments (separated by slashes)
|
||||
* as a List[String]. If there are more than ``count`` segments present the remaining ones will be left unmatched.
|
||||
* If the path has a trailing slash this slash will *not* be matched.
|
||||
*/
|
||||
def Segments(min: Int, max: Int): PathMatcher1[List[String]] = Segment.repeat(min, max, separator = Slash)
|
||||
|
||||
/**
|
||||
* A PathMatcher that never matches anything.
|
||||
*/
|
||||
def nothingMatcher[L: Tuple]: PathMatcher[L] =
|
||||
new PathMatcher[L] {
|
||||
def apply(p: Path) = Unmatched
|
||||
}
|
||||
}
|
||||
|
||||
object PathMatchers extends PathMatchers
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import scala.collection.immutable
|
||||
import akka.http.scaladsl.model._
|
||||
import headers._
|
||||
|
||||
/**
|
||||
* A rejection encapsulates a specific reason why a Route was not able to handle a request. Rejections are gathered
|
||||
* up over the course of a Route evaluation and finally converted to [[spray.http.HttpResponse]]s by the
|
||||
* `handleRejections` directive, if there was no way for the request to be completed.
|
||||
*/
|
||||
trait Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by method filters.
|
||||
* Signals that the request was rejected because the HTTP method is unsupported.
|
||||
*/
|
||||
case class MethodRejection(supported: HttpMethod) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by scheme filters.
|
||||
* Signals that the request was rejected because the Uri scheme is unsupported.
|
||||
*/
|
||||
case class SchemeRejection(supported: String) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by parameter filters.
|
||||
* Signals that the request was rejected because a query parameter was not found.
|
||||
*/
|
||||
case class MissingQueryParamRejection(parameterName: String) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by parameter filters.
|
||||
* Signals that the request was rejected because a query parameter could not be interpreted.
|
||||
*/
|
||||
case class MalformedQueryParamRejection(parameterName: String, errorMsg: String,
|
||||
cause: Option[Throwable] = None) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by form field filters.
|
||||
* Signals that the request was rejected because a form field was not found.
|
||||
*/
|
||||
case class MissingFormFieldRejection(fieldName: String) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by form field filters.
|
||||
* Signals that the request was rejected because a form field could not be interpreted.
|
||||
*/
|
||||
case class MalformedFormFieldRejection(fieldName: String, errorMsg: String,
|
||||
cause: Option[Throwable] = None) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by header directives.
|
||||
* Signals that the request was rejected because a required header could not be found.
|
||||
*/
|
||||
case class MissingHeaderRejection(headerName: String) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by header directives.
|
||||
* Signals that the request was rejected because a header value is malformed.
|
||||
*/
|
||||
case class MalformedHeaderRejection(headerName: String, errorMsg: String,
|
||||
cause: Option[Throwable] = None) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by unmarshallers.
|
||||
* Signals that the request was rejected because the requests content-type is unsupported.
|
||||
*/
|
||||
case class UnsupportedRequestContentTypeRejection(supported: Set[ContentTypeRange]) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by decoding filters.
|
||||
* Signals that the request was rejected because the requests content encoding is unsupported.
|
||||
*/
|
||||
case class UnsupportedRequestEncodingRejection(supported: HttpEncoding) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by range directives.
|
||||
* Signals that the request was rejected because the requests contains only unsatisfiable ByteRanges.
|
||||
* The actualEntityLength gives the client a hint to create satisfiable ByteRanges.
|
||||
*/
|
||||
case class UnsatisfiableRangeRejection(unsatisfiableRanges: Seq[ByteRange], actualEntityLength: Long) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by range directives.
|
||||
* Signals that the request contains too many ranges. An irregular high number of ranges
|
||||
* indicates a broken client or a denial of service attack.
|
||||
*/
|
||||
case class TooManyRangesRejection(maxRanges: Int) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by unmarshallers.
|
||||
* Signals that the request was rejected because unmarshalling failed with an error that wasn't
|
||||
* an `IllegalArgumentException`. Usually that means that the request content was not of the expected format.
|
||||
* Note that semantic issues with the request content (e.g. because some parameter was out of range)
|
||||
* will usually trigger a `ValidationRejection` instead.
|
||||
*/
|
||||
case class MalformedRequestContentRejection(message: String, cause: Option[Throwable] = None) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by unmarshallers.
|
||||
* Signals that the request was rejected because an message body entity was expected but not supplied.
|
||||
*/
|
||||
case object RequestEntityExpectedRejection extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by marshallers.
|
||||
* Signals that the request was rejected because the service is not capable of producing a response entity whose
|
||||
* content type is accepted by the client
|
||||
*/
|
||||
case class UnacceptedResponseContentTypeRejection(supported: Set[ContentType]) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by encoding filters.
|
||||
* Signals that the request was rejected because the service is not capable of producing a response entity whose
|
||||
* content encoding is accepted by the client
|
||||
*/
|
||||
case class UnacceptedResponseEncodingRejection(supported: Set[HttpEncoding]) extends Rejection
|
||||
object UnacceptedResponseEncodingRejection {
|
||||
def apply(supported: HttpEncoding): UnacceptedResponseEncodingRejection = UnacceptedResponseEncodingRejection(Set(supported))
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejection created by an [[akka.http.scaladsl.server.authentication.HttpAuthenticator]].
|
||||
* Signals that the request was rejected because the user could not be authenticated. The reason for the rejection is
|
||||
* specified in the cause.
|
||||
*/
|
||||
case class AuthenticationFailedRejection(cause: AuthenticationFailedRejection.Cause,
|
||||
challenge: HttpChallenge) extends Rejection
|
||||
|
||||
object AuthenticationFailedRejection {
|
||||
/**
|
||||
* Signals the cause of the failed authentication.
|
||||
*/
|
||||
sealed trait Cause
|
||||
|
||||
/**
|
||||
* Signals the cause of the rejecting was that the user could not be authenticated, because the `WWW-Authenticate`
|
||||
* header was not supplied.
|
||||
*/
|
||||
case object CredentialsMissing extends Cause
|
||||
|
||||
/**
|
||||
* Signals the cause of the rejecting was that the user could not be authenticated, because the supplied credentials
|
||||
* are invalid.
|
||||
*/
|
||||
case object CredentialsRejected extends Cause
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejection created by the 'authorize' directive.
|
||||
* Signals that the request was rejected because the user is not authorized.
|
||||
*/
|
||||
case object AuthorizationFailedRejection extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by the `cookie` directive.
|
||||
* Signals that the request was rejected because a cookie was not found.
|
||||
*/
|
||||
case class MissingCookieRejection(cookieName: String) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created when a websocket request was expected but none was found.
|
||||
*/
|
||||
case object ExpectedWebsocketRequestRejection extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by the `validation` directive as well as for `IllegalArgumentExceptions`
|
||||
* thrown by domain model constructors (e.g. via `require`).
|
||||
* It signals that an expected value was semantically invalid.
|
||||
*/
|
||||
case class ValidationRejection(message: String, cause: Option[Throwable] = None) extends Rejection
|
||||
|
||||
/**
|
||||
* A special Rejection that serves as a container for a transformation function on rejections.
|
||||
* It is used by some directives to "cancel" rejections that are added by later directives of a similar type.
|
||||
*
|
||||
* Consider this route structure for example:
|
||||
*
|
||||
* put { reject(ValidationRejection("no") } ~ get { ... }
|
||||
*
|
||||
* If this structure is applied to a PUT request the list of rejections coming back contains three elements:
|
||||
*
|
||||
* 1. A ValidationRejection
|
||||
* 2. A MethodRejection
|
||||
* 3. A TransformationRejection holding a function filtering out the MethodRejection
|
||||
*
|
||||
* so that in the end the RejectionHandler will only see one rejection (the ValidationRejection), because the
|
||||
* MethodRejection added by the ``get`` directive is cancelled by the ``put`` directive (since the HTTP method
|
||||
* did indeed match eventually).
|
||||
*/
|
||||
case class TransformationRejection(transform: immutable.Seq[Rejection] ⇒ immutable.Seq[Rejection]) extends Rejection
|
||||
|
||||
/**
|
||||
* A Throwable wrapping a Rejection.
|
||||
* Can be used for marshalling `Future[T]` or `Try[T]` instances, whose failure side is supposed to trigger a route
|
||||
* rejection rather than an Exception that is handled by the nearest ExceptionHandler.
|
||||
* (Custom marshallers can of course use it as well.)
|
||||
*/
|
||||
case class RejectionError(rejection: Rejection) extends RuntimeException
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.reflect.ClassTag
|
||||
import scala.collection.immutable
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.scaladsl.model._
|
||||
import StatusCodes._
|
||||
import AuthenticationFailedRejection._
|
||||
|
||||
trait RejectionHandler extends (immutable.Seq[Rejection] ⇒ Option[Route]) { self ⇒
|
||||
import RejectionHandler._
|
||||
|
||||
/**
|
||||
* Creates a new [[RejectionHandler]] which uses the given one as fallback for this one.
|
||||
*/
|
||||
def withFallback(that: RejectionHandler): RejectionHandler =
|
||||
(this, that) match {
|
||||
case (a: BuiltRejectionHandler, _) if a.isDefault ⇒ this // the default handler already handles everything
|
||||
case (a: BuiltRejectionHandler, b: BuiltRejectionHandler) ⇒
|
||||
new BuiltRejectionHandler(a.cases ++ b.cases, a.notFound orElse b.notFound, b.isDefault)
|
||||
case _ ⇒ new RejectionHandler {
|
||||
def apply(rejections: immutable.Seq[Rejection]): Option[Route] =
|
||||
self(rejections) orElse that(rejections)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "Seals" this handler by attaching a default handler as fallback if necessary.
|
||||
*/
|
||||
def seal: RejectionHandler =
|
||||
this match {
|
||||
case x: BuiltRejectionHandler if x.isDefault ⇒ x
|
||||
case _ ⇒ withFallback(default)
|
||||
}
|
||||
}
|
||||
|
||||
object RejectionHandler {
|
||||
|
||||
/**
|
||||
* Creates a new [[RejectionHandler]] builder.
|
||||
*/
|
||||
def newBuilder(): Builder = new Builder(isDefault = false)
|
||||
|
||||
final class Builder private[RejectionHandler] (isDefault: Boolean) {
|
||||
private[this] val cases = new immutable.VectorBuilder[Handler]
|
||||
private[this] var notFound: Option[Route] = None
|
||||
|
||||
/**
|
||||
* Handles a single [[Rejection]] with the given partial function.
|
||||
*/
|
||||
def handle(pf: PartialFunction[Rejection, Route]): this.type = {
|
||||
cases += CaseHandler(pf)
|
||||
this
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles several Rejections of the same type at the same time.
|
||||
* The seq passed to the given function is guaranteed to be non-empty.
|
||||
*/
|
||||
def handleAll[T <: Rejection: ClassTag](f: immutable.Seq[T] ⇒ Route): this.type = {
|
||||
val runtimeClass = implicitly[ClassTag[T]].runtimeClass
|
||||
cases += TypeHandler[T](runtimeClass, f)
|
||||
this
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the special "not found" case using the given [[Route]].
|
||||
*/
|
||||
def handleNotFound(route: Route): this.type = {
|
||||
notFound = Some(route)
|
||||
this
|
||||
}
|
||||
|
||||
def result(): RejectionHandler =
|
||||
new BuiltRejectionHandler(cases.result(), notFound, isDefault)
|
||||
}
|
||||
|
||||
private sealed abstract class Handler
|
||||
private final case class CaseHandler(pf: PartialFunction[Rejection, Route]) extends Handler
|
||||
private final case class TypeHandler[T <: Rejection](
|
||||
runtimeClass: Class[_], f: immutable.Seq[T] ⇒ Route) extends Handler with PartialFunction[Rejection, T] {
|
||||
def isDefinedAt(rejection: Rejection) = runtimeClass isInstance rejection
|
||||
def apply(rejection: Rejection) = rejection.asInstanceOf[T]
|
||||
}
|
||||
|
||||
private class BuiltRejectionHandler(val cases: Vector[Handler],
|
||||
val notFound: Option[Route],
|
||||
val isDefault: Boolean) extends RejectionHandler {
|
||||
def apply(rejections: immutable.Seq[Rejection]): Option[Route] =
|
||||
if (rejections.nonEmpty) {
|
||||
@tailrec def rec(ix: Int): Option[Route] =
|
||||
if (ix < cases.length) {
|
||||
cases(ix) match {
|
||||
case CaseHandler(pf) ⇒
|
||||
val route = rejections collectFirst pf
|
||||
if (route.isEmpty) rec(ix + 1) else route
|
||||
case x @ TypeHandler(_, f) ⇒
|
||||
val rejs = rejections collect x
|
||||
if (rejs.isEmpty) rec(ix + 1) else Some(f(rejs))
|
||||
}
|
||||
} else None
|
||||
rec(0)
|
||||
} else notFound
|
||||
}
|
||||
|
||||
import Directives._
|
||||
|
||||
/**
|
||||
* Creates a new default [[RejectionHandler]] instance.
|
||||
*/
|
||||
def default =
|
||||
newBuilder()
|
||||
.handleAll[SchemeRejection] { rejections ⇒
|
||||
val schemes = rejections.map(_.supported).mkString(", ")
|
||||
complete(BadRequest, "Uri scheme not allowed, supported schemes: " + schemes)
|
||||
}
|
||||
.handleAll[MethodRejection] { rejections ⇒
|
||||
val (methods, names) = rejections.map(r ⇒ r.supported -> r.supported.name).unzip
|
||||
complete(MethodNotAllowed, List(Allow(methods)), "HTTP method not allowed, supported methods: " + names.mkString(", "))
|
||||
}
|
||||
.handle {
|
||||
case AuthorizationFailedRejection ⇒
|
||||
complete(Forbidden, "The supplied authentication is not authorized to access this resource")
|
||||
}
|
||||
.handle {
|
||||
case MalformedFormFieldRejection(name, msg, _) ⇒
|
||||
complete(BadRequest, "The form field '" + name + "' was malformed:\n" + msg)
|
||||
}
|
||||
.handle {
|
||||
case MalformedHeaderRejection(headerName, msg, _) ⇒
|
||||
complete(BadRequest, s"The value of HTTP header '$headerName' was malformed:\n" + msg)
|
||||
}
|
||||
.handle {
|
||||
case MalformedQueryParamRejection(name, msg, _) ⇒
|
||||
complete(BadRequest, "The query parameter '" + name + "' was malformed:\n" + msg)
|
||||
}
|
||||
.handle {
|
||||
case MalformedRequestContentRejection(msg, _) ⇒
|
||||
complete(BadRequest, "The request content was malformed:\n" + msg)
|
||||
}
|
||||
.handle {
|
||||
case MissingCookieRejection(cookieName) ⇒
|
||||
complete(BadRequest, "Request is missing required cookie '" + cookieName + '\'')
|
||||
}
|
||||
.handle {
|
||||
case MissingFormFieldRejection(fieldName) ⇒
|
||||
complete(BadRequest, "Request is missing required form field '" + fieldName + '\'')
|
||||
}
|
||||
.handle {
|
||||
case MissingHeaderRejection(headerName) ⇒
|
||||
complete(BadRequest, "Request is missing required HTTP header '" + headerName + '\'')
|
||||
}
|
||||
.handle {
|
||||
case MissingQueryParamRejection(paramName) ⇒
|
||||
complete(NotFound, "Request is missing required query parameter '" + paramName + '\'')
|
||||
}
|
||||
.handle {
|
||||
case RequestEntityExpectedRejection ⇒
|
||||
complete(BadRequest, "Request entity expected but not supplied")
|
||||
}
|
||||
.handle {
|
||||
case TooManyRangesRejection(_) ⇒
|
||||
complete(RequestedRangeNotSatisfiable, "Request contains too many ranges.")
|
||||
}
|
||||
.handle {
|
||||
case UnsatisfiableRangeRejection(unsatisfiableRanges, actualEntityLength) ⇒
|
||||
complete(RequestedRangeNotSatisfiable, List(`Content-Range`(ContentRange.Unsatisfiable(actualEntityLength))),
|
||||
unsatisfiableRanges.mkString("None of the following requested Ranges were satisfiable:\n", "\n", ""))
|
||||
}
|
||||
.handleAll[AuthenticationFailedRejection] { rejections ⇒
|
||||
val rejectionMessage = rejections.head.cause match {
|
||||
case CredentialsMissing ⇒ "The resource requires authentication, which was not supplied with the request"
|
||||
case CredentialsRejected ⇒ "The supplied authentication is invalid"
|
||||
}
|
||||
// Multiple challenges per WWW-Authenticate header are allowed per spec,
|
||||
// however, it seems many browsers will ignore all challenges but the first.
|
||||
// Therefore, multiple WWW-Authenticate headers are rendered, instead.
|
||||
//
|
||||
// See https://code.google.com/p/chromium/issues/detail?id=103220
|
||||
// and https://bugzilla.mozilla.org/show_bug.cgi?id=669675
|
||||
val authenticateHeaders = rejections.map(r ⇒ `WWW-Authenticate`(r.challenge))
|
||||
complete(Unauthorized, authenticateHeaders, rejectionMessage)
|
||||
}
|
||||
.handleAll[UnacceptedResponseContentTypeRejection] { rejections ⇒
|
||||
val supported = rejections.flatMap(_.supported)
|
||||
complete(NotAcceptable, "Resource representation is only available with these Content-Types:\n" +
|
||||
supported.map(_.value).mkString("\n"))
|
||||
}
|
||||
.handleAll[UnacceptedResponseEncodingRejection] { rejections ⇒
|
||||
val supported = rejections.flatMap(_.supported)
|
||||
complete(NotAcceptable, "Resource representation is only available with these Content-Encodings:\n" +
|
||||
supported.map(_.value).mkString("\n"))
|
||||
}
|
||||
.handleAll[UnsupportedRequestContentTypeRejection] { rejections ⇒
|
||||
val supported = rejections.flatMap(_.supported).mkString(" or ")
|
||||
complete(UnsupportedMediaType, "The request's Content-Type is not supported. Expected:\n" + supported)
|
||||
}
|
||||
.handleAll[UnsupportedRequestEncodingRejection] { rejections ⇒
|
||||
val supported = rejections.map(_.supported.value).mkString(" or ")
|
||||
complete(BadRequest, "The request's Content-Encoding is not supported. Expected:\n" + supported)
|
||||
}
|
||||
.handle { case ExpectedWebsocketRequestRejection ⇒ complete(BadRequest, "Expected Websocket Upgrade request") }
|
||||
.handle { case ValidationRejection(msg, _) ⇒ complete(BadRequest, msg) }
|
||||
.handle { case x ⇒ sys.error("Unhandled rejection: " + x) }
|
||||
.handleNotFound { complete(NotFound, "The requested resource could not be found.") }
|
||||
.result()
|
||||
|
||||
/**
|
||||
* Filters out all TransformationRejections from the given sequence and applies them (in order) to the
|
||||
* remaining rejections.
|
||||
*/
|
||||
def applyTransformations(rejections: immutable.Seq[Rejection]): immutable.Seq[Rejection] = {
|
||||
val (transformations, rest) = rejections.partition(_.isInstanceOf[TransformationRejection])
|
||||
(rest.distinct /: transformations.asInstanceOf[Seq[TransformationRejection]]) {
|
||||
case (remaining, transformation) ⇒ transformation.transform(remaining)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import scala.concurrent.{ Future, ExecutionContext }
|
||||
import akka.stream.FlowMaterializer
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.http.scaladsl.marshalling.ToResponseMarshallable
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
/**
|
||||
* Immutable object encapsulating the context of an [[akka.http.scaladsl.model.HttpRequest]]
|
||||
* as it flows through a akka-http Route structure.
|
||||
*/
|
||||
trait RequestContext {
|
||||
|
||||
/** The request this context represents. Modelled as a ``val`` so as to enable an ``import ctx.request._``. */
|
||||
val request: HttpRequest
|
||||
|
||||
/** The unmatched path of this context. Modelled as a ``val`` so as to enable an ``import ctx.unmatchedPath._``. */
|
||||
val unmatchedPath: Uri.Path
|
||||
|
||||
/**
|
||||
* The default ExecutionContext to be used for scheduling asynchronous logic related to this request.
|
||||
*/
|
||||
implicit def executionContext: ExecutionContext
|
||||
|
||||
/**
|
||||
* The default FlowMaterializer.
|
||||
*/
|
||||
implicit def flowMaterializer: FlowMaterializer
|
||||
|
||||
/**
|
||||
* The default LoggingAdapter to be used for logging messages related to this request.
|
||||
*/
|
||||
def log: LoggingAdapter
|
||||
|
||||
/**
|
||||
* The default RoutingSettings to be used for configuring directives.
|
||||
*/
|
||||
def settings: RoutingSettings
|
||||
|
||||
/**
|
||||
* Returns a copy of this context with the given fields updated.
|
||||
*/
|
||||
def reconfigure(
|
||||
executionContext: ExecutionContext = executionContext,
|
||||
flowMaterializer: FlowMaterializer = flowMaterializer,
|
||||
log: LoggingAdapter = log,
|
||||
settings: RoutingSettings = settings): RequestContext
|
||||
|
||||
/**
|
||||
* Completes the request with the given ToResponseMarshallable.
|
||||
*/
|
||||
def complete(obj: ToResponseMarshallable): Future[RouteResult]
|
||||
|
||||
/**
|
||||
* Rejects the request with the given rejections.
|
||||
*/
|
||||
def reject(rejections: Rejection*): Future[RouteResult]
|
||||
|
||||
/**
|
||||
* Bubbles the given error up the response chain where it is dealt with by the closest `handleExceptions`
|
||||
* directive and its ``ExceptionHandler``, unless the error is a ``RejectionError``. In this case the
|
||||
* wrapped rejection is unpacked and "executed".
|
||||
*/
|
||||
def fail(error: Throwable): Future[RouteResult]
|
||||
|
||||
/**
|
||||
* Returns a copy of this context with the new HttpRequest.
|
||||
*/
|
||||
def withRequest(req: HttpRequest): RequestContext
|
||||
|
||||
/**
|
||||
* Returns a copy of this context with the new HttpRequest.
|
||||
*/
|
||||
def withExecutionContext(ec: ExecutionContext): RequestContext
|
||||
|
||||
/**
|
||||
* Returns a copy of this context with the new HttpRequest.
|
||||
*/
|
||||
def withFlowMaterializer(materializer: FlowMaterializer): RequestContext
|
||||
|
||||
/**
|
||||
* Returns a copy of this context with the new LoggingAdapter.
|
||||
*/
|
||||
def withLog(log: LoggingAdapter): RequestContext
|
||||
|
||||
/**
|
||||
* Returns a copy of this context with the new RoutingSettings.
|
||||
*/
|
||||
def withSettings(settings: RoutingSettings): RequestContext
|
||||
|
||||
/**
|
||||
* Returns a copy of this context with the HttpRequest transformed by the given function.
|
||||
*/
|
||||
def mapRequest(f: HttpRequest ⇒ HttpRequest): RequestContext
|
||||
|
||||
/**
|
||||
* Returns a copy of this context with the unmatched path updated to the given one.
|
||||
*/
|
||||
def withUnmatchedPath(path: Uri.Path): RequestContext
|
||||
|
||||
/**
|
||||
* Returns a copy of this context with the unmatchedPath transformed by the given function.
|
||||
*/
|
||||
def mapUnmatchedPath(f: Uri.Path ⇒ Uri.Path): RequestContext
|
||||
|
||||
/**
|
||||
* Removes a potentially existing Accept header from the request headers.
|
||||
*/
|
||||
def withAcceptAll: RequestContext
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import scala.concurrent.{ Future, ExecutionContext }
|
||||
import akka.stream.FlowMaterializer
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.http.scaladsl.marshalling.{ Marshal, ToResponseMarshallable }
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] class RequestContextImpl(
|
||||
val request: HttpRequest,
|
||||
val unmatchedPath: Uri.Path,
|
||||
val executionContext: ExecutionContext,
|
||||
val flowMaterializer: FlowMaterializer,
|
||||
val log: LoggingAdapter,
|
||||
val settings: RoutingSettings) extends RequestContext {
|
||||
|
||||
def this(request: HttpRequest, log: LoggingAdapter, settings: RoutingSettings)(implicit ec: ExecutionContext, materializer: FlowMaterializer) =
|
||||
this(request, request.uri.path, ec, materializer, log, settings)
|
||||
|
||||
def reconfigure(executionContext: ExecutionContext, flowMaterializer: FlowMaterializer, log: LoggingAdapter, settings: RoutingSettings): RequestContext =
|
||||
copy(executionContext = executionContext, flowMaterializer = flowMaterializer, log = log, settings = settings)
|
||||
|
||||
override def complete(trm: ToResponseMarshallable): Future[RouteResult] =
|
||||
trm(request)(executionContext)
|
||||
.fast.map(res ⇒ RouteResult.Complete(res))(executionContext)
|
||||
.fast.recover {
|
||||
case Marshal.UnacceptableResponseContentTypeException(supported) ⇒
|
||||
RouteResult.Rejected(UnacceptedResponseContentTypeRejection(supported) :: Nil)
|
||||
case RejectionError(rej) ⇒ RouteResult.Rejected(rej :: Nil)
|
||||
}(executionContext)
|
||||
|
||||
override def reject(rejections: Rejection*): Future[RouteResult] =
|
||||
FastFuture.successful(RouteResult.Rejected(rejections.toList))
|
||||
|
||||
override def fail(error: Throwable): Future[RouteResult] =
|
||||
FastFuture.failed(error)
|
||||
|
||||
override def withRequest(request: HttpRequest): RequestContext =
|
||||
if (request != this.request) copy(request = request) else this
|
||||
|
||||
override def withExecutionContext(executionContext: ExecutionContext): RequestContext =
|
||||
if (executionContext != this.executionContext) copy(executionContext = executionContext) else this
|
||||
|
||||
override def withFlowMaterializer(flowMaterializer: FlowMaterializer): RequestContext =
|
||||
if (flowMaterializer != this.flowMaterializer) copy(flowMaterializer = flowMaterializer) else this
|
||||
|
||||
override def withLog(log: LoggingAdapter): RequestContext =
|
||||
if (log != this.log) copy(log = log) else this
|
||||
|
||||
override def withSettings(settings: RoutingSettings): RequestContext =
|
||||
if (settings != this.settings) copy(settings = settings) else this
|
||||
|
||||
override def mapRequest(f: HttpRequest ⇒ HttpRequest): RequestContext =
|
||||
copy(request = f(request))
|
||||
|
||||
override def withUnmatchedPath(path: Uri.Path): RequestContext =
|
||||
if (path != unmatchedPath) copy(unmatchedPath = path) else this
|
||||
|
||||
override def mapUnmatchedPath(f: Uri.Path ⇒ Uri.Path): RequestContext =
|
||||
copy(unmatchedPath = f(unmatchedPath))
|
||||
|
||||
override def withAcceptAll: RequestContext = request.header[headers.Accept] match {
|
||||
case Some(accept @ headers.Accept(ranges)) if !accept.acceptsAll ⇒
|
||||
mapRequest(_.mapHeaders(_.map {
|
||||
case `accept` ⇒
|
||||
val acceptAll =
|
||||
if (ranges.exists(_.isWildcard)) ranges.map(r ⇒ if (r.isWildcard) MediaRanges.`*/*;q=MIN` else r)
|
||||
else ranges :+ MediaRanges.`*/*;q=MIN`
|
||||
accept.copy(mediaRanges = acceptAll)
|
||||
case x ⇒ x
|
||||
}))
|
||||
case _ ⇒ this
|
||||
}
|
||||
|
||||
private def copy(request: HttpRequest = request,
|
||||
unmatchedPath: Uri.Path = unmatchedPath,
|
||||
executionContext: ExecutionContext = executionContext,
|
||||
flowMaterializer: FlowMaterializer = flowMaterializer,
|
||||
log: LoggingAdapter = log,
|
||||
settings: RoutingSettings = settings) =
|
||||
new RequestContextImpl(request, unmatchedPath, executionContext, flowMaterializer, log, settings)
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import scala.concurrent.Future
|
||||
import akka.stream.scaladsl.Flow
|
||||
import akka.http.scaladsl.model.{ HttpRequest, HttpResponse }
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
|
||||
object Route {
|
||||
|
||||
/**
|
||||
* Helper for constructing a Route from a function literal.
|
||||
*/
|
||||
def apply(f: Route): Route = f
|
||||
|
||||
/**
|
||||
* "Seals" a route by wrapping it with exception handling and rejection conversion.
|
||||
*/
|
||||
def seal(route: Route)(implicit setup: RoutingSetup): Route = {
|
||||
import directives.ExecutionDirectives._
|
||||
import setup._
|
||||
handleExceptions(exceptionHandler.seal(setup.settings)) {
|
||||
handleRejections(rejectionHandler.seal) {
|
||||
route
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a `Route` into a server flow.
|
||||
*/
|
||||
def handlerFlow(route: Route)(implicit setup: RoutingSetup): Flow[HttpRequest, HttpResponse, Unit] =
|
||||
Flow[HttpRequest].mapAsync(1)(asyncHandler(route))
|
||||
|
||||
/**
|
||||
* Turns a `Route` into an async handler function.
|
||||
*/
|
||||
def asyncHandler(route: Route)(implicit setup: RoutingSetup): HttpRequest ⇒ Future[HttpResponse] = {
|
||||
import setup._
|
||||
val sealedRoute = seal(route)
|
||||
request ⇒
|
||||
sealedRoute(new RequestContextImpl(request, routingLog.requestLog(request), setup.settings)).fast.map {
|
||||
case RouteResult.Complete(response) ⇒ response
|
||||
case RouteResult.Rejected(rejected) ⇒ throw new IllegalStateException(s"Unhandled rejections '$rejected', unsealed RejectionHandler?!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
|
||||
trait RouteConcatenation {
|
||||
|
||||
implicit def enhanceRouteWithConcatenation(route: Route) =
|
||||
new RouteConcatenation.RouteWithConcatenation(route: Route)
|
||||
}
|
||||
|
||||
object RouteConcatenation extends RouteConcatenation {
|
||||
|
||||
class RouteWithConcatenation(route: Route) {
|
||||
/**
|
||||
* Returns a Route that chains two Routes. If the first Route rejects the request the second route is given a
|
||||
* chance to act upon the request.
|
||||
*/
|
||||
def ~(other: Route): Route = { ctx ⇒
|
||||
import ctx.executionContext
|
||||
route(ctx).fast.flatMap {
|
||||
case x: RouteResult.Complete ⇒ FastFuture.successful(x)
|
||||
case RouteResult.Rejected(outerRejections) ⇒
|
||||
other(ctx).fast.map {
|
||||
case x: RouteResult.Complete ⇒ x
|
||||
case RouteResult.Rejected(innerRejections) ⇒ RouteResult.Rejected(outerRejections ++ innerRejections)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import scala.collection.immutable
|
||||
import akka.stream.scaladsl.Flow
|
||||
import akka.http.scaladsl.model.{ HttpRequest, HttpResponse }
|
||||
|
||||
/**
|
||||
* The result of handling a request.
|
||||
*
|
||||
* As a user you typically don't create RouteResult instances directly.
|
||||
* Instead, use the methods on the [[RequestContext]] to achieve the desired effect.
|
||||
*/
|
||||
sealed trait RouteResult
|
||||
|
||||
object RouteResult {
|
||||
final case class Complete(response: HttpResponse) extends RouteResult
|
||||
final case class Rejected(rejections: immutable.Seq[Rejection]) extends RouteResult
|
||||
|
||||
implicit def route2HandlerFlow(route: Route)(implicit setup: RoutingSetup): Flow[HttpRequest, HttpResponse, Unit] =
|
||||
Route.handlerFlow(route)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import akka.actor.ActorRefFactory
|
||||
import akka.http.impl.util._
|
||||
|
||||
case class RoutingSettings(
|
||||
verboseErrorMessages: Boolean,
|
||||
fileGetConditional: Boolean,
|
||||
renderVanityFooter: Boolean,
|
||||
rangeCountLimit: Int,
|
||||
rangeCoalescingThreshold: Long,
|
||||
decodeMaxBytesPerChunk: Int,
|
||||
fileIODispatcher: String)
|
||||
|
||||
object RoutingSettings extends SettingsCompanion[RoutingSettings]("akka.http.routing") {
|
||||
def fromSubConfig(c: Config) = apply(
|
||||
c getBoolean "verbose-error-messages",
|
||||
c getBoolean "file-get-conditional",
|
||||
c getBoolean "render-vanity-footer",
|
||||
c getInt "range-count-limit",
|
||||
c getBytes "range-coalescing-threshold",
|
||||
c getIntBytes "decode-max-bytes-per-chunk",
|
||||
c getString "file-io-dispatcher")
|
||||
|
||||
implicit def default(implicit refFactory: ActorRefFactory) =
|
||||
apply(actorSystem)
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.actor.{ ActorSystem, ActorContext }
|
||||
import akka.stream.FlowMaterializer
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.model.HttpRequest
|
||||
|
||||
/**
|
||||
* Provides a ``RoutingSetup`` for a given connection.
|
||||
*/
|
||||
trait RoutingSetupProvider {
|
||||
def apply(connection: Http.IncomingConnection): RoutingSetup
|
||||
}
|
||||
object RoutingSetupProvider {
|
||||
def apply(f: Http.IncomingConnection ⇒ RoutingSetup): RoutingSetupProvider =
|
||||
new RoutingSetupProvider {
|
||||
def apply(connection: Http.IncomingConnection) = f(connection)
|
||||
}
|
||||
|
||||
implicit def default(implicit setup: RoutingSetup) = RoutingSetupProvider(_ ⇒ setup)
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides all dependencies required for route execution.
|
||||
*/
|
||||
class RoutingSetup(
|
||||
val settings: RoutingSettings,
|
||||
val exceptionHandler: ExceptionHandler,
|
||||
val rejectionHandler: RejectionHandler,
|
||||
val executionContext: ExecutionContext,
|
||||
val flowMaterializer: FlowMaterializer,
|
||||
val routingLog: RoutingLog) {
|
||||
|
||||
// enable `import setup._` to properly bring implicits in scope
|
||||
implicit def executor: ExecutionContext = executionContext
|
||||
implicit def materializer: FlowMaterializer = flowMaterializer
|
||||
}
|
||||
|
||||
object RoutingSetup {
|
||||
implicit def apply(implicit routingSettings: RoutingSettings,
|
||||
exceptionHandler: ExceptionHandler = null,
|
||||
rejectionHandler: RejectionHandler = null,
|
||||
executionContext: ExecutionContext = null,
|
||||
flowMaterializer: FlowMaterializer,
|
||||
routingLog: RoutingLog): RoutingSetup =
|
||||
new RoutingSetup(
|
||||
routingSettings,
|
||||
if (exceptionHandler ne null) exceptionHandler else ExceptionHandler.default(routingSettings),
|
||||
if (rejectionHandler ne null) rejectionHandler else RejectionHandler.default,
|
||||
if (executionContext ne null) executionContext else flowMaterializer.executionContext,
|
||||
flowMaterializer,
|
||||
routingLog)
|
||||
}
|
||||
|
||||
trait RoutingLog {
|
||||
def log: LoggingAdapter
|
||||
def requestLog(request: HttpRequest): LoggingAdapter
|
||||
}
|
||||
|
||||
object RoutingLog extends LowerPriorityRoutingLogImplicits {
|
||||
def apply(defaultLog: LoggingAdapter): RoutingLog =
|
||||
new RoutingLog {
|
||||
def log = defaultLog
|
||||
def requestLog(request: HttpRequest) = defaultLog
|
||||
}
|
||||
|
||||
implicit def fromActorContext(implicit ac: ActorContext): RoutingLog = RoutingLog(ac.system.log)
|
||||
}
|
||||
sealed abstract class LowerPriorityRoutingLogImplicits {
|
||||
implicit def fromActorSystem(implicit system: ActorSystem): RoutingLog = RoutingLog(system.log)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.server.util.Tuple
|
||||
|
||||
/**
|
||||
* A Route that can be implicitly converted into a Directive (fitting any signature).
|
||||
*/
|
||||
abstract class StandardRoute extends Route {
|
||||
def toDirective[L: Tuple]: Directive[L] = StandardRoute.toDirective(this)
|
||||
}
|
||||
|
||||
object StandardRoute {
|
||||
def apply(route: Route): StandardRoute = route match {
|
||||
case x: StandardRoute ⇒ x
|
||||
case x ⇒ new StandardRoute { def apply(ctx: RequestContext) = x(ctx) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the StandardRoute into a directive that never passes the request to its inner route
|
||||
* (and always returns its underlying route).
|
||||
*/
|
||||
implicit def toDirective[L: Tuple](route: StandardRoute) = Directive[L] { _ ⇒ route }
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.concurrent.{ Future, ExecutionContext }
|
||||
import scala.collection.immutable
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.stream.FlowMaterializer
|
||||
import akka.http.scaladsl.server.util.Tuple
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
|
||||
trait BasicDirectives {
|
||||
|
||||
def mapInnerRoute(f: Route ⇒ Route): Directive0 =
|
||||
Directive { inner ⇒ f(inner(())) }
|
||||
|
||||
def mapRequestContext(f: RequestContext ⇒ RequestContext): Directive0 =
|
||||
mapInnerRoute { inner ⇒ ctx ⇒ inner(f(ctx)) }
|
||||
|
||||
def mapRequest(f: HttpRequest ⇒ HttpRequest): Directive0 =
|
||||
mapRequestContext(_ mapRequest f)
|
||||
|
||||
def mapRouteResultFuture(f: Future[RouteResult] ⇒ Future[RouteResult]): Directive0 =
|
||||
Directive { inner ⇒ ctx ⇒ f(inner(())(ctx)) }
|
||||
|
||||
def mapRouteResult(f: RouteResult ⇒ RouteResult): Directive0 =
|
||||
Directive { inner ⇒ ctx ⇒ inner(())(ctx).fast.map(f)(ctx.executionContext) }
|
||||
|
||||
def mapRouteResultWith(f: RouteResult ⇒ Future[RouteResult]): Directive0 =
|
||||
Directive { inner ⇒ ctx ⇒ inner(())(ctx).fast.flatMap(f)(ctx.executionContext) }
|
||||
|
||||
def mapRouteResultPF(f: PartialFunction[RouteResult, RouteResult]): Directive0 =
|
||||
mapRouteResult(f.applyOrElse(_, akka.http.impl.util.identityFunc[RouteResult]))
|
||||
|
||||
def mapRouteResultWithPF(f: PartialFunction[RouteResult, Future[RouteResult]]): Directive0 =
|
||||
mapRouteResultWith(f.applyOrElse(_, FastFuture.successful[RouteResult]))
|
||||
|
||||
def recoverRejections(f: immutable.Seq[Rejection] ⇒ RouteResult): Directive0 =
|
||||
mapRouteResultPF { case RouteResult.Rejected(rejections) ⇒ f(rejections) }
|
||||
|
||||
def recoverRejectionsWith(f: immutable.Seq[Rejection] ⇒ Future[RouteResult]): Directive0 =
|
||||
mapRouteResultWithPF { case RouteResult.Rejected(rejections) ⇒ f(rejections) }
|
||||
|
||||
def mapRejections(f: immutable.Seq[Rejection] ⇒ immutable.Seq[Rejection]): Directive0 =
|
||||
recoverRejections(rejections ⇒ RouteResult.Rejected(f(rejections)))
|
||||
|
||||
def mapResponse(f: HttpResponse ⇒ HttpResponse): Directive0 =
|
||||
mapRouteResultPF { case RouteResult.Complete(response) ⇒ RouteResult.Complete(f(response)) }
|
||||
|
||||
def mapResponseEntity(f: ResponseEntity ⇒ ResponseEntity): Directive0 =
|
||||
mapResponse(_ mapEntity f)
|
||||
|
||||
def mapResponseHeaders(f: immutable.Seq[HttpHeader] ⇒ immutable.Seq[HttpHeader]): Directive0 =
|
||||
mapResponse(_ mapHeaders f)
|
||||
|
||||
/**
|
||||
* A Directive0 that always passes the request on to its inner route
|
||||
* (i.e. does nothing with the request or the response).
|
||||
*/
|
||||
def pass: Directive0 = Directive.Empty
|
||||
|
||||
/**
|
||||
* Injects the given value into a directive.
|
||||
*/
|
||||
def provide[T](value: T): Directive1[T] = tprovide(Tuple1(value))
|
||||
|
||||
/**
|
||||
* Injects the given values into a directive.
|
||||
*/
|
||||
def tprovide[L: Tuple](values: L): Directive[L] =
|
||||
Directive { _(values) }
|
||||
|
||||
/**
|
||||
* Extracts a single value using the given function.
|
||||
*/
|
||||
def extract[T](f: RequestContext ⇒ T): Directive1[T] =
|
||||
textract(ctx ⇒ Tuple1(f(ctx)))
|
||||
|
||||
/**
|
||||
* Extracts a number of values using the given function.
|
||||
*/
|
||||
def textract[L: Tuple](f: RequestContext ⇒ L): Directive[L] =
|
||||
Directive { inner ⇒ ctx ⇒ inner(f(ctx))(ctx) }
|
||||
|
||||
/**
|
||||
* Adds a TransformationRejection cancelling all rejections equal to the given one
|
||||
* to the list of rejections potentially coming back from the inner route.
|
||||
*/
|
||||
def cancelRejection(rejection: Rejection): Directive0 =
|
||||
cancelRejections(_ == rejection)
|
||||
|
||||
/**
|
||||
* Adds a TransformationRejection cancelling all rejections of one of the given classes
|
||||
* to the list of rejections potentially coming back from the inner route.
|
||||
*/
|
||||
def cancelRejections(classes: Class[_]*): Directive0 =
|
||||
cancelRejections(r ⇒ classes.exists(_ isInstance r))
|
||||
|
||||
/**
|
||||
* Adds a TransformationRejection cancelling all rejections for which the given filter function returns true
|
||||
* to the list of rejections potentially coming back from the inner route.
|
||||
*/
|
||||
def cancelRejections(cancelFilter: Rejection ⇒ Boolean): Directive0 =
|
||||
mapRejections(_ :+ TransformationRejection(_ filterNot cancelFilter))
|
||||
|
||||
/**
|
||||
* Transforms the unmatchedPath of the RequestContext using the given function.
|
||||
*/
|
||||
def mapUnmatchedPath(f: Uri.Path ⇒ Uri.Path): Directive0 =
|
||||
mapRequestContext(_ mapUnmatchedPath f)
|
||||
|
||||
/**
|
||||
* Extracts the unmatched path from the RequestContext.
|
||||
*/
|
||||
def extractUnmatchedPath: Directive1[Uri.Path] = BasicDirectives._extractUnmatchedPath
|
||||
|
||||
/**
|
||||
* Extracts the complete request.
|
||||
*/
|
||||
def extractRequest: Directive1[HttpRequest] = BasicDirectives._extractRequest
|
||||
|
||||
/**
|
||||
* Extracts the complete request URI.
|
||||
*/
|
||||
def extractUri: Directive1[Uri] = BasicDirectives._extractUri
|
||||
|
||||
/**
|
||||
* Runs its inner route with the given alternative [[ExecutionContext]].
|
||||
*/
|
||||
def withExecutionContext(ec: ExecutionContext): Directive0 =
|
||||
mapRequestContext(_ withExecutionContext ec)
|
||||
|
||||
/**
|
||||
* Extracts the [[ExecutionContext]] from the [[RequestContext]].
|
||||
*/
|
||||
def extractExecutionContext: Directive1[ExecutionContext] = BasicDirectives._extractExecutionContext
|
||||
|
||||
/**
|
||||
* Runs its inner route with the given alternative [[FlowMaterializer]].
|
||||
*/
|
||||
def withFlowMaterializer(materializer: FlowMaterializer): Directive0 =
|
||||
mapRequestContext(_ withFlowMaterializer materializer)
|
||||
|
||||
/**
|
||||
* Extracts the [[FlowMaterializer]] from the [[RequestContext]].
|
||||
*/
|
||||
def extractFlowMaterializer: Directive1[FlowMaterializer] = BasicDirectives._extractFlowMaterializer
|
||||
|
||||
/**
|
||||
* Runs its inner route with the given alternative [[LoggingAdapter]].
|
||||
*/
|
||||
def withLog(log: LoggingAdapter): Directive0 =
|
||||
mapRequestContext(_ withLog log)
|
||||
|
||||
/**
|
||||
* Extracts the [[LoggingAdapter]] from the [[RequestContext]].
|
||||
*/
|
||||
def extractLog: Directive1[LoggingAdapter] =
|
||||
BasicDirectives._extractLog
|
||||
|
||||
/**
|
||||
* Runs its inner route with the given alternative [[RoutingSettings]].
|
||||
*/
|
||||
def withSettings(settings: RoutingSettings): Directive0 =
|
||||
mapRequestContext(_ withSettings settings)
|
||||
|
||||
/**
|
||||
* Runs the inner route with settings mapped by the given function.
|
||||
*/
|
||||
def mapSettings(f: RoutingSettings ⇒ RoutingSettings): Directive0 =
|
||||
mapRequestContext(ctx ⇒ ctx.withSettings(f(ctx.settings)))
|
||||
|
||||
/**
|
||||
* Extracts the [[RoutingSettings]] from the [[RequestContext]].
|
||||
*/
|
||||
def extractSettings: Directive1[RoutingSettings] =
|
||||
BasicDirectives._extractSettings
|
||||
|
||||
/**
|
||||
* Extracts the [[RequestContext]] itself.
|
||||
*/
|
||||
def extractRequestContext: Directive1[RequestContext] = BasicDirectives._extractRequestContext
|
||||
}
|
||||
|
||||
object BasicDirectives extends BasicDirectives {
|
||||
private val _extractUnmatchedPath: Directive1[Uri.Path] = extract(_.unmatchedPath)
|
||||
private val _extractRequest: Directive1[HttpRequest] = extract(_.request)
|
||||
private val _extractUri: Directive1[Uri] = extract(_.request.uri)
|
||||
private val _extractExecutionContext: Directive1[ExecutionContext] = extract(_.executionContext)
|
||||
private val _extractFlowMaterializer: Directive1[FlowMaterializer] = extract(_.flowMaterializer)
|
||||
private val _extractLog: Directive1[LoggingAdapter] = extract(_.log)
|
||||
private val _extractSettings: Directive1[RoutingSettings] = extract(_.settings)
|
||||
private val _extractRequestContext: Directive1[RequestContext] = extract(akka.http.impl.util.identityFunc)
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.DateTime
|
||||
import headers._
|
||||
import HttpMethods._
|
||||
import StatusCodes._
|
||||
import EntityTag._
|
||||
|
||||
trait CacheConditionDirectives {
|
||||
import BasicDirectives._
|
||||
import RouteDirectives._
|
||||
|
||||
/**
|
||||
* Wraps its inner route with support for Conditional Requests as defined
|
||||
* by http://tools.ietf.org/html/rfc7232
|
||||
*
|
||||
* In particular the algorithm defined by http://tools.ietf.org/html/rfc7232#section-6
|
||||
* is implemented by this directive.
|
||||
*
|
||||
* Note: if you want to combine this directive with `withRangeSupport(...)` you need to put
|
||||
* it on the *outside* of the `withRangeSupport(...)` directive, i.e. `withRangeSupport(...)`
|
||||
* must be on a deeper level in your route structure in order to function correctly.
|
||||
*/
|
||||
def conditional(eTag: EntityTag): Directive0 = conditional(Some(eTag), None)
|
||||
|
||||
/**
|
||||
* Wraps its inner route with support for Conditional Requests as defined
|
||||
* by http://tools.ietf.org/html/rfc7232
|
||||
*
|
||||
* In particular the algorithm defined by http://tools.ietf.org/html/rfc7232#section-6
|
||||
* is implemented by this directive.
|
||||
*
|
||||
* Note: if you want to combine this directive with `withRangeSupport(...)` you need to put
|
||||
* it on the *outside* of the `withRangeSupport(...)` directive, i.e. `withRangeSupport(...)`
|
||||
* must be on a deeper level in your route structure in order to function correctly.
|
||||
*/
|
||||
def conditional(lastModified: DateTime): Directive0 = conditional(None, Some(lastModified))
|
||||
|
||||
/**
|
||||
* Wraps its inner route with support for Conditional Requests as defined
|
||||
* by http://tools.ietf.org/html/rfc7232
|
||||
*
|
||||
* In particular the algorithm defined by http://tools.ietf.org/html/rfc7232#section-6
|
||||
* is implemented by this directive.
|
||||
*
|
||||
* Note: if you want to combine this directive with `withRangeSupport(...)` you need to put
|
||||
* it on the *outside* of the `withRangeSupport(...)` directive, i.e. `withRangeSupport(...)`
|
||||
* must be on a deeper level in your route structure in order to function correctly.
|
||||
*/
|
||||
def conditional(eTag: EntityTag, lastModified: DateTime): Directive0 = conditional(Some(eTag), Some(lastModified))
|
||||
|
||||
/**
|
||||
* Wraps its inner route with support for Conditional Requests as defined
|
||||
* by http://tools.ietf.org/html/rfc7232
|
||||
*
|
||||
* In particular the algorithm defined by http://tools.ietf.org/html/rfc7232#section-6
|
||||
* is implemented by this directive.
|
||||
*
|
||||
* Note: if you want to combine this directive with `withRangeSupport(...)` you need to put
|
||||
* it on the *outside* of the `withRangeSupport(...)` directive, i.e. `withRangeSupport(...)`
|
||||
* must be on a deeper level in your route structure in order to function correctly.
|
||||
*/
|
||||
def conditional(eTag: Option[EntityTag], lastModified: Option[DateTime]): Directive0 = {
|
||||
def addResponseHeaders: Directive0 =
|
||||
mapResponse(_.withDefaultHeaders(eTag.map(ETag(_)).toList ++ lastModified.map(`Last-Modified`(_)).toList))
|
||||
|
||||
// TODO: also handle Cache-Control and Vary
|
||||
def complete304(): Route = addResponseHeaders(complete(HttpResponse(NotModified)))
|
||||
def complete412(): Route = _.complete(PreconditionFailed)
|
||||
|
||||
extractRequest.flatMap { request ⇒
|
||||
import request._
|
||||
mapInnerRoute { route ⇒
|
||||
def innerRouteWithRangeHeaderFilteredOut: Route =
|
||||
(mapRequest(_.mapHeaders(_.filterNot(_.isInstanceOf[Range]))) &
|
||||
addResponseHeaders)(route)
|
||||
|
||||
def isGetOrHead = method == HEAD || method == GET
|
||||
def unmodified(ifModifiedSince: DateTime) =
|
||||
lastModified.get <= ifModifiedSince && ifModifiedSince.clicks < System.currentTimeMillis()
|
||||
|
||||
def step1(): Route =
|
||||
header[`If-Match`] match {
|
||||
case Some(`If-Match`(im)) if eTag.isDefined ⇒
|
||||
if (matchesRange(eTag.get, im, weakComparison = false)) step3() else complete412()
|
||||
case None ⇒ step2()
|
||||
}
|
||||
def step2(): Route =
|
||||
header[`If-Unmodified-Since`] match {
|
||||
case Some(`If-Unmodified-Since`(ius)) if lastModified.isDefined && !unmodified(ius) ⇒ complete412()
|
||||
case _ ⇒ step3()
|
||||
}
|
||||
def step3(): Route =
|
||||
header[`If-None-Match`] match {
|
||||
case Some(`If-None-Match`(inm)) if eTag.isDefined ⇒
|
||||
if (!matchesRange(eTag.get, inm, weakComparison = true)) step5()
|
||||
else if (isGetOrHead) complete304() else complete412()
|
||||
case None ⇒ step4()
|
||||
}
|
||||
def step4(): Route =
|
||||
if (isGetOrHead) {
|
||||
header[`If-Modified-Since`] match {
|
||||
case Some(`If-Modified-Since`(ims)) if lastModified.isDefined && unmodified(ims) ⇒ complete304()
|
||||
case _ ⇒ step5()
|
||||
}
|
||||
} else step5()
|
||||
def step5(): Route =
|
||||
if (method == GET && header[Range].isDefined)
|
||||
header[`If-Range`] match {
|
||||
case Some(`If-Range`(Left(tag))) if eTag.isDefined && !matches(eTag.get, tag, weakComparison = false) ⇒
|
||||
innerRouteWithRangeHeaderFilteredOut
|
||||
case Some(`If-Range`(Right(ims))) if lastModified.isDefined && !unmodified(ims) ⇒
|
||||
innerRouteWithRangeHeaderFilteredOut
|
||||
case _ ⇒ step6()
|
||||
}
|
||||
else step6()
|
||||
def step6(): Route = addResponseHeaders(route)
|
||||
|
||||
step1()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object CacheConditionDirectives extends CacheConditionDirectives
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.immutable
|
||||
import scala.util.control.NonFatal
|
||||
import akka.http.scaladsl.model.headers.{ HttpEncodings, `Accept-Encoding`, HttpEncoding, HttpEncodingRange }
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.coding._
|
||||
import akka.http.impl.util._
|
||||
|
||||
trait CodingDirectives {
|
||||
import BasicDirectives._
|
||||
import MiscDirectives._
|
||||
import RouteDirectives._
|
||||
import CodingDirectives._
|
||||
|
||||
// encoding
|
||||
|
||||
/**
|
||||
* Rejects the request with an UnacceptedResponseEncodingRejection
|
||||
* if the given encoding is not accepted for the response.
|
||||
*/
|
||||
def responseEncodingAccepted(encoding: HttpEncoding): Directive0 =
|
||||
extract(_.request.isEncodingAccepted(encoding))
|
||||
.flatMap(if (_) pass else reject(UnacceptedResponseEncodingRejection(Set(encoding))))
|
||||
|
||||
/**
|
||||
* Encodes the response with the encoding that is requested by the client with the `Accept-
|
||||
* Encoding` header. The response encoding is determined by the rules specified in
|
||||
* http://tools.ietf.org/html/rfc7231#section-5.3.4.
|
||||
*
|
||||
* If the `Accept-Encoding` header is missing or empty or specifies an encoding other than
|
||||
* identity, gzip or deflate then no encoding is used.
|
||||
*/
|
||||
def encodeResponse: Directive0 =
|
||||
encodeResponseWith(NoCoding, Gzip, Deflate)
|
||||
|
||||
/**
|
||||
* Encodes the response with the encoding that is requested by the client with the `Accept-
|
||||
* Encoding` header. The response encoding is determined by the rules specified in
|
||||
* http://tools.ietf.org/html/rfc7231#section-5.3.4.
|
||||
*
|
||||
* If the `Accept-Encoding` header is missing then the response is encoded using the `first`
|
||||
* encoder.
|
||||
*
|
||||
* If the `Accept-Encoding` header is empty and `NoCoding` is part of the encoders then no
|
||||
* response encoding is used. Otherwise the request is rejected.
|
||||
*/
|
||||
def encodeResponseWith(first: Encoder, more: Encoder*): Directive0 =
|
||||
_encodeResponse(immutable.Seq(first +: more: _*))
|
||||
|
||||
// decoding
|
||||
|
||||
/**
|
||||
* Decodes the incoming request using the given Decoder.
|
||||
* If the request encoding doesn't match the request is rejected with an `UnsupportedRequestEncodingRejection`.
|
||||
*/
|
||||
def decodeRequestWith(decoder: Decoder): Directive0 = {
|
||||
def applyDecoder =
|
||||
extractSettings flatMap { settings ⇒
|
||||
val effectiveDecoder = decoder.withMaxBytesPerChunk(settings.decodeMaxBytesPerChunk)
|
||||
mapRequest { request ⇒
|
||||
effectiveDecoder.decode(request).mapEntity(StreamUtils.mapEntityError {
|
||||
case NonFatal(e) ⇒
|
||||
IllegalRequestException(
|
||||
StatusCodes.BadRequest,
|
||||
ErrorInfo("The request's encoding is corrupt", e.getMessage))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
requestEntityEmpty | (
|
||||
requestEncodedWith(decoder.encoding) &
|
||||
applyDecoder &
|
||||
cancelRejections(classOf[UnsupportedRequestEncodingRejection]))
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects the request with an UnsupportedRequestEncodingRejection if its encoding doesn't match the given one.
|
||||
*/
|
||||
def requestEncodedWith(encoding: HttpEncoding): Directive0 =
|
||||
extract(_.request.encoding).flatMap {
|
||||
case `encoding` ⇒ pass
|
||||
case _ ⇒ reject(UnsupportedRequestEncodingRejection(encoding))
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the incoming request if it is encoded with one of the given
|
||||
* encoders. If the request encoding doesn't match one of the given encoders
|
||||
* the request is rejected with an `UnsupportedRequestEncodingRejection`.
|
||||
* If no decoders are given the default encoders (``Gzip``, ``Deflate``, ``NoCoding``) are used.
|
||||
*/
|
||||
def decodeRequestWith(decoders: Decoder*): Directive0 =
|
||||
theseOrDefault(decoders).map(decodeRequestWith).reduce(_ | _)
|
||||
|
||||
/**
|
||||
* Decompresses the incoming request if it is ``gzip`` or ``deflate`` compressed.
|
||||
* Uncompressed requests are passed through untouched.
|
||||
* If the request encoded with another encoding the request is rejected with an `UnsupportedRequestEncodingRejection`.
|
||||
*/
|
||||
def decodeRequest: Directive0 =
|
||||
decodeRequestWith(DefaultCoders: _*)
|
||||
}
|
||||
|
||||
object CodingDirectives extends CodingDirectives {
|
||||
val DefaultCoders: immutable.Seq[Coder] = immutable.Seq(Gzip, Deflate, NoCoding)
|
||||
|
||||
def theseOrDefault[T >: Coder](these: Seq[T]): Seq[T] = if (these.isEmpty) DefaultCoders else these
|
||||
|
||||
import BasicDirectives._
|
||||
import HeaderDirectives._
|
||||
import RouteDirectives._
|
||||
|
||||
private def _encodeResponse(encoders: immutable.Seq[Encoder]): Directive0 =
|
||||
optionalHeaderValueByType(classOf[`Accept-Encoding`]) flatMap { accept ⇒
|
||||
val acceptedEncoder = accept match {
|
||||
case None ⇒
|
||||
// use first defined encoder when Accept-Encoding is missing
|
||||
encoders.headOption
|
||||
case Some(`Accept-Encoding`(encodings)) ⇒
|
||||
// provide fallback to identity
|
||||
val withIdentity =
|
||||
if (encodings.exists {
|
||||
case HttpEncodingRange.One(HttpEncodings.identity, _) ⇒ true
|
||||
case _ ⇒ false
|
||||
}) encodings
|
||||
else encodings :+ HttpEncodings.`identity;q=MIN`
|
||||
// sort client-accepted encodings by q-Value (and orig. order) and find first matching encoder
|
||||
@tailrec def find(encodings: List[HttpEncodingRange]): Option[Encoder] = encodings match {
|
||||
case encoding :: rest ⇒
|
||||
encoders.find(e ⇒ encoding.matches(e.encoding)) match {
|
||||
case None ⇒ find(rest)
|
||||
case x ⇒ x
|
||||
}
|
||||
case _ ⇒ None
|
||||
}
|
||||
find(withIdentity.sortBy(e ⇒ (-e.qValue, withIdentity.indexOf(e))).toList)
|
||||
}
|
||||
acceptedEncoder match {
|
||||
case Some(encoder) ⇒ mapResponse(encoder.encode(_))
|
||||
case _ ⇒ reject(UnacceptedResponseEncodingRejection(encoders.map(_.encoding).toSet))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import headers._
|
||||
import akka.http.impl.util._
|
||||
|
||||
trait CookieDirectives {
|
||||
import HeaderDirectives._
|
||||
import RespondWithDirectives._
|
||||
import RouteDirectives._
|
||||
|
||||
/**
|
||||
* Extracts an HttpCookie with the given name. If the cookie is not present the
|
||||
* request is rejected with a respective [[MissingCookieRejection]].
|
||||
*/
|
||||
def cookie(name: String): Directive1[HttpCookie] =
|
||||
headerValue(findCookie(name)) | reject(MissingCookieRejection(name))
|
||||
|
||||
/**
|
||||
* Extracts an HttpCookie with the given name.
|
||||
* If the cookie is not present a value of `None` is extracted.
|
||||
*/
|
||||
def optionalCookie(name: String): Directive1[Option[HttpCookie]] =
|
||||
optionalHeaderValue(findCookie(name))
|
||||
|
||||
private def findCookie(name: String): HttpHeader ⇒ Option[HttpCookie] = {
|
||||
case Cookie(cookies) ⇒ cookies.find(_.name == name)
|
||||
case _ ⇒ None
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Set-Cookie header with the given cookies to all responses of its inner route.
|
||||
*/
|
||||
def setCookie(first: HttpCookie, more: HttpCookie*): Directive0 =
|
||||
respondWithHeaders((first :: more.toList).map(`Set-Cookie`(_)))
|
||||
|
||||
/**
|
||||
* Adds a Set-Cookie header expiring the given cookies to all responses of its inner route.
|
||||
*/
|
||||
def deleteCookie(first: HttpCookie, more: HttpCookie*): Directive0 =
|
||||
respondWithHeaders((first :: more.toList).map { c ⇒
|
||||
`Set-Cookie`(c.copy(content = "deleted", expires = Some(DateTime.MinValue)))
|
||||
})
|
||||
|
||||
/**
|
||||
* Adds a Set-Cookie header expiring the given cookie to all responses of its inner route.
|
||||
*/
|
||||
def deleteCookie(name: String, domain: String = "", path: String = ""): Directive0 =
|
||||
deleteCookie(HttpCookie(name, "", domain = domain.toOption, path = path.toOption))
|
||||
|
||||
}
|
||||
|
||||
object CookieDirectives extends CookieDirectives
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.event.Logging._
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
trait DebuggingDirectives {
|
||||
import BasicDirectives._
|
||||
|
||||
def logRequest(magnet: LoggingMagnet[HttpRequest ⇒ Unit]): Directive0 =
|
||||
extractRequestContext.flatMap { ctx ⇒
|
||||
magnet.f(ctx.log)(ctx.request)
|
||||
pass
|
||||
}
|
||||
|
||||
def logResult(magnet: LoggingMagnet[RouteResult ⇒ Unit]): Directive0 =
|
||||
extractRequestContext.flatMap { ctx ⇒
|
||||
mapRouteResult { result ⇒
|
||||
magnet.f(ctx.log)(result)
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
def logRequestResult(magnet: LoggingMagnet[HttpRequest ⇒ RouteResult ⇒ Unit]): Directive0 =
|
||||
extractRequestContext.flatMap { ctx ⇒
|
||||
val logResult = magnet.f(ctx.log)(ctx.request)
|
||||
mapRouteResult { result ⇒
|
||||
logResult(result)
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object DebuggingDirectives extends DebuggingDirectives
|
||||
|
||||
case class LoggingMagnet[T](f: LoggingAdapter ⇒ T) // # logging-magnet
|
||||
|
||||
object LoggingMagnet {
|
||||
implicit def forMessageFromMarker[T](marker: String) = // # message-magnets
|
||||
forMessageFromMarkerAndLevel[T](marker -> DebugLevel)
|
||||
|
||||
implicit def forMessageFromMarkerAndLevel[T](markerAndLevel: (String, LogLevel)) = // # message-magnets
|
||||
forMessageFromFullShow[T] {
|
||||
val (marker, level) = markerAndLevel
|
||||
Message ⇒ LogEntry(Message, marker, level)
|
||||
}
|
||||
|
||||
implicit def forMessageFromShow[T](show: T ⇒ String) = // # message-magnets
|
||||
forMessageFromFullShow[T](msg ⇒ LogEntry(show(msg), DebugLevel))
|
||||
|
||||
implicit def forMessageFromFullShow[T](show: T ⇒ LogEntry): LoggingMagnet[T ⇒ Unit] = // # message-magnets
|
||||
LoggingMagnet(log ⇒ show(_).logTo(log))
|
||||
|
||||
implicit def forRequestResponseFromMarker(marker: String) = // # request-response-magnets
|
||||
forRequestResponseFromMarkerAndLevel(marker -> DebugLevel)
|
||||
|
||||
implicit def forRequestResponseFromMarkerAndLevel(markerAndLevel: (String, LogLevel)) = // # request-response-magnets
|
||||
forRequestResponseFromFullShow {
|
||||
val (marker, level) = markerAndLevel
|
||||
request ⇒ response ⇒ Some(
|
||||
LogEntry("Response for\n Request : " + request + "\n Response: " + response, marker, level))
|
||||
}
|
||||
|
||||
implicit def forRequestResponseFromFullShow(show: HttpRequest ⇒ RouteResult ⇒ Option[LogEntry]): LoggingMagnet[HttpRequest ⇒ RouteResult ⇒ Unit] = // # request-response-magnets
|
||||
LoggingMagnet { log ⇒
|
||||
request ⇒
|
||||
val showResult = show(request)
|
||||
result ⇒ showResult(result).foreach(_.logTo(log))
|
||||
}
|
||||
}
|
||||
|
||||
case class LogEntry(obj: Any, level: LogLevel = DebugLevel) {
|
||||
def logTo(log: LoggingAdapter): Unit = {
|
||||
log.log(level, obj.toString)
|
||||
}
|
||||
}
|
||||
|
||||
object LogEntry {
|
||||
def apply(obj: Any, marker: String, level: LogLevel): LogEntry =
|
||||
LogEntry(if (marker.isEmpty) obj else marker + ": " + obj, level)
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.collection.immutable
|
||||
import scala.concurrent.Future
|
||||
import scala.util.control.NonFatal
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
|
||||
trait ExecutionDirectives {
|
||||
import BasicDirectives._
|
||||
|
||||
/**
|
||||
* Transforms exceptions thrown during evaluation of its inner route using the given
|
||||
* [[akka.http.scaladsl.server.ExceptionHandler]].
|
||||
*/
|
||||
def handleExceptions(handler: ExceptionHandler): Directive0 =
|
||||
Directive { innerRouteBuilder ⇒
|
||||
ctx ⇒
|
||||
import ctx.executionContext
|
||||
def handleException: PartialFunction[Throwable, Future[RouteResult]] =
|
||||
handler andThen (_(ctx.withAcceptAll))
|
||||
try innerRouteBuilder(())(ctx).fast.recoverWith(handleException)
|
||||
catch {
|
||||
case NonFatal(e) ⇒ handleException.applyOrElse[Throwable, Future[RouteResult]](e, throw _)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms rejections produced by its inner route using the given
|
||||
* [[akka.http.scaladsl.server.RejectionHandler]].
|
||||
*/
|
||||
def handleRejections(handler: RejectionHandler): Directive0 =
|
||||
extractRequestContext flatMap { ctx ⇒
|
||||
val maxIterations = 8
|
||||
// allow for up to `maxIterations` nested rejections from RejectionHandler before bailing out
|
||||
def handle(rejections: immutable.Seq[Rejection], originalRejections: immutable.Seq[Rejection], iterationsLeft: Int = maxIterations): Future[RouteResult] =
|
||||
if (iterationsLeft > 0) {
|
||||
handler(rejections) match {
|
||||
case Some(route) ⇒ recoverRejectionsWith(handle(_, originalRejections, iterationsLeft - 1))(route)(ctx.withAcceptAll)
|
||||
case None ⇒ FastFuture.successful(RouteResult.Rejected(rejections))
|
||||
}
|
||||
} else
|
||||
sys.error(s"Rejection handler still produced new rejections after $maxIterations iterations. " +
|
||||
s"Is there an infinite handler cycle? Initial rejections: $originalRejections final rejections: $rejections")
|
||||
|
||||
recoverRejectionsWith { rejections ⇒
|
||||
val transformed = RejectionHandler.applyTransformations(rejections)
|
||||
handle(transformed, transformed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ExecutionDirectives extends ExecutionDirectives
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import java.io.{ File, FileInputStream }
|
||||
import java.net.URL
|
||||
|
||||
import scala.annotation.tailrec
|
||||
import akka.actor.ActorSystem
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.http.scaladsl.marshalling.{ Marshaller, ToEntityMarshaller }
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.impl.util._
|
||||
|
||||
trait FileAndResourceDirectives {
|
||||
import CacheConditionDirectives._
|
||||
import MethodDirectives._
|
||||
import FileAndResourceDirectives._
|
||||
import RouteDirectives._
|
||||
import BasicDirectives._
|
||||
import RouteConcatenation._
|
||||
import RangeDirectives._
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content of the given file. The actual I/O operation is
|
||||
* running detached in a `Future`, so it doesn't block the current thread (but potentially
|
||||
* some other thread !). If the file cannot be found or read the request is rejected.
|
||||
*/
|
||||
def getFromFile(fileName: String)(implicit resolver: ContentTypeResolver): Route =
|
||||
getFromFile(new File(fileName))
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content of the given file. The actual I/O operation is
|
||||
* running detached in a `Future`, so it doesn't block the current thread (but potentially
|
||||
* some other thread !). If the file cannot be found or read the request is rejected.
|
||||
*/
|
||||
def getFromFile(file: File)(implicit resolver: ContentTypeResolver): Route =
|
||||
getFromFile(file, resolver(file.getName))
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content of the given file. The actual I/O operation is
|
||||
* running detached in a `Future`, so it doesn't block the current thread (but potentially
|
||||
* some other thread !). If the file cannot be found or read the request is rejected.
|
||||
*/
|
||||
def getFromFile(file: File, contentType: ContentType): Route =
|
||||
get {
|
||||
if (file.isFile && file.canRead)
|
||||
conditionalFor(file.length, file.lastModified) {
|
||||
withRangeSupport {
|
||||
extractSettings { settings ⇒
|
||||
complete {
|
||||
HttpEntity.Default(contentType, file.length,
|
||||
StreamUtils.fromInputStreamSource(new FileInputStream(file), settings.fileIODispatcher))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else reject
|
||||
}
|
||||
|
||||
private def conditionalFor(length: Long, lastModified: Long): Directive0 =
|
||||
extractSettings.flatMap(settings ⇒
|
||||
if (settings.fileGetConditional) {
|
||||
val tag = java.lang.Long.toHexString(lastModified ^ java.lang.Long.reverse(length))
|
||||
val lastModifiedDateTime = DateTime(math.min(lastModified, System.currentTimeMillis))
|
||||
conditional(EntityTag(tag), lastModifiedDateTime)
|
||||
} else pass)
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content of the given resource. The actual I/O operation is
|
||||
* running detached in a `Future`, so it doesn't block the current thread (but potentially
|
||||
* some other thread !).
|
||||
* If the resource cannot be found or read the Route rejects the request.
|
||||
*/
|
||||
def getFromResource(resourceName: String)(implicit resolver: ContentTypeResolver): Route =
|
||||
getFromResource(resourceName, resolver(resourceName))
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content of the given resource. The actual I/O operation is
|
||||
* running detached in a `Future`, so it doesn't block the current thread (but potentially
|
||||
* some other thread !).
|
||||
* If the resource is a directory or cannot be found or read the Route rejects the request.
|
||||
*/
|
||||
def getFromResource(resourceName: String, contentType: ContentType, classLoader: ClassLoader = defaultClassLoader): Route =
|
||||
if (!resourceName.endsWith("/"))
|
||||
get {
|
||||
Option(classLoader.getResource(resourceName)) flatMap ResourceFile.apply match {
|
||||
case Some(ResourceFile(url, length, lastModified)) ⇒
|
||||
conditionalFor(length, lastModified) {
|
||||
withRangeSupport {
|
||||
extractSettings { settings ⇒
|
||||
complete {
|
||||
HttpEntity.Default(contentType, length,
|
||||
StreamUtils.fromInputStreamSource(url.openStream(), settings.fileIODispatcher))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case _ ⇒ reject // not found or directory
|
||||
}
|
||||
}
|
||||
else reject // don't serve the content of resource "directories"
|
||||
|
||||
/**
|
||||
* Completes GET requests with the content of a file underneath the given directory.
|
||||
* If the file cannot be read the Route rejects the request.
|
||||
*/
|
||||
def getFromDirectory(directoryName: String)(implicit resolver: ContentTypeResolver): Route = {
|
||||
val base = withTrailingSlash(directoryName)
|
||||
extractUnmatchedPath { path ⇒
|
||||
extractLog { log ⇒
|
||||
fileSystemPath(base, path, log) match {
|
||||
case "" ⇒ reject
|
||||
case fileName ⇒ getFromFile(fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes GET requests with a unified listing of the contents of all given directories.
|
||||
* The actual rendering of the directory contents is performed by the in-scope `Marshaller[DirectoryListing]`.
|
||||
*/
|
||||
def listDirectoryContents(directories: String*)(implicit renderer: DirectoryRenderer): Route =
|
||||
get {
|
||||
extractRequestContext { ctx ⇒
|
||||
val path = ctx.unmatchedPath
|
||||
val fullPath = ctx.request.uri.path.toString
|
||||
val matchedLength = fullPath.lastIndexOf(path.toString)
|
||||
require(matchedLength >= 0)
|
||||
val pathPrefix = fullPath.substring(0, matchedLength)
|
||||
val pathString = withTrailingSlash(fileSystemPath("/", path, ctx.log, '/'))
|
||||
val dirs = directories flatMap { dir ⇒
|
||||
fileSystemPath(withTrailingSlash(dir), path, ctx.log) match {
|
||||
case "" ⇒ None
|
||||
case fileName ⇒
|
||||
val file = new File(fileName)
|
||||
if (file.isDirectory && file.canRead) Some(file) else None
|
||||
}
|
||||
}
|
||||
implicit val marshaller: ToEntityMarshaller[DirectoryListing] = renderer.marshaller(ctx.settings.renderVanityFooter)
|
||||
|
||||
if (dirs.isEmpty) reject
|
||||
else complete(DirectoryListing(pathPrefix + pathString, isRoot = pathString == "/", dirs.flatMap(_.listFiles)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `getFromBrowseableDirectories` with only one directory.
|
||||
*/
|
||||
def getFromBrowseableDirectory(directory: String)(implicit renderer: DirectoryRenderer, resolver: ContentTypeResolver): Route =
|
||||
getFromBrowseableDirectories(directory)
|
||||
|
||||
/**
|
||||
* Serves the content of the given directories as a file system browser, i.e. files are sent and directories
|
||||
* served as browseable listings.
|
||||
*/
|
||||
def getFromBrowseableDirectories(directories: String*)(implicit renderer: DirectoryRenderer, resolver: ContentTypeResolver): Route = {
|
||||
directories.map(getFromDirectory).reduceLeft(_ ~ _) ~ listDirectoryContents(directories: _*)
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as "getFromDirectory" except that the file is not fetched from the file system but rather from a
|
||||
* "resource directory".
|
||||
* If the requested resource is itself a directory or cannot be found or read the Route rejects the request.
|
||||
*/
|
||||
def getFromResourceDirectory(directoryName: String, classLoader: ClassLoader = defaultClassLoader)(implicit resolver: ContentTypeResolver): Route = {
|
||||
val base = if (directoryName.isEmpty) "" else withTrailingSlash(directoryName)
|
||||
|
||||
extractUnmatchedPath { path ⇒
|
||||
extractLog { log ⇒
|
||||
fileSystemPath(base, path, log, separator = '/') match {
|
||||
case "" ⇒ reject
|
||||
case resourceName ⇒ getFromResource(resourceName, resolver(resourceName), classLoader)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected[http] def defaultClassLoader: ClassLoader = classOf[ActorSystem].getClassLoader
|
||||
}
|
||||
|
||||
object FileAndResourceDirectives extends FileAndResourceDirectives {
|
||||
private def withTrailingSlash(path: String): String = if (path endsWith "/") path else path + '/'
|
||||
private def fileSystemPath(base: String, path: Uri.Path, log: LoggingAdapter, separator: Char = File.separatorChar): String = {
|
||||
import java.lang.StringBuilder
|
||||
@tailrec def rec(p: Uri.Path, result: StringBuilder = new StringBuilder(base)): String =
|
||||
p match {
|
||||
case Uri.Path.Empty ⇒ result.toString
|
||||
case Uri.Path.Slash(tail) ⇒ rec(tail, result.append(separator))
|
||||
case Uri.Path.Segment(head, tail) ⇒
|
||||
if (head.indexOf('/') >= 0 || head == "..") {
|
||||
log.warning("File-system path for base [{}] and Uri.Path [{}] contains suspicious path segment [{}], " +
|
||||
"GET access was disallowed", base, path, head)
|
||||
""
|
||||
} else rec(tail, result.append(head))
|
||||
}
|
||||
rec(if (path.startsWithSlash) path.tail else path)
|
||||
}
|
||||
|
||||
object ResourceFile {
|
||||
def apply(url: URL): Option[ResourceFile] = url.getProtocol match {
|
||||
case "file" ⇒
|
||||
val file = new File(url.toURI)
|
||||
if (file.isDirectory) None
|
||||
else Some(ResourceFile(url, file.length(), file.lastModified()))
|
||||
case "jar" ⇒
|
||||
val jarFile = url.getFile
|
||||
val startIndex = if (jarFile.startsWith("file:")) 5 else 0
|
||||
val bangIndex = jarFile.indexOf("!")
|
||||
val jarFilePath = jarFile.substring(startIndex, bangIndex)
|
||||
val resourcePath = jarFile.substring(bangIndex + 2)
|
||||
val jar = new java.util.zip.ZipFile(jarFilePath)
|
||||
try {
|
||||
val entry = jar.getEntry(resourcePath)
|
||||
Option(jar.getInputStream(entry)) map { is ⇒
|
||||
is.close()
|
||||
ResourceFile(url, entry.getSize, entry.getTime)
|
||||
}
|
||||
} finally jar.close()
|
||||
case _ ⇒ None
|
||||
}
|
||||
}
|
||||
case class ResourceFile(url: URL, length: Long, lastModified: Long)
|
||||
|
||||
trait DirectoryRenderer {
|
||||
def marshaller(renderVanityFooter: Boolean): ToEntityMarshaller[DirectoryListing]
|
||||
}
|
||||
trait LowLevelDirectoryRenderer {
|
||||
implicit def defaultDirectoryRenderer: DirectoryRenderer =
|
||||
new DirectoryRenderer {
|
||||
def marshaller(renderVanityFooter: Boolean): ToEntityMarshaller[DirectoryListing] =
|
||||
DirectoryListing.directoryMarshaller(renderVanityFooter)
|
||||
}
|
||||
}
|
||||
object DirectoryRenderer extends LowLevelDirectoryRenderer {
|
||||
implicit def liftMarshaller(implicit _marshaller: ToEntityMarshaller[DirectoryListing]): DirectoryRenderer =
|
||||
new DirectoryRenderer {
|
||||
def marshaller(renderVanityFooter: Boolean): ToEntityMarshaller[DirectoryListing] = _marshaller
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait ContentTypeResolver {
|
||||
def apply(fileName: String): ContentType
|
||||
}
|
||||
|
||||
object ContentTypeResolver {
|
||||
|
||||
/**
|
||||
* The default way of resolving a filename to a ContentType is by looking up the file extension in the
|
||||
* registry of all defined media-types. By default all non-binary file content is assumed to be UTF-8 encoded.
|
||||
*/
|
||||
implicit val Default = withDefaultCharset(HttpCharsets.`UTF-8`)
|
||||
|
||||
def withDefaultCharset(charset: HttpCharset): ContentTypeResolver =
|
||||
new ContentTypeResolver {
|
||||
def apply(fileName: String) = {
|
||||
val ext = fileName.lastIndexOf('.') match {
|
||||
case -1 ⇒ ""
|
||||
case x ⇒ fileName.substring(x + 1)
|
||||
}
|
||||
val mediaType = MediaTypes.forExtension(ext) getOrElse MediaTypes.`application/octet-stream`
|
||||
ContentType(mediaType) withDefaultCharset charset
|
||||
}
|
||||
}
|
||||
|
||||
def apply(f: String ⇒ ContentType): ContentTypeResolver =
|
||||
new ContentTypeResolver {
|
||||
def apply(fileName: String): ContentType = f(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
case class DirectoryListing(path: String, isRoot: Boolean, files: Seq[File])
|
||||
|
||||
object DirectoryListing {
|
||||
|
||||
private val html =
|
||||
"""<html>
|
||||
|<head><title>Index of $</title></head>
|
||||
|<body>
|
||||
|<h1>Index of $</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|$</pre>
|
||||
|<hr>$
|
||||
|<div style="width:100%;text-align:right;color:gray">
|
||||
|<small>rendered by <a href="http://akka.io">Akka Http</a> on $</small>
|
||||
|</div>$
|
||||
|</body>
|
||||
|</html>
|
||||
|""".stripMarginWithNewline("\n") split '$'
|
||||
|
||||
def directoryMarshaller(renderVanityFooter: Boolean): ToEntityMarshaller[DirectoryListing] =
|
||||
Marshaller.StringMarshaller.wrapWithEC(MediaTypes.`text/html`) { implicit ec ⇒
|
||||
listing ⇒
|
||||
val DirectoryListing(path, isRoot, files) = listing
|
||||
val filesAndNames = files.map(file ⇒ file -> file.getName).sortBy(_._2)
|
||||
val deduped = filesAndNames.zipWithIndex.flatMap {
|
||||
case (fan @ (file, name), ix) ⇒
|
||||
if (ix == 0 || filesAndNames(ix - 1)._2 != name) Some(fan) else None
|
||||
}
|
||||
val (directoryFilesAndNames, fileFilesAndNames) = deduped.partition(_._1.isDirectory)
|
||||
def maxNameLength(seq: Seq[(File, String)]) = if (seq.isEmpty) 0 else seq.map(_._2.length).max
|
||||
val maxNameLen = math.max(maxNameLength(directoryFilesAndNames) + 1, maxNameLength(fileFilesAndNames))
|
||||
val sb = new java.lang.StringBuilder
|
||||
sb.append(html(0)).append(path).append(html(1)).append(path).append(html(2))
|
||||
if (!isRoot) {
|
||||
val secondToLastSlash = path.lastIndexOf('/', path.lastIndexOf('/', path.length - 1) - 1)
|
||||
sb.append("<a href=\"%s/\">../</a>\n" format path.substring(0, secondToLastSlash))
|
||||
}
|
||||
def lastModified(file: File) = DateTime(file.lastModified).toIsoLikeDateTimeString
|
||||
def start(name: String) =
|
||||
sb.append("<a href=\"").append(path + name).append("\">").append(name).append("</a>")
|
||||
.append(" " * (maxNameLen - name.length))
|
||||
def renderDirectory(file: File, name: String) =
|
||||
start(name + '/').append(" ").append(lastModified(file)).append('\n')
|
||||
def renderFile(file: File, name: String) = {
|
||||
val size = akka.http.impl.util.humanReadableByteCount(file.length, si = true)
|
||||
start(name).append(" ").append(lastModified(file))
|
||||
sb.append(" ".substring(size.length)).append(size).append('\n')
|
||||
}
|
||||
for ((file, name) ← directoryFilesAndNames) renderDirectory(file, name)
|
||||
for ((file, name) ← fileFilesAndNames) renderFile(file, name)
|
||||
if (isRoot && files.isEmpty) sb.append("(no files)\n")
|
||||
sb.append(html(3))
|
||||
if (renderVanityFooter) sb.append(html(4)).append(DateTime.now.toIsoLikeDateTimeString).append(html(5))
|
||||
sb.append(html(6)).toString
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{ Failure, Success }
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException
|
||||
import akka.http.scaladsl.common._
|
||||
import akka.http.impl.util._
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
|
||||
trait FormFieldDirectives extends ToNameReceptacleEnhancements {
|
||||
import FormFieldDirectives._
|
||||
|
||||
/**
|
||||
* Rejects the request if the defined form field matcher(s) don't match.
|
||||
* Otherwise the form field value(s) are extracted and passed to the inner route.
|
||||
*/
|
||||
def formField(pdm: FieldMagnet): pdm.Out = pdm()
|
||||
|
||||
/**
|
||||
* Rejects the request if the defined form field matcher(s) don't match.
|
||||
* Otherwise the form field value(s) are extracted and passed to the inner route.
|
||||
*/
|
||||
def formFields(pdm: FieldMagnet): pdm.Out = pdm()
|
||||
|
||||
}
|
||||
|
||||
object FormFieldDirectives extends FormFieldDirectives {
|
||||
sealed trait FieldMagnet {
|
||||
type Out
|
||||
def apply(): Out
|
||||
}
|
||||
object FieldMagnet {
|
||||
implicit def apply[T](value: T)(implicit fdef: FieldDef[T]) =
|
||||
new FieldMagnet {
|
||||
type Out = fdef.Out
|
||||
def apply() = fdef(value)
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait FieldDef[T] {
|
||||
type Out
|
||||
def apply(value: T): Out
|
||||
}
|
||||
|
||||
object FieldDef {
|
||||
def fieldDef[A, B](f: A ⇒ B) =
|
||||
new FieldDef[A] {
|
||||
type Out = B
|
||||
def apply(value: A) = f(value)
|
||||
}
|
||||
|
||||
import akka.http.scaladsl.unmarshalling.{ FromStrictFormFieldUnmarshaller ⇒ FSFFU, _ }
|
||||
import BasicDirectives._
|
||||
import RouteDirectives._
|
||||
import FutureDirectives._
|
||||
type SFU = FromEntityUnmarshaller[StrictForm]
|
||||
type FSFFOU[T] = Unmarshaller[Option[StrictForm.Field], T]
|
||||
|
||||
//////////////////// "regular" formField extraction ////////////////////
|
||||
|
||||
private def extractField[A, B](f: A ⇒ Directive1[B]) = fieldDef(f)
|
||||
private def fieldOfForm[T](fieldName: String, fu: Unmarshaller[Option[StrictForm.Field], T])(implicit sfu: SFU): RequestContext ⇒ Future[T] = { ctx ⇒
|
||||
import ctx.executionContext
|
||||
sfu(ctx.request.entity).fast.flatMap(form ⇒ fu(form field fieldName))
|
||||
}
|
||||
private def filter[T](fieldName: String, fu: FSFFOU[T])(implicit sfu: SFU): Directive1[T] = {
|
||||
extract(fieldOfForm(fieldName, fu)).flatMap {
|
||||
onComplete(_).flatMap {
|
||||
case Success(x) ⇒ provide(x)
|
||||
case Failure(Unmarshaller.NoContentException) ⇒ reject(MissingFormFieldRejection(fieldName))
|
||||
case Failure(x: UnsupportedContentTypeException) ⇒ reject(UnsupportedRequestContentTypeRejection(x.supported))
|
||||
case Failure(x) ⇒ reject(MalformedFormFieldRejection(fieldName, x.getMessage.nullAsEmpty, Option(x.getCause)))
|
||||
}
|
||||
}
|
||||
}
|
||||
implicit def forString(implicit sfu: SFU, fu: FSFFU[String]) =
|
||||
extractField[String, String] { fieldName ⇒ filter(fieldName, fu) }
|
||||
implicit def forSymbol(implicit sfu: SFU, fu: FSFFU[String]) =
|
||||
extractField[Symbol, String] { symbol ⇒ filter(symbol.name, fu) }
|
||||
implicit def forNR[T](implicit sfu: SFU, fu: FSFFU[T]) =
|
||||
extractField[NameReceptacle[T], T] { nr ⇒ filter(nr.name, fu) }
|
||||
implicit def forNUR[T](implicit sfu: SFU) =
|
||||
extractField[NameUnmarshallerReceptacle[T], T] { nr ⇒ filter(nr.name, StrictForm.Field.unmarshallerFromFSU(nr.um)) }
|
||||
implicit def forNOR[T](implicit sfu: SFU, fu: FSFFOU[T]) =
|
||||
extractField[NameOptionReceptacle[T], Option[T]] { nr ⇒ filter[Option[T]](nr.name, fu) }
|
||||
implicit def forNDR[T](implicit sfu: SFU, fu: FSFFOU[T]) =
|
||||
extractField[NameDefaultReceptacle[T], T] { nr ⇒ filter(nr.name, fu withDefaultValue nr.default) }
|
||||
implicit def forNOUR[T](implicit sfu: SFU) =
|
||||
extractField[NameOptionUnmarshallerReceptacle[T], Option[T]] { nr ⇒ filter[Option[T]](nr.name, StrictForm.Field.unmarshallerFromFSU(nr.um): FSFFOU[T]) }
|
||||
implicit def forNDUR[T](implicit sfu: SFU) =
|
||||
extractField[NameDefaultUnmarshallerReceptacle[T], T] { nr ⇒ filter(nr.name, (StrictForm.Field.unmarshallerFromFSU(nr.um): FSFFOU[T]) withDefaultValue nr.default) }
|
||||
|
||||
//////////////////// required formField support ////////////////////
|
||||
|
||||
private def requiredFilter[T](fieldName: String, fu: Unmarshaller[Option[StrictForm.Field], T],
|
||||
requiredValue: Any)(implicit sfu: SFU): Directive0 =
|
||||
extract(fieldOfForm(fieldName, fu)).flatMap {
|
||||
onComplete(_).flatMap {
|
||||
case Success(value) if value == requiredValue ⇒ pass
|
||||
case _ ⇒ reject
|
||||
}
|
||||
}
|
||||
implicit def forRVR[T](implicit sfu: SFU, fu: FSFFU[T]) =
|
||||
fieldDef[RequiredValueReceptacle[T], Directive0] { rvr ⇒ requiredFilter(rvr.name, fu, rvr.requiredValue) }
|
||||
implicit def forRVDR[T](implicit sfu: SFU) =
|
||||
fieldDef[RequiredValueUnmarshallerReceptacle[T], Directive0] { rvr ⇒ requiredFilter(rvr.name, StrictForm.Field.unmarshallerFromFSU(rvr.um), rvr.requiredValue) }
|
||||
|
||||
//////////////////// tuple support ////////////////////
|
||||
|
||||
import akka.http.scaladsl.server.util.TupleOps._
|
||||
import akka.http.scaladsl.server.util.BinaryPolyFunc
|
||||
|
||||
implicit def forTuple[T](implicit fold: FoldLeft[Directive0, T, ConvertParamDefAndConcatenate.type]) =
|
||||
fieldDef[T, fold.Out](fold(pass, _))
|
||||
|
||||
object ConvertParamDefAndConcatenate extends BinaryPolyFunc {
|
||||
implicit def from[P, TA, TB](implicit fdef: FieldDef[P] { type Out = Directive[TB] }, ev: Join[TA, TB]) =
|
||||
at[Directive[TA], P] { (a, t) ⇒ a & fdef(t) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{ Failure, Success, Try }
|
||||
import akka.http.scaladsl.marshalling.ToResponseMarshaller
|
||||
import akka.http.scaladsl.server.util.Tupler
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
|
||||
// format: OFF
|
||||
|
||||
trait FutureDirectives {
|
||||
|
||||
/**
|
||||
* "Unwraps" a ``Future[T]`` and runs its inner route after future
|
||||
* completion with the future's value as an extraction of type ``Try[T]``.
|
||||
*/
|
||||
def onComplete[T](future: ⇒ Future[T]): Directive1[Try[T]] =
|
||||
Directive { inner ⇒ ctx ⇒
|
||||
import ctx.executionContext
|
||||
future.fast.transformWith(t ⇒ inner(Tuple1(t))(ctx))
|
||||
}
|
||||
|
||||
/**
|
||||
* "Unwraps" a ``Future[T]`` and runs its inner route after future
|
||||
* completion with the future's value as an extraction of type ``T``.
|
||||
* If the future fails its failure Throwable is bubbled up to the nearest
|
||||
* ExceptionHandler.
|
||||
* If type ``T`` is already a Tuple it is directly expanded into the respective
|
||||
* number of extractions.
|
||||
*/
|
||||
def onSuccess(magnet: OnSuccessMagnet): Directive[magnet.Out] = magnet.directive
|
||||
|
||||
/**
|
||||
* "Unwraps" a ``Future[T]`` and runs its inner route when the future has failed
|
||||
* with the future's failure exception as an extraction of type ``Throwable``.
|
||||
* If the future succeeds the request is completed using the values marshaller
|
||||
* (This directive therefore requires a marshaller for the futures type to be
|
||||
* implicitly available.)
|
||||
*/
|
||||
def completeOrRecoverWith(magnet: CompleteOrRecoverWithMagnet): Directive1[Throwable] = magnet.directive
|
||||
}
|
||||
|
||||
object FutureDirectives extends FutureDirectives
|
||||
|
||||
trait OnSuccessMagnet {
|
||||
type Out
|
||||
def directive: Directive[Out]
|
||||
}
|
||||
|
||||
object OnSuccessMagnet {
|
||||
implicit def apply[T](future: ⇒ Future[T])(implicit tupler: Tupler[T]) =
|
||||
new OnSuccessMagnet {
|
||||
type Out = tupler.Out
|
||||
val directive = Directive[tupler.Out] { inner ⇒ ctx ⇒
|
||||
import ctx.executionContext
|
||||
future.fast.flatMap(t ⇒ inner(tupler(t))(ctx))
|
||||
}(tupler.OutIsTuple)
|
||||
}
|
||||
}
|
||||
|
||||
trait CompleteOrRecoverWithMagnet {
|
||||
def directive: Directive1[Throwable]
|
||||
}
|
||||
|
||||
object CompleteOrRecoverWithMagnet {
|
||||
implicit def apply[T](future: ⇒ Future[T])(implicit m: ToResponseMarshaller[T]) =
|
||||
new CompleteOrRecoverWithMagnet {
|
||||
val directive = Directive[Tuple1[Throwable]] { inner ⇒ ctx ⇒
|
||||
import ctx.executionContext
|
||||
future.fast.transformWith {
|
||||
case Success(res) ⇒ ctx.complete(res)
|
||||
case Failure(error) ⇒ inner(Tuple1(error))(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.util.control.NonFatal
|
||||
import akka.http.scaladsl.server.util.ClassMagnet
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.impl.util._
|
||||
|
||||
trait HeaderDirectives {
|
||||
import BasicDirectives._
|
||||
import RouteDirectives._
|
||||
|
||||
/**
|
||||
* Extracts an HTTP header value using the given function. If the function result is undefined for all headers the
|
||||
* request is rejected with an empty rejection set. If the given function throws an exception the request is rejected
|
||||
* with a [[spray.routing.MalformedHeaderRejection]].
|
||||
*/
|
||||
def headerValue[T](f: HttpHeader ⇒ Option[T]): Directive1[T] = {
|
||||
val protectedF: HttpHeader ⇒ Option[Either[Rejection, T]] = header ⇒
|
||||
try f(header).map(Right.apply)
|
||||
catch {
|
||||
case NonFatal(e) ⇒ Some(Left(MalformedHeaderRejection(header.name, e.getMessage.nullAsEmpty, Some(e))))
|
||||
}
|
||||
|
||||
extract(_.request.headers.collectFirst(Function.unlift(protectedF))).flatMap {
|
||||
case Some(Right(a)) ⇒ provide(a)
|
||||
case Some(Left(rejection)) ⇒ reject(rejection)
|
||||
case None ⇒ reject
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts an HTTP header value using the given partial function. If the function is undefined for all headers the
|
||||
* request is rejected with an empty rejection set.
|
||||
*/
|
||||
def headerValuePF[T](pf: PartialFunction[HttpHeader, T]): Directive1[T] = headerValue(pf.lift)
|
||||
|
||||
/**
|
||||
* Extracts the value of the HTTP request header with the given name.
|
||||
* If no header with a matching name is found the request is rejected with a [[spray.routing.MissingHeaderRejection]].
|
||||
*/
|
||||
def headerValueByName(headerName: Symbol): Directive1[String] = headerValueByName(headerName.toString)
|
||||
|
||||
/**
|
||||
* Extracts the value of the HTTP request header with the given name.
|
||||
* If no header with a matching name is found the request is rejected with a [[spray.routing.MissingHeaderRejection]].
|
||||
*/
|
||||
def headerValueByName(headerName: String): Directive1[String] =
|
||||
headerValue(optionalValue(headerName.toLowerCase)) | reject(MissingHeaderRejection(headerName))
|
||||
|
||||
/**
|
||||
* Extracts the HTTP request header of the given type.
|
||||
* If no header with a matching type is found the request is rejected with a [[spray.routing.MissingHeaderRejection]].
|
||||
*/
|
||||
def headerValueByType[T <: HttpHeader](magnet: ClassMagnet[T]): Directive1[T] =
|
||||
headerValuePF(magnet.extractPF) | reject(MissingHeaderRejection(magnet.runtimeClass.getSimpleName))
|
||||
|
||||
/**
|
||||
* Extracts an optional HTTP header value using the given function.
|
||||
* If the given function throws an exception the request is rejected
|
||||
* with a [[spray.routing.MalformedHeaderRejection]].
|
||||
*/
|
||||
def optionalHeaderValue[T](f: HttpHeader ⇒ Option[T]): Directive1[Option[T]] =
|
||||
headerValue(f).map(Some(_): Option[T]).recoverPF {
|
||||
case Nil ⇒ provide(None)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts an optional HTTP header value using the given partial function.
|
||||
* If the given function throws an exception the request is rejected
|
||||
* with a [[spray.routing.MalformedHeaderRejection]].
|
||||
*/
|
||||
def optionalHeaderValuePF[T](pf: PartialFunction[HttpHeader, T]): Directive1[Option[T]] =
|
||||
optionalHeaderValue(pf.lift)
|
||||
|
||||
/**
|
||||
* Extracts the value of the optional HTTP request header with the given name.
|
||||
*/
|
||||
def optionalHeaderValueByName(headerName: Symbol): Directive1[Option[String]] =
|
||||
optionalHeaderValueByName(headerName.toString)
|
||||
|
||||
/**
|
||||
* Extracts the value of the optional HTTP request header with the given name.
|
||||
*/
|
||||
def optionalHeaderValueByName(headerName: String): Directive1[Option[String]] = {
|
||||
val lowerCaseName = headerName.toLowerCase
|
||||
extract(_.request.headers.collectFirst {
|
||||
case HttpHeader(`lowerCaseName`, value) ⇒ value
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the header value of the optional HTTP request header with the given type.
|
||||
*/
|
||||
def optionalHeaderValueByType[T <: HttpHeader](magnet: ClassMagnet[T]): Directive1[Option[T]] =
|
||||
optionalHeaderValuePF(magnet.extractPF)
|
||||
|
||||
private def optionalValue(lowerCaseName: String): HttpHeader ⇒ Option[String] = {
|
||||
case HttpHeader(`lowerCaseName`, value) ⇒ Some(value)
|
||||
case _ ⇒ None
|
||||
}
|
||||
}
|
||||
|
||||
object HeaderDirectives extends HeaderDirectives
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.util.matching.Regex
|
||||
import akka.http.impl.util._
|
||||
|
||||
trait HostDirectives {
|
||||
import BasicDirectives._
|
||||
import RouteDirectives._
|
||||
|
||||
/**
|
||||
* Extracts the hostname part of the Host header value in the request.
|
||||
*/
|
||||
def extractHost: Directive1[String] = HostDirectives._extractHost
|
||||
|
||||
/**
|
||||
* Rejects all requests with a host name different from the given ones.
|
||||
*/
|
||||
def host(hostNames: String*): Directive0 = host(hostNames.contains(_))
|
||||
|
||||
/**
|
||||
* Rejects all requests for whose host name the given predicate function returns false.
|
||||
*/
|
||||
def host(predicate: String ⇒ Boolean): Directive0 = extractHost.require(predicate)
|
||||
|
||||
/**
|
||||
* Rejects all requests with a host name that doesn't have a prefix matching the given regular expression.
|
||||
* For all matching requests the prefix string matching the regex is extracted and passed to the inner route.
|
||||
* If the regex contains a capturing group only the string matched by this group is extracted.
|
||||
* If the regex contains more than one capturing group an IllegalArgumentException is thrown.
|
||||
*/
|
||||
def host(regex: Regex): Directive1[String] = {
|
||||
def forFunc(regexMatch: String ⇒ Option[String]): Directive1[String] = {
|
||||
extractHost.flatMap { name ⇒
|
||||
regexMatch(name) match {
|
||||
case Some(matched) ⇒ provide(matched)
|
||||
case None ⇒ reject
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
regex.groupCount match {
|
||||
case 0 ⇒ forFunc(regex.findPrefixOf(_))
|
||||
case 1 ⇒ forFunc(regex.findPrefixMatchOf(_).map(_.group(1)))
|
||||
case _ ⇒ throw new IllegalArgumentException("Path regex '" + regex.pattern.pattern +
|
||||
"' must not contain more than one capturing group")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object HostDirectives extends HostDirectives {
|
||||
import BasicDirectives._
|
||||
|
||||
private val _extractHost: Directive1[String] =
|
||||
extract(_.request.uri.authority.host.address)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.concurrent.Promise
|
||||
import scala.util.{ Failure, Success }
|
||||
import akka.http.scaladsl.marshalling.ToResponseMarshaller
|
||||
import akka.http.scaladsl.unmarshalling.{ Unmarshaller, FromRequestUnmarshaller }
|
||||
import akka.http.impl.util._
|
||||
|
||||
trait MarshallingDirectives {
|
||||
import BasicDirectives._
|
||||
import FutureDirectives._
|
||||
import RouteDirectives._
|
||||
|
||||
/**
|
||||
* Unmarshalls the requests entity to the given type passes it to its inner Route.
|
||||
* If there is a problem with unmarshalling the request is rejected with the [[Rejection]]
|
||||
* produced by the unmarshaller.
|
||||
*/
|
||||
def entity[T](um: FromRequestUnmarshaller[T]): Directive1[T] =
|
||||
extractRequestContext.flatMap[Tuple1[T]] { ctx ⇒
|
||||
import ctx.executionContext
|
||||
onComplete(um(ctx.request)) flatMap {
|
||||
case Success(value) ⇒ provide(value)
|
||||
case Failure(Unmarshaller.NoContentException) ⇒ reject(RequestEntityExpectedRejection)
|
||||
case Failure(Unmarshaller.UnsupportedContentTypeException(x)) ⇒ reject(UnsupportedRequestContentTypeRejection(x))
|
||||
case Failure(x: IllegalArgumentException) ⇒ reject(ValidationRejection(x.getMessage.nullAsEmpty, Some(x)))
|
||||
case Failure(x) ⇒ reject(MalformedRequestContentRejection(x.getMessage.nullAsEmpty, Option(x.getCause)))
|
||||
}
|
||||
} & cancelRejections(RequestEntityExpectedRejection.getClass, classOf[UnsupportedRequestContentTypeRejection])
|
||||
|
||||
/**
|
||||
* Returns the in-scope [[FromRequestUnmarshaller]] for the given type.
|
||||
*/
|
||||
def as[T](implicit um: FromRequestUnmarshaller[T]) = um
|
||||
|
||||
/**
|
||||
* Uses the marshaller for the given type to produce a completion function that is passed to its inner function.
|
||||
* You can use it do decouple marshaller resolution from request completion.
|
||||
*/
|
||||
def completeWith[T](marshaller: ToResponseMarshaller[T])(inner: (T ⇒ Unit) ⇒ Unit): Route =
|
||||
extractExecutionContext { implicit ec ⇒
|
||||
implicit val m = marshaller
|
||||
complete {
|
||||
val promise = Promise[T]()
|
||||
inner(promise.success(_))
|
||||
promise.future
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the in-scope Marshaller for the given type.
|
||||
*/
|
||||
def instanceOf[T](implicit m: ToResponseMarshaller[T]): ToResponseMarshaller[T] = m
|
||||
|
||||
/**
|
||||
* Completes the request using the given function. The input to the function is produced with the in-scope
|
||||
* entity unmarshaller and the result value of the function is marshalled with the in-scope marshaller.
|
||||
*/
|
||||
def handleWith[A, B](f: A ⇒ B)(implicit um: FromRequestUnmarshaller[A], m: ToResponseMarshaller[B]): Route =
|
||||
entity(um) { a ⇒ complete(f(a)) }
|
||||
}
|
||||
|
||||
object MarshallingDirectives extends MarshallingDirectives
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model.{ StatusCodes, HttpMethod }
|
||||
import akka.http.scaladsl.model.HttpMethods._
|
||||
|
||||
trait MethodDirectives {
|
||||
import BasicDirectives._
|
||||
import RouteDirectives._
|
||||
import ParameterDirectives._
|
||||
import MethodDirectives._
|
||||
|
||||
/**
|
||||
* A route filter that rejects all non-DELETE requests.
|
||||
*/
|
||||
def delete: Directive0 = _delete
|
||||
|
||||
/**
|
||||
* A route filter that rejects all non-GET requests.
|
||||
*/
|
||||
def get: Directive0 = _get
|
||||
|
||||
/**
|
||||
* A route filter that rejects all non-HEAD requests.
|
||||
*/
|
||||
def head: Directive0 = _head
|
||||
|
||||
/**
|
||||
* A route filter that rejects all non-OPTIONS requests.
|
||||
*/
|
||||
def options: Directive0 = _options
|
||||
|
||||
/**
|
||||
* A route filter that rejects all non-PATCH requests.
|
||||
*/
|
||||
def patch: Directive0 = _patch
|
||||
|
||||
/**
|
||||
* A route filter that rejects all non-POST requests.
|
||||
*/
|
||||
def post: Directive0 = _post
|
||||
|
||||
/**
|
||||
* A route filter that rejects all non-PUT requests.
|
||||
*/
|
||||
def put: Directive0 = _put
|
||||
|
||||
/**
|
||||
* Extracts the request method.
|
||||
*/
|
||||
def extractMethod: Directive1[HttpMethod] = _extractMethod
|
||||
|
||||
/**
|
||||
* Rejects all requests whose HTTP method does not match the given one.
|
||||
*/
|
||||
def method(httpMethod: HttpMethod): Directive0 =
|
||||
extractMethod.flatMap[Unit] {
|
||||
case `httpMethod` ⇒ pass
|
||||
case _ ⇒ reject(MethodRejection(httpMethod))
|
||||
} & cancelRejections(classOf[MethodRejection])
|
||||
|
||||
/**
|
||||
* Changes the HTTP method of the request to the value of the specified query string parameter. If the query string
|
||||
* parameter is not specified this directive has no effect. If the query string is specified as something that is not
|
||||
* a HTTP method, then this directive completes the request with a `501 Not Implemented` response.
|
||||
*
|
||||
* This directive is useful for:
|
||||
* - Use in combination with JSONP (JSONP only supports GET)
|
||||
* - Supporting older browsers that lack support for certain HTTP methods. E.g. IE8 does not support PATCH
|
||||
*/
|
||||
def overrideMethodWithParameter(paramName: String): Directive0 =
|
||||
parameter(paramName?) flatMap {
|
||||
case Some(method) ⇒
|
||||
getForKey(method.toUpperCase) match {
|
||||
case Some(m) ⇒ mapRequest(_.copy(method = m))
|
||||
case _ ⇒ complete(StatusCodes.NotImplemented)
|
||||
}
|
||||
case None ⇒ pass
|
||||
}
|
||||
}
|
||||
|
||||
object MethodDirectives extends MethodDirectives {
|
||||
private val _extractMethod: Directive1[HttpMethod] =
|
||||
BasicDirectives.extract(_.request.method)
|
||||
|
||||
// format: OFF
|
||||
private val _delete : Directive0 = method(DELETE)
|
||||
private val _get : Directive0 = method(GET)
|
||||
private val _head : Directive0 = method(HEAD)
|
||||
private val _options: Directive0 = method(OPTIONS)
|
||||
private val _patch : Directive0 = method(PATCH)
|
||||
private val _post : Directive0 = method(POST)
|
||||
private val _put : Directive0 = method(PUT)
|
||||
// format: ON
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import headers._
|
||||
|
||||
trait MiscDirectives {
|
||||
import RouteDirectives._
|
||||
|
||||
/**
|
||||
* Returns a Directive which checks the given condition before passing on the [[spray.routing.RequestContext]] to
|
||||
* its inner Route. If the condition fails the route is rejected with a [[spray.routing.ValidationRejection]].
|
||||
*/
|
||||
def validate(check: ⇒ Boolean, errorMsg: String): Directive0 =
|
||||
Directive { inner ⇒ if (check) inner() else reject(ValidationRejection(errorMsg)) }
|
||||
|
||||
/**
|
||||
* Directive extracting the IP of the client from either the X-Forwarded-For, Remote-Address or X-Real-IP header
|
||||
* (in that order of priority).
|
||||
*/
|
||||
def extractClientIP: Directive1[RemoteAddress] = MiscDirectives._extractClientIP
|
||||
|
||||
/**
|
||||
* Rejects the request if its entity is not empty.
|
||||
*/
|
||||
def requestEntityEmpty: Directive0 = MiscDirectives._requestEntityEmpty
|
||||
|
||||
/**
|
||||
* Rejects empty requests with a RequestEntityExpectedRejection.
|
||||
* Non-empty requests are passed on unchanged to the inner route.
|
||||
*/
|
||||
def requestEntityPresent: Directive0 = MiscDirectives._requestEntityPresent
|
||||
|
||||
/**
|
||||
* Converts responses with an empty entity into (empty) rejections.
|
||||
* This way you can, for example, have the marshalling of a ''None'' option be treated as if the request could
|
||||
* not be matched.
|
||||
*/
|
||||
def rejectEmptyResponse: Directive0 = MiscDirectives._rejectEmptyResponse
|
||||
}
|
||||
|
||||
object MiscDirectives extends MiscDirectives {
|
||||
import BasicDirectives._
|
||||
import HeaderDirectives._
|
||||
import RouteDirectives._
|
||||
import RouteResult._
|
||||
|
||||
private val _extractClientIP: Directive1[RemoteAddress] =
|
||||
headerValuePF { case `X-Forwarded-For`(Seq(address, _*)) ⇒ address } |
|
||||
headerValuePF { case `Remote-Address`(address) ⇒ address } |
|
||||
headerValuePF { case h if h.is("x-real-ip") ⇒ RemoteAddress(h.value) }
|
||||
|
||||
private val _requestEntityEmpty: Directive0 =
|
||||
extract(_.request.entity.isKnownEmpty).flatMap(if (_) pass else reject)
|
||||
|
||||
private val _requestEntityPresent: Directive0 =
|
||||
extract(_.request.entity.isKnownEmpty).flatMap(if (_) reject else pass)
|
||||
|
||||
private val _rejectEmptyResponse: Directive0 =
|
||||
mapRouteResult {
|
||||
case Complete(response) if response.entity.isKnownEmpty ⇒ Rejected(Nil)
|
||||
case x ⇒ x
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.collection.immutable
|
||||
import scala.util.{ Failure, Success }
|
||||
import akka.http.scaladsl.common._
|
||||
import akka.http.impl.util._
|
||||
|
||||
trait ParameterDirectives extends ToNameReceptacleEnhancements {
|
||||
import ParameterDirectives._
|
||||
|
||||
/**
|
||||
* Extracts the requests query parameters as a Map[String, String].
|
||||
*/
|
||||
def parameterMap: Directive1[Map[String, String]] = _parameterMap
|
||||
|
||||
/**
|
||||
* Extracts the requests query parameters as a Map[String, List[String]].
|
||||
*/
|
||||
def parameterMultiMap: Directive1[Map[String, List[String]]] = _parameterMultiMap
|
||||
|
||||
/**
|
||||
* Extracts the requests query parameters as a Seq[(String, String)].
|
||||
*/
|
||||
def parameterSeq: Directive1[immutable.Seq[(String, String)]] = _parameterSeq
|
||||
|
||||
/**
|
||||
* Rejects the request if the defined query parameter matcher(s) don't match.
|
||||
* Otherwise the parameter value(s) are extracted and passed to the inner route.
|
||||
*/
|
||||
def parameter(pdm: ParamMagnet): pdm.Out = pdm()
|
||||
|
||||
/**
|
||||
* Rejects the request if the defined query parameter matcher(s) don't match.
|
||||
* Otherwise the parameter value(s) are extracted and passed to the inner route.
|
||||
*/
|
||||
def parameters(pdm: ParamMagnet): pdm.Out = pdm()
|
||||
|
||||
}
|
||||
|
||||
object ParameterDirectives extends ParameterDirectives {
|
||||
import BasicDirectives._
|
||||
|
||||
private val _parameterMap: Directive1[Map[String, String]] =
|
||||
extract(_.request.uri.query.toMap)
|
||||
|
||||
private val _parameterMultiMap: Directive1[Map[String, List[String]]] =
|
||||
extract(_.request.uri.query.toMultiMap)
|
||||
|
||||
private val _parameterSeq: Directive1[immutable.Seq[(String, String)]] =
|
||||
extract(_.request.uri.query.toSeq)
|
||||
|
||||
sealed trait ParamMagnet {
|
||||
type Out
|
||||
def apply(): Out
|
||||
}
|
||||
object ParamMagnet {
|
||||
implicit def apply[T](value: T)(implicit pdef: ParamDef[T]) =
|
||||
new ParamMagnet {
|
||||
type Out = pdef.Out
|
||||
def apply() = pdef(value)
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait ParamDef[T] {
|
||||
type Out
|
||||
def apply(value: T): Out
|
||||
}
|
||||
object ParamDef {
|
||||
def paramDef[A, B](f: A ⇒ B) =
|
||||
new ParamDef[A] {
|
||||
type Out = B
|
||||
def apply(value: A) = f(value)
|
||||
}
|
||||
|
||||
import akka.http.scaladsl.unmarshalling.{ FromStringUnmarshaller ⇒ FSU, _ }
|
||||
import BasicDirectives._
|
||||
import RouteDirectives._
|
||||
import FutureDirectives._
|
||||
type FSOU[T] = Unmarshaller[Option[String], T]
|
||||
|
||||
//////////////////// "regular" parameter extraction //////////////////////
|
||||
|
||||
private def extractParameter[A, B](f: A ⇒ Directive1[B]) = paramDef(f)
|
||||
private def filter[T](paramName: String, fsou: FSOU[T]): Directive1[T] =
|
||||
extractRequestContext flatMap { ctx ⇒
|
||||
import ctx.executionContext
|
||||
onComplete(fsou(ctx.request.uri.query get paramName)) flatMap {
|
||||
case Success(x) ⇒ provide(x)
|
||||
case Failure(Unmarshaller.NoContentException) ⇒ reject(MissingQueryParamRejection(paramName))
|
||||
case Failure(x) ⇒ reject(MalformedQueryParamRejection(paramName, x.getMessage.nullAsEmpty, Option(x.getCause)))
|
||||
}
|
||||
}
|
||||
implicit def forString(implicit fsu: FSU[String]) =
|
||||
extractParameter[String, String] { string ⇒ filter(string, fsu) }
|
||||
implicit def forSymbol(implicit fsu: FSU[String]) =
|
||||
extractParameter[Symbol, String] { symbol ⇒ filter(symbol.name, fsu) }
|
||||
implicit def forNR[T](implicit fsu: FSU[T]) =
|
||||
extractParameter[NameReceptacle[T], T] { nr ⇒ filter(nr.name, fsu) }
|
||||
implicit def forNUR[T] =
|
||||
extractParameter[NameUnmarshallerReceptacle[T], T] { nr ⇒ filter(nr.name, nr.um) }
|
||||
implicit def forNOR[T](implicit fsou: FSOU[T]) =
|
||||
extractParameter[NameOptionReceptacle[T], Option[T]] { nr ⇒ filter[Option[T]](nr.name, fsou) }
|
||||
implicit def forNDR[T](implicit fsou: FSOU[T]) =
|
||||
extractParameter[NameDefaultReceptacle[T], T] { nr ⇒ filter[T](nr.name, fsou withDefaultValue nr.default) }
|
||||
implicit def forNOUR[T] =
|
||||
extractParameter[NameOptionUnmarshallerReceptacle[T], Option[T]] { nr ⇒ filter(nr.name, nr.um: FSOU[T]) }
|
||||
implicit def forNDUR[T] =
|
||||
extractParameter[NameDefaultUnmarshallerReceptacle[T], T] { nr ⇒ filter[T](nr.name, (nr.um: FSOU[T]) withDefaultValue nr.default) }
|
||||
|
||||
//////////////////// required parameter support ////////////////////
|
||||
|
||||
private def requiredFilter[T](paramName: String, fsou: FSOU[T], requiredValue: Any): Directive0 =
|
||||
extractRequestContext flatMap { ctx ⇒
|
||||
import ctx.executionContext
|
||||
onComplete(fsou(ctx.request.uri.query get paramName)) flatMap {
|
||||
case Success(value) if value == requiredValue ⇒ pass
|
||||
case _ ⇒ reject
|
||||
}
|
||||
}
|
||||
implicit def forRVR[T](implicit fsu: FSU[T]) =
|
||||
paramDef[RequiredValueReceptacle[T], Directive0] { rvr ⇒ requiredFilter(rvr.name, fsu, rvr.requiredValue) }
|
||||
implicit def forRVDR[T] =
|
||||
paramDef[RequiredValueUnmarshallerReceptacle[T], Directive0] { rvr ⇒ requiredFilter(rvr.name, rvr.um, rvr.requiredValue) }
|
||||
|
||||
//////////////////// tuple support ////////////////////
|
||||
|
||||
import akka.http.scaladsl.server.util.TupleOps._
|
||||
import akka.http.scaladsl.server.util.BinaryPolyFunc
|
||||
|
||||
implicit def forTuple[T](implicit fold: FoldLeft[Directive0, T, ConvertParamDefAndConcatenate.type]) =
|
||||
paramDef[T, fold.Out](fold(BasicDirectives.pass, _))
|
||||
|
||||
object ConvertParamDefAndConcatenate extends BinaryPolyFunc {
|
||||
implicit def from[P, TA, TB](implicit pdef: ParamDef[P] { type Out = Directive[TB] }, ev: Join[TA, TB]) =
|
||||
at[Directive[TA], P] { (a, t) ⇒ a & pdef(t) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.common.ToNameReceptacleEnhancements
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
import akka.http.scaladsl.model.Uri.Path
|
||||
|
||||
trait PathDirectives extends PathMatchers with ImplicitPathMatcherConstruction with ToNameReceptacleEnhancements {
|
||||
import BasicDirectives._
|
||||
import RouteDirectives._
|
||||
import PathMatcher._
|
||||
|
||||
/**
|
||||
* Consumes a leading slash from the unmatched path of the [[akka.http.scaladsl.server.RequestContext]]
|
||||
* before applying the given matcher. The matcher has to match the remaining path completely
|
||||
* or leave only a single trailing slash.
|
||||
* If matched the value extracted by the PathMatcher is extracted on the directive level.
|
||||
*/
|
||||
def path[L](pm: PathMatcher[L]): Directive[L] = pathPrefix(pm ~ PathEnd)
|
||||
|
||||
/**
|
||||
* Consumes a leading slash from the unmatched path of the [[akka.http.scaladsl.server.RequestContext]]
|
||||
* before applying the given matcher. The matcher has to match a prefix of the remaining path.
|
||||
* If matched the value extracted by the PathMatcher is extracted on the directive level.
|
||||
*/
|
||||
def pathPrefix[L](pm: PathMatcher[L]): Directive[L] = rawPathPrefix(Slash ~ pm)
|
||||
|
||||
/**
|
||||
* Applies the given matcher directly to the unmatched path of the [[akka.http.scaladsl.server.RequestContext]]
|
||||
* (i.e. without implicitly consuming a leading slash).
|
||||
* The matcher has to match a prefix of the remaining path.
|
||||
* If matched the value extracted by the PathMatcher is extracted on the directive level.
|
||||
*/
|
||||
def rawPathPrefix[L](pm: PathMatcher[L]): Directive[L] = {
|
||||
implicit def LIsTuple = pm.ev
|
||||
extract(ctx ⇒ pm(ctx.unmatchedPath)).flatMap {
|
||||
case Matched(rest, values) ⇒ tprovide(values) & mapRequestContext(_ withUnmatchedPath rest)
|
||||
case Unmatched ⇒ reject
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the unmatchedPath of the [[akka.http.scaladsl.server.RequestContext]] has a prefix matched by the
|
||||
* given PathMatcher. In analogy to the `pathPrefix` directive a leading slash is implied.
|
||||
*/
|
||||
def pathPrefixTest[L](pm: PathMatcher[L]): Directive[L] = rawPathPrefixTest(Slash ~ pm)
|
||||
|
||||
/**
|
||||
* Checks whether the unmatchedPath of the [[akka.http.scaladsl.server.RequestContext]] has a prefix matched by the
|
||||
* given PathMatcher. However, as opposed to the `pathPrefix` directive the matched path is not
|
||||
* actually "consumed".
|
||||
*/
|
||||
def rawPathPrefixTest[L](pm: PathMatcher[L]): Directive[L] = {
|
||||
implicit def LIsTuple = pm.ev
|
||||
extract(ctx ⇒ pm(ctx.unmatchedPath)).flatMap {
|
||||
case Matched(_, values) ⇒ tprovide(values)
|
||||
case Unmatched ⇒ reject
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects the request if the unmatchedPath of the [[akka.http.scaladsl.server.RequestContext]] does not have a suffix
|
||||
* matched the given PathMatcher. If matched the value extracted by the PathMatcher is extracted
|
||||
* and the matched parts of the path are consumed.
|
||||
* Note that, for efficiency reasons, the given PathMatcher must match the desired suffix in reversed-segment
|
||||
* order, i.e. `pathSuffix("baz" / "bar")` would match `/foo/bar/baz`!
|
||||
*/
|
||||
def pathSuffix[L](pm: PathMatcher[L]): Directive[L] = {
|
||||
implicit def LIsTuple = pm.ev
|
||||
extract(ctx ⇒ pm(ctx.unmatchedPath.reverse)).flatMap {
|
||||
case Matched(rest, values) ⇒ tprovide(values) & mapRequestContext(_.withUnmatchedPath(rest.reverse))
|
||||
case Unmatched ⇒ reject
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the unmatchedPath of the [[akka.http.scaladsl.server.RequestContext]] has a suffix matched by the
|
||||
* given PathMatcher. However, as opposed to the pathSuffix directive the matched path is not
|
||||
* actually "consumed".
|
||||
* Note that, for efficiency reasons, the given PathMatcher must match the desired suffix in reversed-segment
|
||||
* order, i.e. `pathSuffixTest("baz" / "bar")` would match `/foo/bar/baz`!
|
||||
*/
|
||||
def pathSuffixTest[L](pm: PathMatcher[L]): Directive[L] = {
|
||||
implicit def LIsTuple = pm.ev
|
||||
extract(ctx ⇒ pm(ctx.unmatchedPath.reverse)).flatMap {
|
||||
case Matched(_, values) ⇒ tprovide(values)
|
||||
case Unmatched ⇒ reject
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects the request if the unmatchedPath of the [[akka.http.scaladsl.server.RequestContext]] is non-empty,
|
||||
* or said differently: only passes on the request to its inner route if the request path
|
||||
* has been matched completely.
|
||||
*/
|
||||
def pathEnd: Directive0 = rawPathPrefix(PathEnd)
|
||||
|
||||
/**
|
||||
* Only passes on the request to its inner route if the request path has been matched
|
||||
* completely or only consists of exactly one remaining slash.
|
||||
*
|
||||
* Note that trailing slash and non-trailing slash URLs are '''not''' the same, although they often serve
|
||||
* the same content. It is recommended to serve only one URL version and make the other redirect to it using
|
||||
* [[redirectToTrailingSlashIfMissing]] or [[redirectToNoTrailingSlashIfPresent]] directive.
|
||||
*
|
||||
* For example:
|
||||
* {{{
|
||||
* def route = {
|
||||
* // redirect '/users/' to '/users', '/users/:userId/' to '/users/:userId'
|
||||
* redirectToNoTrailingSlashIfPresent(Found) {
|
||||
* pathPrefix("users") {
|
||||
* pathEnd {
|
||||
* // user list ...
|
||||
* } ~
|
||||
* path(UUID) { userId =>
|
||||
* // user profile ...
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }}}
|
||||
*
|
||||
* For further information, refer to:
|
||||
* [[http://googlewebmastercentral.blogspot.de/2010/04/to-slash-or-not-to-slash.html]]
|
||||
*/
|
||||
def pathEndOrSingleSlash: Directive0 = rawPathPrefix(Slash.? ~ PathEnd)
|
||||
|
||||
/**
|
||||
* Only passes on the request to its inner route if the request path
|
||||
* consists of exactly one remaining slash.
|
||||
*/
|
||||
def pathSingleSlash: Directive0 = pathPrefix(PathEnd)
|
||||
|
||||
/**
|
||||
* If the request path doesn't end with a slash, redirect to the same uri with trailing slash in the path.
|
||||
*
|
||||
* '''Caveat''': [[path]] without trailing slash and [[pathEnd]] directives will not match inside of this directive.
|
||||
*/
|
||||
def redirectToTrailingSlashIfMissing(redirectionType: StatusCodes.Redirection): Directive0 =
|
||||
extractUri.flatMap { uri ⇒
|
||||
if (uri.path.endsWithSlash) pass
|
||||
else {
|
||||
val newPath = uri.path ++ Path.SingleSlash
|
||||
val newUri = uri.withPath(newPath)
|
||||
redirect(newUri, redirectionType)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the request path ends with a slash, redirect to the same uri without trailing slash in the path.
|
||||
*
|
||||
* '''Caveat''': [[pathSingleSlash]] directive will not match inside of this directive.
|
||||
*/
|
||||
def redirectToNoTrailingSlashIfPresent(redirectionType: StatusCodes.Redirection): Directive0 =
|
||||
extractUri.flatMap { uri ⇒
|
||||
if (uri.path.endsWithSlash) {
|
||||
val newPath = uri.path.reverse.tail.reverse
|
||||
val newUri = uri.withPath(newPath)
|
||||
redirect(newUri, redirectionType)
|
||||
} else pass
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object PathDirectives extends PathDirectives
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.scaladsl.server.RouteResult.Complete
|
||||
import akka.http.impl.util._
|
||||
import akka.stream.scaladsl.Source
|
||||
|
||||
import scala.collection.immutable
|
||||
|
||||
trait RangeDirectives {
|
||||
import akka.http.scaladsl.server.directives.BasicDirectives._
|
||||
import akka.http.scaladsl.server.directives.RouteDirectives._
|
||||
|
||||
/**
|
||||
* Answers GET requests with an `Accept-Ranges: bytes` header and converts HttpResponses coming back from its inner
|
||||
* route into partial responses if the initial request contained a valid `Range` request header. The requested
|
||||
* byte-ranges may be coalesced.
|
||||
* This directive is transparent to non-GET requests
|
||||
* Rejects requests with unsatisfiable ranges `UnsatisfiableRangeRejection`.
|
||||
* Rejects requests with too many expected ranges.
|
||||
*
|
||||
* Note: if you want to combine this directive with `conditional(...)` you need to put
|
||||
* it on the *inside* of the `conditional(...)` directive, i.e. `conditional(...)` must be
|
||||
* on a higher level in your route structure in order to function correctly.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc7233
|
||||
*/
|
||||
def withRangeSupport: Directive0 =
|
||||
extractRequestContext.flatMap { ctx ⇒
|
||||
import ctx.flowMaterializer
|
||||
val settings = ctx.settings
|
||||
implicit val log = ctx.log
|
||||
import settings.{ rangeCountLimit, rangeCoalescingThreshold }
|
||||
|
||||
class IndexRange(val start: Long, val end: Long) {
|
||||
def length = end - start
|
||||
def apply(entity: UniversalEntity): UniversalEntity = entity.transformDataBytes(length, StreamUtils.sliceBytesTransformer(start, length))
|
||||
def distance(other: IndexRange) = mergedEnd(other) - mergedStart(other) - (length + other.length)
|
||||
def mergeWith(other: IndexRange) = new IndexRange(mergedStart(other), mergedEnd(other))
|
||||
def contentRange(entityLength: Long) = ContentRange(start, end - 1, entityLength)
|
||||
private def mergedStart(other: IndexRange) = math.min(start, other.start)
|
||||
private def mergedEnd(other: IndexRange) = math.max(end, other.end)
|
||||
}
|
||||
|
||||
def indexRange(entityLength: Long)(range: ByteRange): IndexRange =
|
||||
range match {
|
||||
case ByteRange.Slice(start, end) ⇒ new IndexRange(start, math.min(end + 1, entityLength))
|
||||
case ByteRange.FromOffset(first) ⇒ new IndexRange(first, entityLength)
|
||||
case ByteRange.Suffix(suffixLength) ⇒ new IndexRange(math.max(0, entityLength - suffixLength), entityLength)
|
||||
}
|
||||
|
||||
// See comment of the `range-coalescing-threshold` setting in `reference.conf` for the rationale of this behavior.
|
||||
def coalesceRanges(iRanges: Seq[IndexRange]): Seq[IndexRange] =
|
||||
iRanges.foldLeft(Seq.empty[IndexRange]) { (acc, iRange) ⇒
|
||||
val (mergeCandidates, otherCandidates) = acc.partition(_.distance(iRange) <= rangeCoalescingThreshold)
|
||||
val merged = mergeCandidates.foldLeft(iRange)(_ mergeWith _)
|
||||
otherCandidates :+ merged
|
||||
}
|
||||
|
||||
def multipartRanges(ranges: Seq[ByteRange], entity: UniversalEntity): Multipart.ByteRanges = {
|
||||
val length = entity.contentLength
|
||||
val iRanges: Seq[IndexRange] = ranges.map(indexRange(length))
|
||||
|
||||
// It's only possible to run once over the input entity data stream because it's not known if the
|
||||
// source is reusable.
|
||||
// Therefore, ranges need to be sorted to prevent that some selected ranges already start to accumulate data
|
||||
// but cannot be sent out because another range is blocking the queue.
|
||||
val coalescedRanges = coalesceRanges(iRanges).sortBy(_.start)
|
||||
val bodyPartTransformers = coalescedRanges.map(ir ⇒ StreamUtils.sliceBytesTransformer(ir.start, ir.length)).toVector
|
||||
val bodyPartByteStreams = StreamUtils.transformMultiple(entity.dataBytes, bodyPartTransformers)
|
||||
val bodyParts = (coalescedRanges, bodyPartByteStreams).zipped.map { (range, bytes) ⇒
|
||||
Multipart.ByteRanges.BodyPart(range.contentRange(length), HttpEntity(entity.contentType, range.length, bytes))
|
||||
}
|
||||
Multipart.ByteRanges(Source(bodyParts.toVector))
|
||||
}
|
||||
|
||||
def rangeResponse(range: ByteRange, entity: UniversalEntity, length: Long, headers: immutable.Seq[HttpHeader]) = {
|
||||
val aiRange = indexRange(length)(range)
|
||||
HttpResponse(PartialContent, `Content-Range`(aiRange.contentRange(length)) +: headers, aiRange(entity))
|
||||
}
|
||||
|
||||
def satisfiable(entityLength: Long)(range: ByteRange): Boolean =
|
||||
range match {
|
||||
case ByteRange.Slice(firstPos, _) ⇒ firstPos < entityLength
|
||||
case ByteRange.FromOffset(firstPos) ⇒ firstPos < entityLength
|
||||
case ByteRange.Suffix(length) ⇒ length > 0
|
||||
}
|
||||
def universal(entity: HttpEntity): Option[UniversalEntity] = entity match {
|
||||
case u: UniversalEntity ⇒ Some(u)
|
||||
case _ ⇒ None
|
||||
}
|
||||
|
||||
def applyRanges(ranges: Seq[ByteRange]): Directive0 =
|
||||
extractRequestContext.flatMap { ctx ⇒
|
||||
mapRouteResultWithPF {
|
||||
case Complete(HttpResponse(OK, headers, entity, protocol)) ⇒
|
||||
universal(entity) match {
|
||||
case Some(entity) ⇒
|
||||
val length = entity.contentLength
|
||||
ranges.filter(satisfiable(length)) match {
|
||||
case Nil ⇒ ctx.reject(UnsatisfiableRangeRejection(ranges, length))
|
||||
case Seq(satisfiableRange) ⇒ ctx.complete(rangeResponse(satisfiableRange, entity, length, headers))
|
||||
case satisfiableRanges ⇒
|
||||
ctx.complete(PartialContent, headers, multipartRanges(satisfiableRanges, entity))
|
||||
}
|
||||
case None ⇒
|
||||
// Ranges not supported for Chunked or CloseDelimited responses
|
||||
ctx.reject(UnsatisfiableRangeRejection(ranges, -1)) // FIXME: provide better error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def rangeHeaderOfGetRequests(ctx: RequestContext): Option[Range] =
|
||||
if (ctx.request.method == HttpMethods.GET) ctx.request.header[Range] else None
|
||||
|
||||
extract(rangeHeaderOfGetRequests).flatMap {
|
||||
case Some(Range(RangeUnits.Bytes, ranges)) ⇒
|
||||
if (ranges.size <= rangeCountLimit) applyRanges(ranges) & RangeDirectives.respondWithAcceptByteRangesHeader
|
||||
else reject(TooManyRangesRejection(rangeCountLimit))
|
||||
case _ ⇒ MethodDirectives.get & RangeDirectives.respondWithAcceptByteRangesHeader | pass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object RangeDirectives extends RangeDirectives {
|
||||
private val respondWithAcceptByteRangesHeader: Directive0 =
|
||||
RespondWithDirectives.respondWithHeader(`Accept-Ranges`(RangeUnits.Bytes))
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import scala.collection.immutable
|
||||
|
||||
trait RespondWithDirectives {
|
||||
import BasicDirectives._
|
||||
|
||||
/**
|
||||
* Overrides the given response status on all HTTP responses of its inner Route.
|
||||
*/
|
||||
def overrideStatusCode(responseStatus: StatusCode): Directive0 =
|
||||
mapResponse(_.copy(status = responseStatus))
|
||||
|
||||
/**
|
||||
* Unconditionally adds the given response header to all HTTP responses of its inner Route.
|
||||
*/
|
||||
def respondWithHeader(responseHeader: HttpHeader): Directive0 = respondWithHeaders(responseHeader)
|
||||
|
||||
/**
|
||||
* Adds the given response header to all HTTP responses of its inner Route,
|
||||
* if the response from the inner Route doesn't already contain a header with the same name.
|
||||
*/
|
||||
def respondWithDefaultHeader(responseHeader: HttpHeader): Directive0 = respondWithDefaultHeaders(responseHeader)
|
||||
|
||||
/**
|
||||
* Unconditionally adds the given response headers to all HTTP responses of its inner Route.
|
||||
*/
|
||||
def respondWithHeaders(responseHeaders: HttpHeader*): Directive0 =
|
||||
respondWithHeaders(responseHeaders.toList)
|
||||
|
||||
/**
|
||||
* Unconditionally adds the given response headers to all HTTP responses of its inner Route.
|
||||
*/
|
||||
def respondWithHeaders(responseHeaders: immutable.Seq[HttpHeader]): Directive0 =
|
||||
mapResponseHeaders(responseHeaders ++ _)
|
||||
|
||||
/**
|
||||
* Adds the given response headers to all HTTP responses of its inner Route,
|
||||
* if a header already exists it is not added again.
|
||||
*/
|
||||
def respondWithDefaultHeaders(responseHeaders: HttpHeader*): Directive0 =
|
||||
respondWithDefaultHeaders(responseHeaders.toList)
|
||||
|
||||
/* Adds the given response headers to all HTTP responses of its inner Route,
|
||||
* if a header already exists it is not added again.
|
||||
*/
|
||||
def respondWithDefaultHeaders(responseHeaders: immutable.Seq[HttpHeader]): Directive0 =
|
||||
mapResponse(_.withDefaultHeaders(responseHeaders))
|
||||
}
|
||||
|
||||
object RespondWithDirectives extends RespondWithDirectives
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.marshalling.ToResponseMarshallable
|
||||
import akka.http.scaladsl.model._
|
||||
import StatusCodes._
|
||||
|
||||
trait RouteDirectives {
|
||||
|
||||
/**
|
||||
* Rejects the request with an empty set of rejections.
|
||||
*/
|
||||
def reject: StandardRoute = RouteDirectives._reject
|
||||
|
||||
/**
|
||||
* Rejects the request with the given rejections.
|
||||
*/
|
||||
def reject(rejections: Rejection*): StandardRoute =
|
||||
StandardRoute(_.reject(rejections: _*))
|
||||
|
||||
/**
|
||||
* Completes the request with redirection response of the given type to the given URI.
|
||||
*/
|
||||
def redirect(uri: Uri, redirectionType: Redirection): StandardRoute =
|
||||
StandardRoute {
|
||||
_. //# red-impl
|
||||
complete {
|
||||
HttpResponse(
|
||||
status = redirectionType,
|
||||
headers = headers.Location(uri) :: Nil,
|
||||
entity = redirectionType.htmlTemplate match {
|
||||
case "" ⇒ HttpEntity.Empty
|
||||
case template ⇒ HttpEntity(MediaTypes.`text/html`, template format uri)
|
||||
})
|
||||
}
|
||||
//#
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the request using the given arguments.
|
||||
*/
|
||||
def complete(m: ⇒ ToResponseMarshallable): StandardRoute =
|
||||
StandardRoute(_.complete(m))
|
||||
|
||||
/**
|
||||
* Bubbles the given error up the response chain, where it is dealt with by the closest `handleExceptions`
|
||||
* directive and its ExceptionHandler.
|
||||
*/
|
||||
def failWith(error: Throwable): StandardRoute =
|
||||
StandardRoute(_.fail(error))
|
||||
}
|
||||
|
||||
object RouteDirectives extends RouteDirectives {
|
||||
private val _reject = StandardRoute(_.reject())
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
trait SchemeDirectives {
|
||||
import BasicDirectives._
|
||||
|
||||
/**
|
||||
* Extracts the Uri scheme from the request.
|
||||
*/
|
||||
def extractScheme: Directive1[String] = SchemeDirectives._extractScheme
|
||||
|
||||
/**
|
||||
* Rejects all requests whose Uri scheme does not match the given one.
|
||||
*/
|
||||
def scheme(name: String): Directive0 =
|
||||
extractScheme.require(_ == name, SchemeRejection(name)) & cancelRejections(classOf[SchemeRejection])
|
||||
}
|
||||
|
||||
object SchemeDirectives extends SchemeDirectives {
|
||||
import BasicDirectives._
|
||||
|
||||
private val _extractScheme: Directive1[String] = extract(_.request.uri.scheme)
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.reflect.ClassTag
|
||||
import scala.concurrent.Future
|
||||
import akka.http.impl.util._
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.scaladsl.server.AuthenticationFailedRejection.{ CredentialsRejected, CredentialsMissing }
|
||||
|
||||
/**
|
||||
* Provides directives for securing an inner route using the standard Http authentication headers [[`WWW-Authenticate`]]
|
||||
* and [[Authorization]]. Most prominently, HTTP Basic authentication as defined in RFC 2617.
|
||||
*/
|
||||
trait SecurityDirectives {
|
||||
import BasicDirectives._
|
||||
import HeaderDirectives._
|
||||
import FutureDirectives._
|
||||
import RouteDirectives._
|
||||
|
||||
/**
|
||||
* The result of an HTTP authentication attempt is either the user object or
|
||||
* an HttpChallenge to present to the browser.
|
||||
*/
|
||||
type AuthenticationResult[+T] = Either[HttpChallenge, T]
|
||||
|
||||
type Authenticator[T] = UserCredentials ⇒ Option[T]
|
||||
type AsyncAuthenticator[T] = UserCredentials ⇒ Future[Option[T]]
|
||||
type AuthenticatorPF[T] = PartialFunction[UserCredentials, T]
|
||||
type AsyncAuthenticatorPF[T] = PartialFunction[UserCredentials, Future[T]]
|
||||
|
||||
/**
|
||||
* Extracts the potentially present [[HttpCredentials]] provided with the request's [[Authorization]] header.
|
||||
*/
|
||||
def extractCredentials: Directive1[Option[HttpCredentials]] =
|
||||
optionalHeaderValueByType[Authorization]().map(_.map(_.credentials))
|
||||
|
||||
/**
|
||||
* A directive that wraps the inner route with Http Basic authentication support.
|
||||
* The given authenticator determines whether the credentials in the request are valid
|
||||
* and, if so, which user object to supply to the inner route.
|
||||
*/
|
||||
def authenticateBasic[T](realm: String, authenticator: Authenticator[T]): AuthenticationDirective[T] =
|
||||
authenticateBasicAsync(realm, cred ⇒ FastFuture.successful(authenticator(cred)))
|
||||
|
||||
/**
|
||||
* A directive that wraps the inner route with Http Basic authentication support.
|
||||
* The given authenticator determines whether the credentials in the request are valid
|
||||
* and, if so, which user object to supply to the inner route.
|
||||
*/
|
||||
def authenticateBasicAsync[T](realm: String, authenticator: AsyncAuthenticator[T]): AuthenticationDirective[T] =
|
||||
extractExecutionContext.flatMap { implicit ec ⇒
|
||||
authenticateOrRejectWithChallenge[BasicHttpCredentials, T] { basic ⇒
|
||||
authenticator(UserCredentials(basic)).fast.map {
|
||||
case Some(t) ⇒ AuthenticationResult.success(t)
|
||||
case None ⇒ AuthenticationResult.failWithChallenge(challengeFor(realm))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A directive that wraps the inner route with Http Basic authentication support.
|
||||
* The given authenticator determines whether the credentials in the request are valid
|
||||
* and, if so, which user object to supply to the inner route.
|
||||
*/
|
||||
def authenticateBasicPF[T](realm: String, authenticator: AuthenticatorPF[T]): AuthenticationDirective[T] =
|
||||
authenticateBasic(realm, authenticator.lift)
|
||||
|
||||
/**
|
||||
* A directive that wraps the inner route with Http Basic authentication support.
|
||||
* The given authenticator determines whether the credentials in the request are valid
|
||||
* and, if so, which user object to supply to the inner route.
|
||||
*/
|
||||
def authenticateBasicPFAsync[T](realm: String, authenticator: AsyncAuthenticatorPF[T]): AuthenticationDirective[T] =
|
||||
extractExecutionContext.flatMap { implicit ec ⇒
|
||||
authenticateBasicAsync(realm, credentials ⇒
|
||||
if (authenticator isDefinedAt credentials) authenticator(credentials).fast.map(Some(_))
|
||||
else FastFuture.successful(None))
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifts an authenticator function into a directive. The authenticator function gets passed in credentials from the
|
||||
* [[Authorization]] header of the request. If the function returns ``Right(user)`` the user object is provided
|
||||
* to the inner route. If the function returns ``Left(challenge)`` the request is rejected with an
|
||||
* [[AuthenticationFailedRejection]] that contains this challenge to be added to the response.
|
||||
*
|
||||
*/
|
||||
def authenticateOrRejectWithChallenge[T](authenticator: Option[HttpCredentials] ⇒ Future[AuthenticationResult[T]]): AuthenticationDirective[T] =
|
||||
extractExecutionContext.flatMap { implicit ec ⇒
|
||||
extractCredentials.flatMap { cred ⇒
|
||||
onSuccess(authenticator(cred)).flatMap {
|
||||
case Right(user) ⇒ provide(user)
|
||||
case Left(challenge) ⇒
|
||||
val cause = if (cred.isEmpty) CredentialsMissing else CredentialsRejected
|
||||
reject(AuthenticationFailedRejection(cause, challenge)): Directive1[T]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifts an authenticator function into a directive. Same as ``authenticateOrRejectWithChallenge``
|
||||
* but only applies the authenticator function with a certain type of credentials.
|
||||
*/
|
||||
def authenticateOrRejectWithChallenge[C <: HttpCredentials: ClassTag, T](
|
||||
authenticator: Option[C] ⇒ Future[AuthenticationResult[T]]): AuthenticationDirective[T] =
|
||||
authenticateOrRejectWithChallenge[T](cred ⇒ authenticator(cred collect { case c: C ⇒ c }))
|
||||
|
||||
/**
|
||||
* Applies the given authorization check to the request.
|
||||
* If the check fails the route is rejected with an [[AuthorizationFailedRejection]].
|
||||
*/
|
||||
def authorize(check: ⇒ Boolean): Directive0 = authorize(_ ⇒ check)
|
||||
|
||||
/**
|
||||
* Applies the given authorization check to the request.
|
||||
* If the check fails the route is rejected with an [[AuthorizationFailedRejection]].
|
||||
*/
|
||||
def authorize(check: RequestContext ⇒ Boolean): Directive0 =
|
||||
extract(check).flatMap[Unit](if (_) pass else reject(AuthorizationFailedRejection)) &
|
||||
cancelRejection(AuthorizationFailedRejection)
|
||||
|
||||
/**
|
||||
* Creates a ``Basic`` [[HttpChallenge]] for the given realm.
|
||||
*/
|
||||
def challengeFor(realm: String) = HttpChallenge(scheme = "Basic", realm = realm, params = Map.empty)
|
||||
}
|
||||
|
||||
object SecurityDirectives extends SecurityDirectives
|
||||
|
||||
/**
|
||||
* Represents authentication credentials supplied with a request. Credentials can either be
|
||||
* [[UserCredentials.Missing]] or can be [[UserCredentials.Provided]] in which case a username is
|
||||
* supplied and a function to check the known secret against the provided one in a secure fashion.
|
||||
*/
|
||||
sealed trait UserCredentials
|
||||
object UserCredentials {
|
||||
case object Missing extends UserCredentials
|
||||
abstract case class Provided(username: String) extends UserCredentials {
|
||||
def verifySecret(secret: String): Boolean
|
||||
}
|
||||
|
||||
def apply(cred: Option[BasicHttpCredentials]): UserCredentials =
|
||||
cred match {
|
||||
case Some(BasicHttpCredentials(username, receivedSecret)) ⇒
|
||||
new UserCredentials.Provided(username) {
|
||||
def verifySecret(secret: String): Boolean = secret secure_== receivedSecret
|
||||
}
|
||||
case None ⇒ UserCredentials.Missing
|
||||
}
|
||||
}
|
||||
|
||||
import SecurityDirectives._
|
||||
|
||||
object AuthenticationResult {
|
||||
def success[T](user: T): AuthenticationResult[T] = Right(user)
|
||||
def failWithChallenge(challenge: HttpChallenge): AuthenticationResult[Nothing] = Left(challenge)
|
||||
}
|
||||
|
||||
trait AuthenticationDirective[T] extends Directive1[T] {
|
||||
import BasicDirectives._
|
||||
import RouteDirectives._
|
||||
|
||||
/**
|
||||
* Returns a copy of this [[AuthenticationDirective]] that will provide ``Some(user)`` if credentials
|
||||
* were supplied and otherwise ``None``.
|
||||
*/
|
||||
def optional: Directive1[Option[T]] =
|
||||
this.map(Some(_): Option[T]) recover {
|
||||
case AuthenticationFailedRejection(CredentialsMissing, _) +: _ ⇒ provide(None)
|
||||
case rejs ⇒ reject(rejs: _*)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this [[AuthenticationDirective]] that uses the given object as the
|
||||
* anonymous user which will be used if no credentials were supplied in the request.
|
||||
*/
|
||||
def withAnonymousUser(anonymous: T): Directive1[T] = optional map (_ getOrElse anonymous)
|
||||
}
|
||||
object AuthenticationDirective {
|
||||
implicit def apply[T](other: Directive1[T]): AuthenticationDirective[T] =
|
||||
new AuthenticationDirective[T] { def tapply(inner: Tuple1[T] ⇒ Route) = other.tapply(inner) }
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model.ws.{ UpgradeToWebsocket, Message }
|
||||
import akka.stream.scaladsl.Flow
|
||||
|
||||
trait WebsocketDirectives {
|
||||
import BasicDirectives._
|
||||
import RouteDirectives._
|
||||
import HeaderDirectives._
|
||||
|
||||
/**
|
||||
* Handles websocket requests with the given handler and rejects other requests with a
|
||||
* [[ExpectedWebsocketRequestRejection]].
|
||||
*/
|
||||
def handleWebsocketMessages(handler: Flow[Message, Message, Any]): Route =
|
||||
extractFlowMaterializer { implicit mat ⇒
|
||||
optionalHeaderValueByType[UpgradeToWebsocket]() {
|
||||
case Some(upgrade) ⇒ complete(upgrade.handleMessages(handler))
|
||||
case None ⇒ reject(ExpectedWebsocketRequestRejection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
package object server {
|
||||
|
||||
type Route = RequestContext ⇒ Future[RouteResult]
|
||||
type RouteGenerator[T] = T ⇒ Route
|
||||
type Directive0 = Directive[Unit]
|
||||
type Directive1[T] = Directive[Tuple1[T]]
|
||||
type PathMatcher0 = PathMatcher[Unit]
|
||||
type PathMatcher1[T] = PathMatcher[Tuple1[T]]
|
||||
|
||||
def FIXME = throw new RuntimeException("Not yet implemented")
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.util
|
||||
|
||||
import akka.http.scaladsl.server._
|
||||
|
||||
/**
|
||||
* ApplyConverter allows generic conversion of functions of type `(T1, T2, ...) => Route` to
|
||||
* `(TupleX(T1, T2, ...)) => Route`.
|
||||
*/
|
||||
abstract class ApplyConverter[L] {
|
||||
type In
|
||||
def apply(f: In): L ⇒ Route
|
||||
}
|
||||
|
||||
object ApplyConverter extends ApplyConverterInstances
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.util
|
||||
|
||||
/**
|
||||
* Allows the definition of binary poly-functions (e.g. for folding over tuples).
|
||||
*
|
||||
* Note: the poly-function implementation seen here is merely a stripped down version of
|
||||
* what Miles Sabin made available with his awesome shapeless library. All credit goes to him!
|
||||
*/
|
||||
trait BinaryPolyFunc {
|
||||
def at[A, B] = new CaseBuilder[A, B]
|
||||
class CaseBuilder[A, B] {
|
||||
def apply[R](f: (A, B) ⇒ R) = new BinaryPolyFunc.Case[A, B, BinaryPolyFunc.this.type] {
|
||||
type Out = R
|
||||
def apply(a: A, b: B) = f(a, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object BinaryPolyFunc {
|
||||
sealed trait Case[A, B, Op] {
|
||||
type Out
|
||||
def apply(a: A, b: B): Out
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue