+hco various smaller cleanups and helper additions

This commit is contained in:
Mathias 2014-07-17 17:44:41 +02:00
parent 2ccf028a94
commit 5fce301c28
6 changed files with 48 additions and 204 deletions

View file

@ -61,6 +61,11 @@ sealed trait HttpEntity extends japi.HttpEntity {
})
.toFuture(materializer)
/**
* Creates a copy of this HttpEntity with the `contentType` overridden with the given one.
*/
def withContentType(contentType: ContentType): HttpEntity
/** Java API */
def getDataBytes(materializer: FlowMaterializer): Publisher[ByteString] = dataBytes(materializer)
@ -102,6 +107,7 @@ object HttpEntity {
* Close-delimited entities are not `Regular` as they exists primarily for backwards compatibility with HTTP/1.0.
*/
sealed trait Regular extends japi.HttpEntityRegular with HttpEntity {
def withContentType(contentType: ContentType): HttpEntity.Regular
override def isRegular: Boolean = true
}
@ -120,6 +126,8 @@ object HttpEntity {
override def toStrict(timeout: FiniteDuration, materializer: FlowMaterializer)(implicit ec: ExecutionContext): Future[Strict] =
Future.successful(this)
def withContentType(contentType: ContentType): Strict = copy(contentType = contentType)
}
/**
@ -133,6 +141,8 @@ object HttpEntity {
override def isDefault: Boolean = true
def dataBytes(materializer: FlowMaterializer): Publisher[ByteString] = data
def withContentType(contentType: ContentType): Default = copy(contentType = contentType)
}
/**
@ -145,6 +155,8 @@ object HttpEntity {
override def isCloseDelimited: Boolean = true
def dataBytes(materializer: FlowMaterializer): Publisher[ByteString] = data
def withContentType(contentType: ContentType): CloseDelimited = copy(contentType = contentType)
}
/**
@ -157,6 +169,8 @@ object HttpEntity {
def dataBytes(materializer: FlowMaterializer): Publisher[ByteString] =
Flow(chunks).map(_.data).filter(_.nonEmpty).toPublisher(materializer)
def withContentType(contentType: ContentType): Chunked = copy(contentType = contentType)
/** Java API */
def getChunks: Publisher[japi.ChunkStreamPart] = chunks.asInstanceOf[Publisher[japi.ChunkStreamPart]]
}

View file

@ -221,40 +221,6 @@ final case class HttpRequest(method: HttpMethod = HttpMethods.GET,
case x x collectFirst { case r if r matches encoding r.qValue } getOrElse 0f
}
/**
* 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(contentTypes: IndexedSeq[ContentType]): Option[ContentType] = {
val mediaRanges = acceptedMediaRanges // cache for performance
val charsetRanges = acceptedCharsetRanges // cache for performance
@tailrec def findBest(ix: Int = 0, result: ContentType = null, maxQ: Float = 0f): Option[ContentType] =
if (ix < contentTypes.size) {
val ct = contentTypes(ix)
val q = qValueForMediaType(ct.mediaType, mediaRanges)
if (q > maxQ && (ct.noCharsetDefined || isCharsetAccepted(ct.charset, charsetRanges))) findBest(ix + 1, ct, q)
else findBest(ix + 1, result, maxQ)
} else Option(result)
findBest() flatMap { ct
def withCharset(cs: HttpCharset) = Some(ContentType(ct.mediaType, cs))
// logic for choosing the charset adapted from http://tools.ietf.org/html/rfc7231#section-5.3.3
if (ct.isCharsetDefined) Some(ct) // if there is already an acceptable charset chosen we are done
else if (qValueForCharset(`UTF-8`, charsetRanges) == 1f) withCharset(`UTF-8`) // prefer UTF-8 if fully accepted
else charsetRanges match { // ranges are sorted by descending q-value,
case (HttpCharsetRange.One(cs, qValue)) :: _ // so we only need to look at the first one
if (qValue == 1f) withCharset(cs) // if the client has high preference for this charset, pick it
else if (qValueForCharset(`ISO-8859-1`, charsetRanges) == 1f) withCharset(`ISO-8859-1`) // give some more preference to `ISO-8859-1`
else if (qValue > 0f) withCharset(cs) // ok, simply choose the first one if the client doesn't reject it
else None
case _ None
}
}
}
/**
* Determines whether this request can be safely retried, which is the case only of the request method is idempotent.
*/

View file

@ -7,7 +7,6 @@ package akka.http.rendering
import java.net.InetSocketAddress
import org.reactivestreams.Publisher
import scala.annotation.tailrec
import scala.collection.immutable
import akka.event.LoggingAdapter
import akka.util.ByteString
import akka.stream.scaladsl.Flow
@ -30,7 +29,7 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.`
final class HttpRequestRenderer extends Transformer[RequestRenderingContext, Publisher[ByteString]] {
def onNext(ctx: RequestRenderingContext): immutable.Seq[Publisher[ByteString]] = {
def onNext(ctx: RequestRenderingContext): List[Publisher[ByteString]] = {
val r = new ByteStringRendering(requestHeaderSizeHint)
import ctx.request._
@ -74,11 +73,8 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.`
case x: `Raw-Request-URI` // we never render this header
renderHeaders(tail, hostHeaderSeen, userAgentSeen)
case x: RawHeader if x.lowercaseName == "content-type" ||
x.lowercaseName == "content-length" ||
x.lowercaseName == "transfer-encoding" ||
x.lowercaseName == "host" ||
x.lowercaseName == "user-agent"
case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") ||
(x is "host") || (x is "user-agent")
suppressionWarning(log, x, "illegal RawHeader")
renderHeaders(tail, hostHeaderSeen, userAgentSeen)
@ -97,30 +93,31 @@ private[http] class HttpRequestRendererFactory(userAgentHeader: Option[headers.`
r ~~ CrLf
}
def completeRequestRendering(): immutable.Seq[Publisher[ByteString]] =
def completeRequestRendering(): List[Publisher[ByteString]] =
entity match {
case HttpEntity.Strict(contentType, data)
case x if x.isKnownEmpty
renderContentLength(0)
SynchronousPublisherFromIterable(r.get :: Nil) :: Nil
case HttpEntity.Strict(_, data)
renderContentLength(data.length)
SynchronousPublisherFromIterable(r.get :: data :: Nil) :: Nil
case HttpEntity.Default(contentType, contentLength, data)
case HttpEntity.Default(_, contentLength, data)
renderContentLength(contentLength)
renderByteStrings(r,
Flow(data).transform(new CheckContentLengthTransformer(contentLength)).toPublisher(materializer),
materializer)
case HttpEntity.Chunked(contentType, chunks)
r ~~ `Transfer-Encoding` ~~ Chunked ~~ CrLf ~~ CrLf
case HttpEntity.Chunked(_, chunks)
r ~~ `Transfer-Encoding` ~~ ChunkedBytes ~~ CrLf ~~ CrLf
renderByteStrings(r, Flow(chunks).transform(new ChunkTransformer).toPublisher(materializer), materializer)
}
renderRequestLine()
renderHeaders(headers.toList)
renderEntityContentType(r, entity)
if (entity.isKnownEmpty) {
renderContentLength(0)
SynchronousPublisherFromIterable(r.get :: Nil) :: Nil
} else completeRequestRendering()
completeRequestRendering()
}
}
}

View file

@ -6,7 +6,6 @@ package akka.http.rendering
import org.reactivestreams.Publisher
import scala.annotation.tailrec
import scala.collection.immutable
import akka.event.LoggingAdapter
import akka.util.ByteString
import akka.stream.scaladsl.Flow
@ -59,14 +58,14 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
override def isComplete = close
def onNext(ctx: ResponseRenderingContext): immutable.Seq[Publisher[ByteString]] = {
def onNext(ctx: ResponseRenderingContext): List[Publisher[ByteString]] = {
val r = new ByteStringRendering(responseHeaderSizeHint)
import ctx.response._
val noEntity = entity.isKnownEmpty || ctx.requestMethod == HttpMethods.HEAD
def renderStatusLine(): Unit =
if (status eq StatusCodes.OK) r ~~ DefaultStatusLine else r ~~ StatusLineStart ~~ status ~~ CrLf
if (status eq StatusCodes.OK) r ~~ DefaultStatusLineBytes else r ~~ StatusLineStartBytes ~~ status ~~ CrLf
def render(h: HttpHeader) = r ~~ h ~~ CrLf
@ -94,12 +93,8 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
render(x)
renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen = true)
case x: RawHeader if x.lowercaseName == "content-type" ||
x.lowercaseName == "content-length" ||
x.lowercaseName == "transfer-encoding" ||
x.lowercaseName == "date" ||
x.lowercaseName == "server" ||
x.lowercaseName == "connection"
case x: RawHeader if (x is "content-type") || (x is "content-length") || (x is "transfer-encoding") ||
(x is "date") || (x is "server") || (x is "connection")
suppressionWarning(log, x, "illegal RawHeader")
renderHeaders(tail, alwaysClose, connHeader, serverHeaderSeen)
@ -115,31 +110,31 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
ctx.closeAfterResponseCompletion || // request wants to close
(connHeader != null && connHeader.hasClose) // application wants to close
ctx.requestProtocol match {
case `HTTP/1.0` if !close r ~~ Connection ~~ KeepAlive ~~ CrLf
case `HTTP/1.1` if close r ~~ Connection ~~ Close ~~ CrLf
case `HTTP/1.0` if !close r ~~ Connection ~~ KeepAliveBytes ~~ CrLf
case `HTTP/1.1` if close r ~~ Connection ~~ CloseBytes ~~ CrLf
case _ // no need for rendering
}
}
def byteStrings(entityBytes: Publisher[ByteString]): immutable.Seq[Publisher[ByteString]] =
def byteStrings(entityBytes: Publisher[ByteString]): List[Publisher[ByteString]] =
renderByteStrings(r, entityBytes, materializer, skipEntity = noEntity)
def completeResponseRendering(entity: HttpEntity): immutable.Seq[Publisher[ByteString]] =
def completeResponseRendering(entity: HttpEntity): List[Publisher[ByteString]] =
entity match {
case HttpEntity.Strict(contentType, data)
case HttpEntity.Strict(_, data)
renderHeaders(headers.toList)
renderEntityContentType(r, entity)
r ~~ `Content-Length` ~~ data.length ~~ CrLf ~~ CrLf
val entityBytes = if (noEntity) Nil else data :: Nil
SynchronousPublisherFromIterable(r.get :: entityBytes) :: Nil
case HttpEntity.Default(contentType, contentLength, data)
case HttpEntity.Default(_, contentLength, data)
renderHeaders(headers.toList)
renderEntityContentType(r, entity)
r ~~ `Content-Length` ~~ contentLength ~~ CrLf ~~ CrLf
byteStrings(Flow(data).transform(new CheckContentLengthTransformer(contentLength)).toPublisher(materializer))
case HttpEntity.CloseDelimited(contentType, data)
case HttpEntity.CloseDelimited(_, data)
renderHeaders(headers.toList, alwaysClose = true)
renderEntityContentType(r, entity)
r ~~ CrLf
@ -152,7 +147,7 @@ private[http] class HttpResponseRendererFactory(serverHeader: Option[headers.Ser
renderHeaders(headers.toList)
renderEntityContentType(r, entity)
if (!entity.isKnownEmpty || ctx.requestMethod == HttpMethods.HEAD)
r ~~ `Transfer-Encoding` ~~ Chunked ~~ CrLf
r ~~ `Transfer-Encoding` ~~ ChunkedBytes ~~ CrLf
r ~~ CrLf
byteStrings(Flow(chunks).transform(new ChunkTransformer).toPublisher(materializer))
}

View file

@ -5,7 +5,6 @@
package akka.http.rendering
import org.reactivestreams.Publisher
import scala.collection.immutable
import akka.parboiled2.CharUtils
import akka.util.ByteString
import akka.event.LoggingAdapter
@ -19,11 +18,11 @@ import akka.http.util._
* INTERNAL API
*/
private object RenderSupport {
val DefaultStatusLine = "HTTP/1.1 200 OK\r\n".getAsciiBytes
val StatusLineStart = "HTTP/1.1 ".getAsciiBytes
val Chunked = "chunked".getAsciiBytes
val KeepAlive = "Keep-Alive".getAsciiBytes
val Close = "close".getAsciiBytes
val DefaultStatusLineBytes = "HTTP/1.1 200 OK\r\n".getAsciiBytes
val StatusLineStartBytes = "HTTP/1.1 ".getAsciiBytes
val ChunkedBytes = "chunked".getAsciiBytes
val KeepAliveBytes = "Keep-Alive".getAsciiBytes
val CloseBytes = "close".getAsciiBytes
def CrLf = Rendering.CrLf
@ -36,7 +35,7 @@ private object RenderSupport {
r ~~ headers.`Content-Type` ~~ entity.contentType ~~ CrLf
def renderByteStrings(r: ByteStringRendering, entityBytes: Publisher[ByteString], materializer: FlowMaterializer,
skipEntity: Boolean = false): immutable.Seq[Publisher[ByteString]] = {
skipEntity: Boolean = false): List[Publisher[ByteString]] = {
val messageStart = SynchronousPublisherFromIterable(r.get :: Nil)
val messageBytes =
if (!skipEntity) Flow(messageStart).concat(entityBytes).toPublisher(materializer)
@ -46,7 +45,7 @@ private object RenderSupport {
class ChunkTransformer extends Transformer[HttpEntity.ChunkStreamPart, ByteString] {
var lastChunkSeen = false
def onNext(chunk: HttpEntity.ChunkStreamPart): immutable.Seq[ByteString] = {
def onNext(chunk: HttpEntity.ChunkStreamPart): List[ByteString] = {
if (chunk.isLastChunk) lastChunkSeen = true
renderChunk(chunk) :: Nil
}
@ -56,14 +55,14 @@ private object RenderSupport {
class CheckContentLengthTransformer(length: Long) extends Transformer[ByteString, ByteString] {
var sent = 0L
def onNext(elem: ByteString): immutable.Seq[ByteString] = {
def onNext(elem: ByteString): List[ByteString] = {
sent += elem.length
if (sent > length)
throw new InvalidContentLengthException(s"HTTP message had declared Content-Length $length but entity chunk stream amounts to more bytes")
elem :: Nil
}
override def onTermination(e: Option[Throwable]): immutable.Seq[ByteString] = {
override def onTermination(e: Option[Throwable]): List[ByteString] = {
if (sent < length)
throw new InvalidContentLengthException(s"HTTP message had declared Content-Length $length but entity chunk stream amounts to ${length - sent} bytes less")
Nil

View file

@ -1,127 +0,0 @@
/**
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.http.model
import org.scalatest.{ Matchers, FreeSpec }
import akka.http.model.parser.HeaderParser
import headers._
import MediaTypes._
import HttpCharsets._
class ContentNegotiationSpec extends FreeSpec with Matchers {
"Content Negotiation should work properly for requests with header(s)" - {
"(without headers)" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
}
"Accept: */*" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
}
"Accept: */*;q=.8" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
}
"Accept: text/*" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`)
accept(`audio/ogg`) should reject
}
"Accept: text/*;q=.8" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`)
accept(`audio/ogg`) should reject
}
"Accept: text/*;q=0" in test { accept
accept(`text/plain`) should reject
accept(`text/xml` withCharset `UTF-16`) should reject
accept(`audio/ogg`) should reject
}
"Accept-Charset: UTF-16" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-16`)
accept(`text/plain` withCharset `UTF-8`) should reject
}
"Accept-Charset: UTF-16, UTF-8" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
}
"Accept-Charset: UTF-8;q=.2, UTF-16" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-16`)
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
}
"Accept-Charset: UTF-8;q=.2" in test { accept
accept(`text/plain`) should select(`text/plain`, `ISO-8859-1`)
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
}
"Accept-Charset: latin1;q=.1, UTF-8;q=.2" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
}
"Accept-Charset: *" in test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
}
"Accept-Charset: *;q=0" in test { accept
accept(`text/plain`) should reject
accept(`text/plain` withCharset `UTF-16`) should reject
}
"Accept-Charset: us;q=0.1,*;q=0" in test { accept
accept(`text/plain`) should select(`text/plain`, `US-ASCII`)
accept(`text/plain` withCharset `UTF-8`) should reject
}
"Accept: text/xml, text/html;q=.5" in test { accept
accept(`text/plain`) should reject
accept(`text/xml`) should select(`text/xml`, `UTF-8`)
accept(`text/html`) should select(`text/html`, `UTF-8`)
accept(`text/html`, `text/xml`) should select(`text/xml`, `UTF-8`)
accept(`text/xml`, `text/html`) should select(`text/xml`, `UTF-8`)
accept(`text/plain`, `text/xml`) should select(`text/xml`, `UTF-8`)
accept(`text/plain`, `text/html`) should select(`text/html`, `UTF-8`)
}
"""Accept: text/html, text/plain;q=0.8, application/*;q=.5, *;q= .2
Accept-Charset: UTF-16""" in test { accept
accept(`text/plain`, `text/html`, `audio/ogg`) should select(`text/html`, `UTF-16`)
accept(`text/plain`, `text/html` withCharset `UTF-8`, `audio/ogg`) should select(`text/plain`, `UTF-16`)
accept(`audio/ogg`, `application/javascript`, `text/plain` withCharset `UTF-8`) should select(`application/javascript`, `UTF-16`)
accept(`image/gif`, `application/javascript`) should select(`application/javascript`, `UTF-16`)
accept(`image/gif`, `audio/ogg`) should select(`image/gif`, `UTF-16`)
}
}
def test[U](body: ((ContentType*) Option[ContentType]) U): String U = { example
val headers =
if (example != "(without headers)") {
example.split('\n').toList map { rawHeader
val Array(name, value) = rawHeader.split(':')
HeaderParser.parseHeader(RawHeader(name.trim, value.trim)) match {
case Right(header) header
case Left(err) fail(err.formatPretty)
}
}
} else Nil
val request = HttpRequest(headers = headers)
body(contentTypes request.acceptableContentType(Vector(contentTypes: _*)))
}
def reject = equal(None)
def select(mediaType: MediaType, charset: HttpCharset) = equal(Some(ContentType(mediaType, charset)))
}