Permalink
Browse files

! http(x): support content negotiation, fixes #167

  • Loading branch information...
2beaucoup committed Jul 19, 2013
1 parent 7222a6f commit f8f5b6d87900b09a029d50fd6ac6bfeb9e7e31f1
@@ -38,19 +38,15 @@ trait DemoService extends HttpService {
val demoRoute = {
get {
path("") {
- respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
- complete(index)
- }
+ complete(index)
} ~
path("ping") {
complete("PONG!")
} ~
path("stream1") {
- respondWithMediaType(`text/html`) {
- // we detach in order to move the blocking code inside the simpleStringStream off the service actor
- detachTo(singleRequestServiceActor) {
- complete(simpleStringStream)
- }
+ // we detach in order to move the blocking code inside the simpleStringStream off the service actor
+ detachTo(singleRequestServiceActor) {
+ complete(simpleStringStream)
}
} ~
path("stream2") {
@@ -41,19 +41,15 @@ trait DemoService extends HttpService {
val demoRoute = {
get {
path("") {
- respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
- complete(index)
- }
+ complete(index)
} ~
path("ping") {
complete("PONG!")
} ~
path("stream1") {
- respondWithMediaType(`text/html`) {
- // we detach in order to move the blocking code inside the simpleStringStream off the service actor
- detachTo(singleRequestServiceActor) {
- complete(simpleStringStream)
- }
+ // we detach in order to move the blocking code inside the simpleStringStream off the service actor
+ detachTo(singleRequestServiceActor) {
+ complete(simpleStringStream)
}
} ~
path("stream2") {
@@ -15,13 +15,11 @@ object Main extends App with SimpleRoutingApp {
redirect("/hello", StatusCodes.Found)
} ~
path("hello") {
- respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
- complete {
- <html>
- <h1>Say hello to <em>spray</em> on <em>spray-can</em>!</h1>
- <p>(<a href="/stop?method=post">stop server</a>)</p>
- </html>
- }
+ complete {
+ <html>
+ <h1>Say hello to <em>spray</em> on <em>spray-can</em>!</h1>
+ <p>(<a href="/stop?method=post">stop server</a>)</p>
+ </html>
}
}
} ~
@@ -145,10 +145,11 @@ case class HttpRequest(method: HttpMethod = HttpMethods.GET,
else sys.error("'Host' header value doesn't match request target authority")
}
- def acceptedMediaRanges: List[MediaRange] = {
- // TODO: sort by preference
- for (Accept(mediaRanges) headers; range mediaRanges) yield range
- }
+ def acceptedMediaRanges: List[MediaRange] =
+ (for {
+ Accept(mediaRanges) headers
+ range mediaRanges
+ } yield range).sortBy(-_.qValue)
def acceptedCharsetRanges: List[HttpCharsetRange] = {
// TODO: sort by preference
@@ -169,7 +170,7 @@ case class HttpRequest(method: HttpMethod = HttpMethods.GET,
// according to the HTTP spec a client has to accept all mime types if no Accept header is sent with the request
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
val ranges = acceptedMediaRanges
- ranges.isEmpty || ranges.exists(_.matches(mediaType))
+ ranges.isEmpty || ranges.exists(r r.qValue > 0.0f && r.matches(mediaType))
}
/**
@@ -196,21 +197,34 @@ case class HttpRequest(method: HttpMethod = HttpMethods.GET,
/**
* Determines whether the given content-type is accepted by the client.
*/
- def isContentTypeAccepted(ct: ContentType) = {
+ def isContentTypeAccepted(ct: ContentType) =
isMediaTypeAccepted(ct.mediaType) && (ct.noCharsetDefined || isCharsetAccepted(ct.definedCharset.get))
- }
/**
- * Determines whether the given content-type is accepted by the client.
- * If the given ContentType does not define a charset an accepted charset is selected, i.e. the method guarantees
+ * Determines whether one of the given content-types is accepted by the client.
+ * If a given ContentType does not define a charset an accepted charset is selected, i.e. the method guarantees
* that, if a ContentType instance is returned within the option, it will contain a defined charset.
*/
- def acceptableContentType(contentType: ContentType): Option[ContentType] = {
- if (isContentTypeAccepted(contentType)) Some {
- if (contentType.isCharsetDefined) contentType
- else ContentType(contentType.mediaType, acceptedCharset)
+ def acceptableContentType(contentTypes: Seq[ContentType]): Option[ContentType] = {
+ @tailrec def negotiate(mediaRanges: List[MediaRange]): Option[ContentType] = mediaRanges match {
+ case r :: rs
+ val contentType = contentTypes.find { ct
+ r.matches(ct) && isContentTypeAccepted(ct)
+ }
+ if (contentType.nonEmpty) contentType else negotiate(rs)
+ case Nil None
+ }
+
+ val mediaRanges = acceptedMediaRanges
+ val contentType =
+ if (mediaRanges.nonEmpty)
+ negotiate(mediaRanges)
+ else
+ contentTypes.headOption
+ contentType map { ct
+ if (ct.isCharsetDefined) ct
+ else ContentType(ct.mediaType, acceptedCharset)
}
- else None
}
/**
@@ -31,6 +31,7 @@ sealed abstract class MediaRange extends LazyValueBytesRenderable {
def isMultipart = false
def isText = false
def isVideo = false
+ def qValue = parameters.getOrElse("q", "1.0").toFloat
}
object MediaRange {
@@ -34,8 +34,8 @@ private[parser] trait AcceptHeader {
@tailrec def toNonQValueMap(remaining: List[(String, String)],
builder: StringMapBuilder = null): Map[String, String] =
remaining match {
- case Nil if (builder eq null) Map.empty else builder.result()
- case ("q", _) :: tail toNonQValueMap(tail, builder)
+ case Nil if (builder eq null) Map.empty else builder.result()
+ // case ("q", _) :: tail ⇒ toNonQValueMap(tail, builder)
case kvp :: tail
val b = if (builder eq null) Map.newBuilder[String, String] else builder
b += kvp
@@ -42,7 +42,7 @@ class CollectingMarshallingContext(implicit actorRefFactory: ActorRefFactory = n
def chunkedMessageEnd: Option[ChunkedMessageEnd] = _chunkedMessageEnd.get
// we always convert to the first content-type the marshaller can marshal to
- def tryAccept(contentType: ContentType) = Some(contentType)
+ def tryAccept(contentTypes: Seq[ContentType]) = contentTypes.headOption
def rejectMarshalling(supported: Seq[ContentType]): Unit = {
handleError(new RuntimeException("Marshaller rejected marshalling, only supports " + supported))
@@ -38,12 +38,11 @@ object Marshaller extends BasicMarshallers
def of[T](marshalTo: ContentType*)(f: (T, ContentType, MarshallingContext) Unit): Marshaller[T] =
new Marshaller[T] {
- def apply(value: T, ctx: MarshallingContext): Unit = {
- marshalTo.mapFind(ctx.tryAccept) match {
+ def apply(value: T, ctx: MarshallingContext): Unit =
+ ctx.tryAccept(marshalTo) match {
case Some(contentType) f(value, contentType, ctx)
case None ctx.rejectMarshalling(marshalTo)
}
- }
}
def delegate[A, B](marshalTo: ContentType*) = new MarshallerDelegation[A, B](marshalTo)
@@ -26,7 +26,9 @@ trait MarshallingContext { self ⇒
* If the given ContentType does not define a charset an accepted charset is selected, i.e. the method guarantees
* that, if a ContentType instance is returned within the option, it will contain a defined charset.
*/
- def tryAccept(contentType: ContentType): Option[ContentType]
+ def tryAccept(contentTypes: Seq[ContentType]): Option[ContentType]
+
+ def tryAccept(contentType: ContentType): Option[ContentType] = tryAccept(List(contentType))
/**
* Signals that the Marshaller rejects the marshalling request because
@@ -61,8 +63,8 @@ trait MarshallingContext { self ⇒
*/
def withContentTypeOverriding(contentType: ContentType): MarshallingContext =
new DelegatingMarshallingContext(self) {
- override def tryAccept(ct: ContentType) =
- Some(if (contentType.isCharsetDefined) ct.withCharset(contentType.charset) else ct)
+ override def tryAccept(cts: Seq[ContentType]) =
+ Some(if (contentType.isCharsetDefined) cts.head.withCharset(contentType.charset) else cts.head)
override def marshalTo(entity: HttpEntity): Unit = { self.marshalTo(overrideContentType(entity)) }
override def startChunkedMessage(entity: HttpEntity, ack: Option[Any])(implicit sender: ActorRef) =
self.startChunkedMessage(overrideContentType(entity), ack)
@@ -76,7 +78,7 @@ trait MarshallingContext { self ⇒
* wrap another MarshallingContext with some extra logic.
*/
class DelegatingMarshallingContext(underlying: MarshallingContext) extends MarshallingContext {
- def tryAccept(contentType: ContentType) = underlying.tryAccept(contentType)
+ def tryAccept(contentTypes: Seq[ContentType]) = underlying.tryAccept(contentTypes)
def rejectMarshalling(supported: Seq[ContentType]): Unit = { underlying.rejectMarshalling(supported) }
def marshalTo(entity: HttpEntity): Unit = { underlying.marshalTo(entity) }
def handleError(error: Throwable): Unit = { underlying.handleError(error) }
@@ -276,7 +276,7 @@ case class RequestContext(request: HttpRequest, responder: ActorRef, unmatchedPa
*/
def marshallingContext(status: StatusCode, headers: List[HttpHeader]): MarshallingContext =
new MarshallingContext {
- def tryAccept(contentType: ContentType) = request.acceptableContentType(contentType)
+ def tryAccept(contentTypes: Seq[ContentType]) = request.acceptableContentType(contentTypes)
def rejectMarshalling(onlyTo: Seq[ContentType]): Unit = { reject(UnacceptedResponseContentTypeRejection(onlyTo)) }
def marshalTo(entity: HttpEntity): Unit = { complete(response(entity)) }
def handleError(error: Throwable): Unit = { failWith(error) }
@@ -39,7 +39,7 @@ abstract class PimpedSeq[+A] {
class PimpedLinearSeq[+A](underlying: LinearSeq[A]) extends PimpedSeq[A] {
def mapFind[B](f: A Option[B]): Option[B] = {
@tailrec def mapFind(seq: LinearSeq[A]): Option[B] =
- if (!seq.isEmpty) {
+ if (seq.nonEmpty) {
val x = f(seq.head)
if (x.isEmpty) mapFind(seq.tail) else x
} else None

0 comments on commit f8f5b6d

Please sign in to comment.