!htp #19034 refactor content negotiation, upgrade to new MediaType / ContentType model
This commit is contained in:
parent
e9240b7d86
commit
899b92faf2
46 changed files with 652 additions and 403 deletions
|
|
@ -35,7 +35,7 @@ public class ModelDocTest {
|
|||
Authorization authorization = Authorization.basic("user", "pass");
|
||||
HttpRequest complexRequest =
|
||||
HttpRequest.PUT("/user")
|
||||
.withEntity(HttpEntities.create(MediaTypes.TEXT_PLAIN.toContentType(), "abc"))
|
||||
.withEntity(HttpEntities.create(ContentTypes.TEXT_PLAIN_UTF8, "abc"))
|
||||
.addHeader(authorization)
|
||||
.withProtocol(HttpProtocols.HTTP_1_0);
|
||||
//#construct-request
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public class HighLevelServerExample extends HttpApp {
|
|||
// matches the empty path
|
||||
pathSingleSlash().route(
|
||||
// return a constant string with a certain content type
|
||||
complete(ContentTypes.TEXT_HTML,
|
||||
complete(ContentTypes.TEXT_HTML_UTF8,
|
||||
"<html><body>Hello world!</body></html>")
|
||||
),
|
||||
path("ping").route(
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ public class HttpServerExampleDocTest {
|
|||
.via(failureDetection)
|
||||
.map(request -> {
|
||||
Source<ByteString, Object> bytes = request.entity().getDataBytes();
|
||||
HttpEntity.Chunked entity = HttpEntities.create(ContentTypes.TEXT_PLAIN, bytes);
|
||||
HttpEntityChunked entity = HttpEntities.create(ContentTypes.TEXT_PLAIN_UTF8, bytes);
|
||||
|
||||
return HttpResponse.create()
|
||||
.withEntity(entity);
|
||||
|
|
@ -199,7 +199,7 @@ public class HttpServerExampleDocTest {
|
|||
if (uri.path().equals("/"))
|
||||
return
|
||||
HttpResponse.create()
|
||||
.withEntity(ContentTypes.TEXT_HTML,
|
||||
.withEntity(ContentTypes.TEXT_HTML_UTF8,
|
||||
"<html><body>Hello world!</body></html>");
|
||||
else if (uri.path().equals("/hello")) {
|
||||
String name = Util.getOrElse(uri.query().get("name"), "Mister X");
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ class HttpServerExampleSpec extends WordSpec with Matchers {
|
|||
.via(reactToConnectionFailure)
|
||||
.map { request =>
|
||||
// simple text "echo" response:
|
||||
HttpResponse(entity = HttpEntity(ContentTypes.`text/plain`, request.entity.dataBytes))
|
||||
HttpResponse(entity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, request.entity.dataBytes))
|
||||
}
|
||||
|
||||
serverSource
|
||||
|
|
@ -173,7 +173,7 @@ class HttpServerExampleSpec extends WordSpec with Matchers {
|
|||
|
||||
val requestHandler: HttpRequest => HttpResponse = {
|
||||
case HttpRequest(GET, Uri.Path("/"), _, _, _) =>
|
||||
HttpResponse(entity = HttpEntity(MediaTypes.`text/html`,
|
||||
HttpResponse(entity = HttpEntity(ContentTypes.`text/html(UTF-8)`,
|
||||
"<html><body>Hello world!</body></html>"))
|
||||
|
||||
case HttpRequest(GET, Uri.Path("/ping"), _, _, _) =>
|
||||
|
|
@ -207,7 +207,7 @@ class HttpServerExampleSpec extends WordSpec with Matchers {
|
|||
|
||||
val requestHandler: HttpRequest => HttpResponse = {
|
||||
case HttpRequest(GET, Uri.Path("/"), _, _, _) =>
|
||||
HttpResponse(entity = HttpEntity(MediaTypes.`text/html`,
|
||||
HttpResponse(entity = HttpEntity(ContentTypes.`text/html(UTF-8)`,
|
||||
"<html><body>Hello world!</body></html>"))
|
||||
|
||||
case HttpRequest(GET, Uri.Path("/ping"), _, _, _) =>
|
||||
|
|
|
|||
|
|
@ -32,12 +32,13 @@ class ModelSpec extends AkkaSpec {
|
|||
// customize every detail of HTTP request
|
||||
import HttpProtocols._
|
||||
import MediaTypes._
|
||||
import HttpCharsets._
|
||||
val userData = ByteString("abc")
|
||||
val authorization = headers.Authorization(BasicHttpCredentials("user", "pass"))
|
||||
HttpRequest(
|
||||
PUT,
|
||||
uri = "/user",
|
||||
entity = HttpEntity(`text/plain`, userData),
|
||||
entity = HttpEntity(`text/plain` withCharset `UTF-8`, userData),
|
||||
headers = List(authorization),
|
||||
protocol = `HTTP/1.0`)
|
||||
//#construct-request
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class CodingDirectivesExamplesSpec extends RoutingSpec {
|
|||
Get("/") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "content"
|
||||
}
|
||||
Get("/") ~> `Accept-Encoding`(`identity;q=MIN`) ~> route ~> check {
|
||||
Get("/") ~> `Accept-Encoding`(deflate) ~> route ~> check {
|
||||
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
|
||||
}
|
||||
}
|
||||
|
|
@ -48,9 +48,6 @@ class CodingDirectivesExamplesSpec extends RoutingSpec {
|
|||
Get("/") ~> route ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
}
|
||||
Get("/") ~> `Accept-Encoding`() ~> route ~> check {
|
||||
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
|
||||
}
|
||||
Get("/") ~> `Accept-Encoding`(gzip, deflate) ~> route ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,11 @@
|
|||
*/
|
||||
package docs.http.scaladsl.server.directives
|
||||
|
||||
import java.io.File
|
||||
|
||||
import akka.http.scaladsl.model.{ MediaTypes, HttpEntity, Multipart, StatusCodes }
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.stream.io.Framing
|
||||
import akka.util.ByteString
|
||||
import docs.http.scaladsl.server.RoutingSpec
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.{ Success, Failure }
|
||||
|
||||
class FileUploadDirectivesExamplesSpec extends RoutingSpec {
|
||||
|
||||
|
|
@ -32,7 +28,7 @@ class FileUploadDirectivesExamplesSpec extends RoutingSpec {
|
|||
Multipart.FormData(
|
||||
Multipart.FormData.BodyPart.Strict(
|
||||
"csv",
|
||||
HttpEntity(MediaTypes.`text/plain`, "1,5,7\n11,13,17"),
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "1,5,7\n11,13,17"),
|
||||
Map("filename" -> "data.csv")))
|
||||
|
||||
Post("/", multipartForm) ~> route ~> check {
|
||||
|
|
@ -68,7 +64,7 @@ class FileUploadDirectivesExamplesSpec extends RoutingSpec {
|
|||
val multipartForm =
|
||||
Multipart.FormData(Multipart.FormData.BodyPart.Strict(
|
||||
"csv",
|
||||
HttpEntity(MediaTypes.`text/plain`, "2,3,5\n7,11,13,17,23\n29,31,37\n"),
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "2,3,5\n7,11,13,17,23\n29,31,37\n"),
|
||||
Map("filename" -> "primes.csv")))
|
||||
|
||||
Post("/", multipartForm) ~> route ~> check {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@
|
|||
package akka.http.scaladsl.marshallers.sprayjson
|
||||
|
||||
import scala.language.implicitConversions
|
||||
import akka.stream.Materializer
|
||||
import akka.http.scaladsl.marshalling.{ ToEntityMarshaller, Marshaller }
|
||||
import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshaller }
|
||||
import akka.http.scaladsl.model.{ ContentTypes, HttpCharsets }
|
||||
import akka.http.scaladsl.model.{ MediaTypes, HttpCharsets }
|
||||
import akka.http.scaladsl.model.MediaTypes.`application/json`
|
||||
import spray.json._
|
||||
|
||||
|
|
@ -33,6 +32,6 @@ trait SprayJsonSupport {
|
|||
implicit def sprayJsonMarshaller[T](implicit writer: RootJsonWriter[T], printer: JsonPrinter = PrettyPrinter): ToEntityMarshaller[T] =
|
||||
sprayJsValueMarshaller compose writer.write
|
||||
implicit def sprayJsValueMarshaller(implicit printer: JsonPrinter = PrettyPrinter): ToEntityMarshaller[JsValue] =
|
||||
Marshaller.StringMarshaller.wrap(ContentTypes.`application/json`)(printer)
|
||||
Marshaller.StringMarshaller.wrap(MediaTypes.`application/json`)(printer)
|
||||
}
|
||||
object SprayJsonSupport extends SprayJsonSupport
|
||||
|
|
@ -8,7 +8,6 @@ import java.io.{ ByteArrayInputStream, InputStreamReader }
|
|||
import javax.xml.parsers.{ SAXParserFactory, SAXParser }
|
||||
import scala.collection.immutable
|
||||
import scala.xml.{ XML, NodeSeq }
|
||||
import akka.stream.Materializer
|
||||
import akka.http.scaladsl.unmarshalling._
|
||||
import akka.http.scaladsl.marshalling._
|
||||
import akka.http.scaladsl.model._
|
||||
|
|
@ -16,10 +15,10 @@ import MediaTypes._
|
|||
|
||||
trait ScalaXmlSupport {
|
||||
implicit def defaultNodeSeqMarshaller: ToEntityMarshaller[NodeSeq] =
|
||||
Marshaller.oneOf(ScalaXmlSupport.nodeSeqContentTypes.map(nodeSeqMarshaller): _*)
|
||||
Marshaller.oneOf(ScalaXmlSupport.nodeSeqMediaTypes.map(nodeSeqMarshaller): _*)
|
||||
|
||||
def nodeSeqMarshaller(contentType: ContentType): ToEntityMarshaller[NodeSeq] =
|
||||
Marshaller.StringMarshaller.wrap(contentType)(_.toString())
|
||||
def nodeSeqMarshaller(mediaType: MediaType.NonBinary): ToEntityMarshaller[NodeSeq] =
|
||||
Marshaller.StringMarshaller.wrap(mediaType)(_.toString())
|
||||
|
||||
implicit def defaultNodeSeqUnmarshaller: FromEntityUnmarshaller[NodeSeq] =
|
||||
nodeSeqUnmarshaller(ScalaXmlSupport.nodeSeqContentTypeRanges: _*)
|
||||
|
|
@ -35,13 +34,12 @@ trait ScalaXmlSupport {
|
|||
/**
|
||||
* Provides a SAXParser for the NodeSeqUnmarshaller to use. Override to provide a custom SAXParser implementation.
|
||||
* Will be called once for for every request to be unmarshalled. The default implementation calls [[ScalaXmlSupport.createSaferSAXParser]].
|
||||
* @return
|
||||
*/
|
||||
protected def createSAXParser(): SAXParser = ScalaXmlSupport.createSaferSAXParser()
|
||||
}
|
||||
object ScalaXmlSupport extends ScalaXmlSupport {
|
||||
val nodeSeqContentTypes: immutable.Seq[ContentType] = List(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`)
|
||||
val nodeSeqContentTypeRanges: immutable.Seq[ContentTypeRange] = nodeSeqContentTypes.map(ContentTypeRange(_))
|
||||
val nodeSeqMediaTypes: immutable.Seq[MediaType.NonBinary] = List(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`)
|
||||
val nodeSeqContentTypeRanges: immutable.Seq[ContentTypeRange] = nodeSeqMediaTypes.map(ContentTypeRange(_))
|
||||
|
||||
/** Creates a safer SAXParser. */
|
||||
def createSaferSAXParser(): SAXParser = {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
package akka.http.javadsl.server.values;
|
||||
|
||||
import akka.http.javadsl.model.HttpCharsets;
|
||||
import akka.http.javadsl.model.HttpRequest;
|
||||
import akka.http.javadsl.model.MediaTypes;
|
||||
import akka.http.javadsl.server.RequestVal;
|
||||
|
|
@ -50,7 +51,7 @@ public class FormFieldsTest extends JUnitRouteTest {
|
|||
|
||||
return
|
||||
HttpRequest.POST("/test")
|
||||
.withEntity(MediaTypes.APPLICATION_X_WWW_FORM_URLENCODED.toContentType(), sb.toString());
|
||||
.withEntity(MediaTypes.APPLICATION_X_WWW_FORM_URLENCODED.toContentType(HttpCharsets.UTF_8), sb.toString());
|
||||
}
|
||||
private HttpRequest singleParameterUrlEncodedRequest(String name, String value) {
|
||||
return urlEncodedRequest(entry(name, value));
|
||||
|
|
|
|||
|
|
@ -7,16 +7,15 @@ package akka.http.scaladsl.marshallers.xml
|
|||
import java.io.File
|
||||
|
||||
import akka.http.scaladsl.TestUtils
|
||||
import scala.concurrent.duration._
|
||||
import org.xml.sax.SAXParseException
|
||||
|
||||
import scala.concurrent.{ Future, Await }
|
||||
import scala.xml.NodeSeq
|
||||
import scala.concurrent.{ Future, Await }
|
||||
import scala.concurrent.duration._
|
||||
import org.scalatest.{ Inside, FreeSpec, Matchers }
|
||||
import akka.util.ByteString
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import akka.http.scaladsl.unmarshalling.{ Unmarshaller, Unmarshal }
|
||||
import akka.http.scaladsl.model._
|
||||
import HttpCharsets._
|
||||
import MediaTypes._
|
||||
|
||||
class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest with Inside {
|
||||
|
|
@ -25,13 +24,13 @@ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest
|
|||
"NodeSeqMarshaller should" - {
|
||||
"marshal xml snippets to `text/xml` content in UTF-8" in {
|
||||
marshal(<employee><nr>Ha“llo</nr></employee>) shouldEqual
|
||||
HttpEntity(ContentType(`text/xml`, `UTF-8`), "<employee><nr>Ha“llo</nr></employee>")
|
||||
HttpEntity(ContentTypes.`text/xml(UTF-8)`, "<employee><nr>Ha“llo</nr></employee>")
|
||||
}
|
||||
"unmarshal `text/xml` content in UTF-8 to NodeSeqs" in {
|
||||
Unmarshal(HttpEntity(`text/xml`, "<int>Hällö</int>")).to[NodeSeq].map(_.text) should evaluateTo("Hällö")
|
||||
Unmarshal(HttpEntity(ContentTypes.`text/xml(UTF-8)`, "<int>Hällö</int>")).to[NodeSeq].map(_.text) should evaluateTo("Hällö")
|
||||
}
|
||||
"reject `application/octet-stream`" in {
|
||||
Unmarshal(HttpEntity(`application/octet-stream`, "<int>Hällö</int>")).to[NodeSeq].map(_.text) should
|
||||
Unmarshal(HttpEntity(`application/octet-stream`, ByteString("<int>Hällö</int>"))).to[NodeSeq].map(_.text) should
|
||||
haveFailedWith(Unmarshaller.UnsupportedContentTypeException(nodeSeqContentTypeRanges: _*))
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +42,7 @@ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest
|
|||
| <!ELEMENT foo ANY >
|
||||
| <!ENTITY xxe SYSTEM "${f.toURI}">]><foo>hello&xxe;</foo>""".stripMargin
|
||||
|
||||
shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(`text/xml`, xml)).to[NodeSeq])
|
||||
shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(ContentTypes.`text/xml(UTF-8)`, xml)).to[NodeSeq])
|
||||
}
|
||||
}
|
||||
"parse XML bodies without loading in a related schema from a parameter" in {
|
||||
|
|
@ -58,7 +57,7 @@ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest
|
|||
| %xpe;
|
||||
| %pe;
|
||||
| ]><foo>hello&xxe;</foo>""".stripMargin
|
||||
shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(`text/xml`, xml)).to[NodeSeq])
|
||||
shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(ContentTypes.`text/xml(UTF-8)`, xml)).to[NodeSeq])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -73,7 +72,7 @@ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest
|
|||
| ]>
|
||||
| <billion>&laugh30;</billion>""".stripMargin
|
||||
|
||||
shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(`text/xml`, xml)).to[NodeSeq])
|
||||
shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(ContentTypes.`text/xml(UTF-8)`, xml)).to[NodeSeq])
|
||||
}
|
||||
"gracefully fail when an entity expands to be very large" in {
|
||||
val as = "a" * 50000
|
||||
|
|
@ -83,7 +82,7 @@ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest
|
|||
| <!ENTITY a "$as">
|
||||
| ]>
|
||||
| <kaboom>$entities</kaboom>""".stripMargin
|
||||
shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(`text/xml`, xml)).to[NodeSeq])
|
||||
shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(ContentTypes.`text/xml(UTF-8)`, xml)).to[NodeSeq])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import akka.http.scaladsl.model.MediaType.Encoding
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
import akka.http.scaladsl.server.ContentNegotiator.Alternative
|
||||
import akka.util.ByteString
|
||||
import org.scalatest.{ Matchers, FreeSpec }
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
import akka.http.scaladsl.model._
|
||||
|
|
@ -19,29 +19,30 @@ class ContentNegotiationSpec extends FreeSpec with Matchers {
|
|||
"Content Negotiation should work properly for requests with header(s)" - {
|
||||
|
||||
"(without headers)" 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/plain` withCharset `UTF-16`) should select(`text/plain` withCharset `UTF-16`)
|
||||
accept(`text/plain`, `text/html`) should select(`text/plain` withCharset `UTF-8`)
|
||||
accept(`text/html`, `text/plain`) should select(`text/html` withCharset `UTF-8`)
|
||||
}
|
||||
|
||||
"Accept: */*" 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/plain`) should select(`text/plain` withCharset `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain` withCharset `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept: */*;q=.8" 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/plain`) should select(`text/plain` withCharset `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain` withCharset `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept: text/*" test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`)
|
||||
accept(`text/plain`) should select(`text/plain` withCharset `UTF-8`)
|
||||
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml` withCharset `UTF-16`)
|
||||
accept(`audio/ogg`) should reject
|
||||
}
|
||||
|
||||
"Accept: text/*;q=.8" test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`)
|
||||
accept(`text/plain`) should select(`text/plain` withCharset `UTF-8`)
|
||||
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml` withCharset `UTF-16`)
|
||||
accept(`audio/ogg`) should reject
|
||||
}
|
||||
|
||||
|
|
@ -51,39 +52,43 @@ class ContentNegotiationSpec extends FreeSpec with Matchers {
|
|||
accept(`audio/ogg`) should reject
|
||||
}
|
||||
|
||||
"Accept: text/*, application/json;q=0.8, text/plain;q=0.5" test { accept ⇒
|
||||
accept(`text/plain`, `application/json`) should select(`application/json`)
|
||||
}
|
||||
|
||||
"Accept-Charset: UTF-16" test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-16`)
|
||||
accept(`text/plain`) should select(`text/plain` withCharset `UTF-16`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should reject
|
||||
}
|
||||
|
||||
"manually created Accept-Charset: UTF-16" in testHeaders(headers.`Accept-Charset`(Vector(HttpCharsets.`UTF-16`.toRange))) { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-16`)
|
||||
accept(`text/plain`) should select(`text/plain` withCharset `UTF-16`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should reject
|
||||
}
|
||||
|
||||
"Accept-Charset: UTF-16, UTF-8" 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/plain`) should select(`text/plain` withCharset `UTF-16`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain` withCharset `UTF-8`)
|
||||
}
|
||||
|
||||
"Accept-Charset: UTF-8;q=.2, UTF-16" test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-16`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain`) should select(`text/plain` withCharset `UTF-16`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain` withCharset `UTF-8`)
|
||||
}
|
||||
|
||||
"Accept-Charset: ISO-8859-1;q=.2" test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `ISO-8859-1`)
|
||||
accept(`text/plain`) should select(`text/plain` withCharset `ISO-8859-1`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should reject
|
||||
}
|
||||
|
||||
"Accept-Charset: latin1;q=.1, UTF-8;q=.2" test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain`) should select(`text/plain` withCharset `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain` withCharset `UTF-8`)
|
||||
}
|
||||
|
||||
"Accept-Charset: *" 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/plain`) should select(`text/plain` withCharset `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain` withCharset `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept-Charset: *;q=0" test { accept ⇒
|
||||
|
|
@ -92,47 +97,53 @@ class ContentNegotiationSpec extends FreeSpec with Matchers {
|
|||
}
|
||||
|
||||
"Accept-Charset: us;q=0.1,*;q=0" test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `US-ASCII`)
|
||||
accept(`text/plain`) should select(`text/plain` withCharset `US-ASCII`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should reject
|
||||
}
|
||||
|
||||
"Accept-Charset: UTF-8, *;q=0.8, us;q=0.1" test { accept ⇒
|
||||
accept(`text/plain` withCharset `US-ASCII`,
|
||||
`text/plain` withCharset `ISO-8859-1`) should select(`text/plain` withCharset `ISO-8859-1`)
|
||||
}
|
||||
|
||||
"Accept: text/xml, text/html;q=.5" 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/xml`) should select(`text/xml` withCharset `UTF-8`)
|
||||
accept(`text/html`) should select(`text/html` withCharset `UTF-8`)
|
||||
accept(`text/html`, `text/xml`) should select(`text/xml` withCharset `UTF-8`)
|
||||
accept(`text/xml`, `text/html`) should select(`text/xml` withCharset `UTF-8`)
|
||||
accept(`text/plain`, `text/xml`) should select(`text/xml` withCharset `UTF-8`)
|
||||
accept(`text/plain`, `text/html`) should select(`text/html` withCharset `UTF-8`)
|
||||
}
|
||||
|
||||
"""Accept: text/html, text/plain;q=0.8, application/*;q=.5, *;q= .2
|
||||
|Accept-Charset: UTF-16""" 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(`text/plain`, `text/html`, `audio/ogg`) should select(`text/html` withCharset `UTF-16`)
|
||||
accept(`text/plain`, `text/html` withCharset `UTF-8`, `audio/ogg`) should select(`text/plain` withCharset `UTF-16`)
|
||||
accept(`audio/ogg`, `application/javascript`, `text/plain` withCharset `UTF-8`) should select(`application/javascript` withCharset `UTF-16`)
|
||||
accept(`image/gif`, `application/javascript`) should select(`application/javascript` withCharset `UTF-16`)
|
||||
accept(`image/gif`, `audio/ogg`) should select(`image/gif`)
|
||||
}
|
||||
|
||||
"Accept: text/xml, text/plain" test { accept ⇒
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain` withCharset `UTF-16`)
|
||||
accept(`text/plain`, `text/xml`) should select(`text/plain` withCharset `UTF-8`)
|
||||
accept(`text/xml`, `text/plain`) should select(`text/xml` withCharset `UTF-8`)
|
||||
}
|
||||
}
|
||||
|
||||
def testHeaders[U](headers: HttpHeader*)(body: ((ContentType*) ⇒ Option[ContentType]) ⇒ U): U = {
|
||||
def testHeaders[U](headers: HttpHeader*)(body: ((Alternative*) ⇒ Option[ContentType]) ⇒ U): U = {
|
||||
val request = HttpRequest(headers = headers.toVector)
|
||||
body { contentTypes ⇒
|
||||
body { alternatives ⇒
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
||||
// creates a pseudo marshaller for X, that applies for all the given content types
|
||||
trait X
|
||||
object X extends X
|
||||
implicit val marshallers: ToEntityMarshaller[X] =
|
||||
Marshaller.oneOf(contentTypes map {
|
||||
case ct @ ContentType(mt, Some(cs)) ⇒
|
||||
Marshaller.withFixedCharset(mt, cs)((s: X) ⇒ HttpEntity(ct, "The X"))
|
||||
case ContentType(mt, None) ⇒
|
||||
mt.encoding match {
|
||||
case Encoding.Open ⇒ Marshaller.withOpenCharset(mt)((s: X, cs) ⇒ HttpEntity(ContentType(mt, cs), "The X"))
|
||||
case _ ⇒ Marshaller.withOpenCharset(mt)((s: X, _) ⇒ HttpEntity(ContentType(mt, None), "The X"))
|
||||
}
|
||||
Marshaller.oneOf(alternatives map {
|
||||
case Alternative.ContentType(ct) ⇒ Marshaller.withFixedContentType(ct)((s: X) ⇒ HttpEntity(ct, ByteString("The X")))
|
||||
case Alternative.MediaType(mt) ⇒ Marshaller.withOpenCharset(mt)((s: X, cs) ⇒ HttpEntity(mt withCharset cs, "The X"))
|
||||
}: _*)
|
||||
|
||||
Await.result(Marshal(X).toResponseFor(request)
|
||||
|
|
@ -142,11 +153,10 @@ class ContentNegotiationSpec extends FreeSpec with Matchers {
|
|||
}
|
||||
|
||||
def reject = equal(None)
|
||||
def select(mediaType: MediaType, charset: HttpCharset) = equal(Some(ContentType(mediaType, charset)))
|
||||
def select(mediaType: MediaType) = equal(Some(ContentType(mediaType, None)))
|
||||
def select(contentType: ContentType) = equal(Some(contentType))
|
||||
|
||||
implicit class AddStringToIn(example: String) {
|
||||
def test(body: ((ContentType*) ⇒ Option[ContentType]) ⇒ Unit): Unit = example in {
|
||||
def test(body: ((Alternative*) ⇒ Option[ContentType]) ⇒ Unit): Unit = example in {
|
||||
val headers =
|
||||
if (example != "(without headers)") {
|
||||
example.stripMarginWithNewline("\n").split('\n').toList map { rawHeader ⇒
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import akka.http.scaladsl.testkit.MarshallingTestUtils
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
|
||||
|
||||
import scala.collection.immutable.ListMap
|
||||
import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers }
|
||||
import akka.util.ByteString
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorMaterializer
|
||||
import akka.stream.scaladsl.Source
|
||||
import akka.http.scaladsl.testkit.MarshallingTestUtils
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
|
||||
import akka.http.impl.util._
|
||||
import akka.http.scaladsl.model._
|
||||
import headers._
|
||||
|
|
@ -32,7 +32,7 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with
|
|||
}
|
||||
"FormDataMarshaller should marshal FormData instances to application/x-www-form-urlencoded content" in {
|
||||
marshal(FormData(Map("name" -> "Bob", "pass" -> "hällo", "admin" -> ""))) shouldEqual
|
||||
HttpEntity(ContentType(`application/x-www-form-urlencoded`, `UTF-8`), "name=Bob&pass=h%C3%A4llo&admin=")
|
||||
HttpEntity(`application/x-www-form-urlencoded` withCharset `UTF-8`, "name=Bob&pass=h%C3%A4llo&admin=")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with
|
|||
"multipartMarshaller should correctly marshal multipart content with" - {
|
||||
"one empty part" in {
|
||||
marshal(Multipart.General(`multipart/mixed`, Multipart.General.BodyPart.Strict(""))) shouldEqual HttpEntity(
|
||||
contentType = ContentType(`multipart/mixed` withBoundary randomBoundary),
|
||||
contentType = `multipart/mixed` withBoundary randomBoundary withCharset `UTF-8`,
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/plain; charset=UTF-8
|
||||
|
|
||||
|
|
@ -61,10 +61,10 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with
|
|||
}
|
||||
"one non-empty part" in {
|
||||
marshal(Multipart.General(`multipart/alternative`, Multipart.General.BodyPart.Strict(
|
||||
entity = HttpEntity(ContentType(`text/plain`, `UTF-8`), "test@there.com"),
|
||||
entity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, "test@there.com"),
|
||||
headers = `Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")) :: Nil))) shouldEqual
|
||||
HttpEntity(
|
||||
contentType = ContentType(`multipart/alternative` withBoundary randomBoundary),
|
||||
contentType = `multipart/alternative` withBoundary randomBoundary withCharset `UTF-8`,
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/plain; charset=UTF-8
|
||||
|Content-Disposition: form-data; name=email
|
||||
|
|
@ -74,12 +74,12 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with
|
|||
}
|
||||
"two different parts" in {
|
||||
marshal(Multipart.General(`multipart/related`,
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentType(`text/plain`, Some(`US-ASCII`)), "first part, with a trailing linebreak\r\n")),
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(`text/plain` withCharset `US-ASCII`, "first part, with a trailing linebreak\r\n")),
|
||||
Multipart.General.BodyPart.Strict(
|
||||
HttpEntity(ContentType(`application/octet-stream`), "filecontent"),
|
||||
HttpEntity(`application/octet-stream`, ByteString("filecontent")),
|
||||
RawHeader("Content-Transfer-Encoding", "binary") :: Nil))) shouldEqual
|
||||
HttpEntity(
|
||||
contentType = ContentType(`multipart/related` withBoundary randomBoundary),
|
||||
contentType = `multipart/related` withBoundary randomBoundary withCharset `UTF-8`,
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/plain; charset=US-ASCII
|
||||
|
|
||||
|
|
@ -100,7 +100,7 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with
|
|||
"surname" -> HttpEntity("Mike"),
|
||||
"age" -> marshal(<int>42</int>)))) shouldEqual
|
||||
HttpEntity(
|
||||
contentType = ContentType(`multipart/form-data` withBoundary randomBoundary),
|
||||
contentType = `multipart/form-data` withBoundary randomBoundary withCharset `UTF-8`,
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/plain; charset=UTF-8
|
||||
|Content-Disposition: form-data; name=surname
|
||||
|
|
@ -116,14 +116,14 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with
|
|||
|
||||
"two fields having a custom `Content-Disposition`" in {
|
||||
marshal(Multipart.FormData(Source(List(
|
||||
Multipart.FormData.BodyPart("attachment[0]", HttpEntity(`text/csv`, "name,age\r\n\"John Doe\",20\r\n"),
|
||||
Multipart.FormData.BodyPart("attachment[0]", HttpEntity(`text/csv` withCharset `UTF-8`, "name,age\r\n\"John Doe\",20\r\n"),
|
||||
Map("filename" -> "attachment.csv")),
|
||||
Multipart.FormData.BodyPart("attachment[1]", HttpEntity("naice!".getBytes),
|
||||
Map("filename" -> "attachment2.csv"), List(RawHeader("Content-Transfer-Encoding", "binary"))))))) shouldEqual
|
||||
HttpEntity(
|
||||
contentType = ContentType(`multipart/form-data` withBoundary randomBoundary),
|
||||
contentType = `multipart/form-data` withBoundary randomBoundary withCharset `UTF-8`,
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/csv
|
||||
|Content-Type: text/csv; charset=UTF-8
|
||||
|Content-Disposition: form-data; filename=attachment.csv; name="attachment[0]"
|
||||
|
|
||||
|name,age
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ class CodingDirectivesSpec extends RoutingSpec with Inside {
|
|||
() ⇒ text.grouped(8).map { chars ⇒
|
||||
Chunk(chars.mkString): ChunkStreamPart
|
||||
}
|
||||
val chunkedTextEntity = HttpEntity.Chunked(MediaTypes.`text/plain`, Source(textChunks))
|
||||
val chunkedTextEntity = HttpEntity.Chunked(ContentTypes.`text/plain(UTF-8)`, Source(textChunks))
|
||||
|
||||
Post() ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Gzip) {
|
||||
|
|
@ -348,6 +348,13 @@ class CodingDirectivesSpec extends RoutingSpec with Inside {
|
|||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
|
||||
Get("/") ~> `Accept-Encoding`(HttpEncodingRange.`*`, deflate withQValue 0.2) ~> {
|
||||
encodeResponseWith(Deflate, Gzip) { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"reject the request if it has an Accept-Encoding header with an encoding that doesn't match" in {
|
||||
Get("/") ~> `Accept-Encoding`(deflate) ~> {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import java.io.File
|
|||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ ExecutionContext, Future }
|
||||
import scala.util.Properties
|
||||
import akka.util.ByteString
|
||||
import org.scalatest.matchers.Matcher
|
||||
import org.scalatest.{ Inside, Inspectors }
|
||||
import akka.http.scaladsl.model.MediaTypes._
|
||||
|
|
@ -37,7 +38,7 @@ class FileAndResourceDirectivesSpec extends RoutingSpec with Inspectors with Ins
|
|||
writeAllText("This is PDF", file)
|
||||
Get() ~> getFromFile(file.getPath) ~> check {
|
||||
mediaType shouldEqual `application/pdf`
|
||||
definedCharset shouldEqual None
|
||||
charsetOption shouldEqual None
|
||||
responseAs[String] shouldEqual "This is PDF"
|
||||
headers should contain(`Last-Modified`(DateTime(file.lastModified)))
|
||||
}
|
||||
|
|
@ -71,7 +72,7 @@ class FileAndResourceDirectivesSpec extends RoutingSpec with Inspectors with Ins
|
|||
try {
|
||||
writeAllText("ABCDEFGHIJKLMNOPQRSTUVWXYZ", file)
|
||||
val rangeHeader = Range(ByteRange(1, 10), ByteRange.suffix(10))
|
||||
Get() ~> addHeader(rangeHeader) ~> getFromFile(file, ContentTypes.`text/plain`) ~> check {
|
||||
Get() ~> addHeader(rangeHeader) ~> getFromFile(file, ContentTypes.`text/plain(UTF-8)`) ~> check {
|
||||
status shouldEqual StatusCodes.PartialContent
|
||||
header[`Content-Range`] shouldEqual None
|
||||
mediaType.withParams(Map.empty) shouldEqual `multipart/byteranges`
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ package akka.http.scaladsl.server.directives
|
|||
|
||||
import java.io.{ FileInputStream, File }
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.server.{ MissingFormFieldRejection, RoutingSpec }
|
||||
import akka.util.ByteString
|
||||
|
|
@ -22,7 +21,7 @@ class FileUploadDirectivesSpec extends RoutingSpec {
|
|||
val simpleMultipartUpload =
|
||||
Multipart.FormData(Multipart.FormData.BodyPart.Strict(
|
||||
"fieldName",
|
||||
HttpEntity(MediaTypes.`text/xml`, xml),
|
||||
HttpEntity(ContentTypes.`text/xml(UTF-8)`, xml),
|
||||
Map("filename" -> "age.xml")))
|
||||
|
||||
@volatile var file: Option[File] = None
|
||||
|
|
@ -36,7 +35,7 @@ class FileUploadDirectivesSpec extends RoutingSpec {
|
|||
}
|
||||
} ~> check {
|
||||
file.isDefined === true
|
||||
responseAs[String] === FileInfo("fieldName", "age.xml", ContentTypes.`text/xml`).toString
|
||||
responseAs[String] === FileInfo("fieldName", "age.xml", ContentTypes.`text/xml(UTF-8)`).toString
|
||||
read(file.get) === xml
|
||||
}
|
||||
} finally {
|
||||
|
|
@ -72,7 +71,7 @@ class FileUploadDirectivesSpec extends RoutingSpec {
|
|||
val multipartForm =
|
||||
Multipart.FormData(Multipart.FormData.BodyPart.Strict(
|
||||
"field1",
|
||||
HttpEntity(MediaTypes.`text/plain`, str1),
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, str1),
|
||||
Map("filename" -> "data1.txt")))
|
||||
|
||||
Post("/", multipartForm) ~> route ~> check {
|
||||
|
|
@ -93,11 +92,11 @@ class FileUploadDirectivesSpec extends RoutingSpec {
|
|||
Multipart.FormData(
|
||||
Multipart.FormData.BodyPart.Strict(
|
||||
"field1",
|
||||
HttpEntity(MediaTypes.`text/plain`, str1),
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, str1),
|
||||
Map("filename" -> "data1.txt")),
|
||||
Multipart.FormData.BodyPart.Strict(
|
||||
"field1",
|
||||
HttpEntity(MediaTypes.`text/plain`, str2),
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, str2),
|
||||
Map("filename" -> "data2.txt")))
|
||||
|
||||
Post("/", multipartForm) ~> route ~> check {
|
||||
|
|
@ -130,7 +129,7 @@ class FileUploadDirectivesSpec extends RoutingSpec {
|
|||
val multipartForm =
|
||||
Multipart.FormData(Multipart.FormData.BodyPart.Strict(
|
||||
"field1",
|
||||
HttpEntity(MediaTypes.`text/plain`, str1),
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, str1),
|
||||
Map("filename" -> "data1.txt")))
|
||||
|
||||
Post("/", multipartForm) ~> route ~> check {
|
||||
|
|
|
|||
|
|
@ -24,18 +24,18 @@ class FormFieldDirectivesSpec extends RoutingSpec {
|
|||
val multipartForm = Multipart.FormData {
|
||||
Map(
|
||||
"firstName" -> HttpEntity("Mike"),
|
||||
"age" -> HttpEntity(`text/xml`, "<int>42</int>"),
|
||||
"age" -> HttpEntity(ContentTypes.`text/xml(UTF-8)`, "<int>42</int>"),
|
||||
"VIPBoolean" -> HttpEntity("true"))
|
||||
}
|
||||
val multipartFormWithTextHtml = Multipart.FormData {
|
||||
Map(
|
||||
"firstName" -> HttpEntity("Mike"),
|
||||
"age" -> HttpEntity(`text/xml`, "<int>42</int>"),
|
||||
"VIP" -> HttpEntity(`text/html`, "<b>yes</b>"),
|
||||
"age" -> HttpEntity(ContentTypes.`text/xml(UTF-8)`, "<int>42</int>"),
|
||||
"VIP" -> HttpEntity(ContentTypes.`text/html(UTF-8)`, "<b>yes</b>"),
|
||||
"super" -> HttpEntity("no"))
|
||||
}
|
||||
val multipartFormWithFile = Multipart.FormData(
|
||||
Multipart.FormData.BodyPart.Strict("file", HttpEntity(MediaTypes.`text/xml`, "<int>42</int>"),
|
||||
Multipart.FormData.BodyPart.Strict("file", HttpEntity(ContentTypes.`text/xml(UTF-8)`, "<int>42</int>"),
|
||||
Map("filename" -> "age.xml")))
|
||||
|
||||
"The 'formFields' extraction directive" should {
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import akka.http.scaladsl.unmarshalling._
|
|||
import akka.http.scaladsl.marshalling._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
|
||||
import spray.json.DefaultJsonProtocol._
|
||||
import MediaTypes._
|
||||
import HttpCharsets._
|
||||
import headers._
|
||||
import spray.json.DefaultJsonProtocol._
|
||||
|
||||
class MarshallingDirectivesSpec extends RoutingSpec with Inside {
|
||||
import ScalaXmlSupport._
|
||||
|
|
@ -27,9 +27,10 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside {
|
|||
case x ⇒ { val i = x.text.toInt; require(i >= 0); i }
|
||||
}
|
||||
|
||||
val `text/xxml` = MediaType.customWithFixedCharset("text", "xxml", `UTF-8`)
|
||||
implicit val IntMarshaller: ToEntityMarshaller[Int] =
|
||||
Marshaller.oneOf(ContentType(`application/xhtml+xml`), ContentType(`text/xml`, `UTF-8`)) { contentType ⇒
|
||||
nodeSeqMarshaller(contentType).wrap(contentType) { (i: Int) ⇒ <int>{ i }</int> }
|
||||
Marshaller.oneOf[MediaType.NonBinary, Int, MessageEntity](`application/xhtml+xml`, `text/xxml`) { mediaType ⇒
|
||||
nodeSeqMarshaller(mediaType).wrap(mediaType) { (i: Int) ⇒ <int>{ i }</int> }
|
||||
}
|
||||
|
||||
"The 'entityAs' directive" should {
|
||||
|
|
@ -44,7 +45,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside {
|
|||
} ~> check { rejection shouldEqual RequestEntityExpectedRejection }
|
||||
}
|
||||
"return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope" in {
|
||||
Put("/", HttpEntity(`text/css`, "<p>cool</p>")) ~> {
|
||||
Put("/", HttpEntity(`text/css` withCharset `UTF-8`, "<p>cool</p>")) ~> {
|
||||
entity(as[NodeSeq]) { echoComplete }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`))
|
||||
|
|
@ -56,7 +57,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside {
|
|||
}
|
||||
}
|
||||
"cancel UnsupportedRequestContentTypeRejections if a subsequent `entity` directive succeeds" in {
|
||||
Put("/", HttpEntity(`text/plain`, "yeah")) ~> {
|
||||
Put("/", HttpEntity(ContentTypes.`text/plain(UTF-8)`, "yeah")) ~> {
|
||||
entity(as[NodeSeq]) { _ ⇒ completeOk } ~
|
||||
entity(as[String]) { _ ⇒ validate(false, "Problem") { completeOk } }
|
||||
} ~> check { rejection shouldEqual ValidationRejection("Problem") }
|
||||
|
|
@ -71,7 +72,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside {
|
|||
}
|
||||
}
|
||||
"return a MalformedRequestContentRejection if unmarshalling failed due to a not further classified error" in {
|
||||
Put("/", HttpEntity(`text/xml`, "<foo attr='illegal xml'")) ~> {
|
||||
Put("/", HttpEntity(ContentTypes.`text/xml(UTF-8)`, "<foo attr='illegal xml'")) ~> {
|
||||
entity(as[NodeSeq]) { _ ⇒ completeOk }
|
||||
} ~> check {
|
||||
rejection shouldEqual MalformedRequestContentRejection(
|
||||
|
|
@ -89,7 +90,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside {
|
|||
} ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
"return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope (for Option[T]s)" in {
|
||||
Put("/", HttpEntity(`text/css`, "<p>cool</p>")) ~> {
|
||||
Put("/", HttpEntity(`text/css` withCharset `UTF-8`, "<p>cool</p>")) ~> {
|
||||
entity(as[Option[NodeSeq]]) { echoComplete }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`))
|
||||
|
|
@ -105,13 +106,13 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside {
|
|||
|
||||
val route = entity(as[Person]) { echoComplete }
|
||||
|
||||
Put("/", HttpEntity(`text/xml`, "<name>Peter Xml</name>")) ~> route ~> check {
|
||||
Put("/", HttpEntity(ContentTypes.`text/xml(UTF-8)`, "<name>Peter Xml</name>")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Person(Peter Xml)"
|
||||
}
|
||||
Put("/", HttpEntity(`application/json`, """{ "name": "Paul Json" }""")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Person(Paul Json)"
|
||||
}
|
||||
Put("/", HttpEntity(`text/plain`, """name = Sir Text }""")) ~> route ~> check {
|
||||
Put("/", HttpEntity(ContentTypes.`text/plain(UTF-8)`, """name = Sir Text }""")) ~> route ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`application/json`, `text/xml`))
|
||||
}
|
||||
}
|
||||
|
|
@ -125,7 +126,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside {
|
|||
}
|
||||
"return a UnacceptedResponseContentTypeRejection rejection if no acceptable marshaller is in scope" in {
|
||||
Get() ~> Accept(`text/css`) ~> completeWith(instanceOf[Int]) { prod ⇒ prod(42) } ~> check {
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, ContentType(`text/xml`, `UTF-8`)))
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, `text/xxml`))
|
||||
}
|
||||
}
|
||||
"convert the response content to an accepted charset" in {
|
||||
|
|
@ -139,19 +140,19 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside {
|
|||
def times2(x: Int) = x * 2
|
||||
|
||||
"support proper round-trip content unmarshalling/marshalling to and from a function" in (
|
||||
Put("/", HttpEntity(`text/html`, "<int>42</int>")) ~> Accept(`text/xml`) ~> handleWith(times2)
|
||||
~> check { responseEntity shouldEqual HttpEntity(ContentType(`text/xml`, `UTF-8`), "<int>84</int>") })
|
||||
Put("/", HttpEntity(ContentTypes.`text/html(UTF-8)`, "<int>42</int>")) ~> Accept(`text/xxml`) ~> handleWith(times2)
|
||||
~> check { responseEntity shouldEqual HttpEntity(`text/xxml`, "<int>84</int>") })
|
||||
|
||||
"result in UnsupportedRequestContentTypeRejection rejection if there is no unmarshaller supporting the requests charset" in (
|
||||
Put("/", HttpEntity(`text/xml`, "<int>42</int>")) ~> Accept(`text/xml`) ~> handleWith(times2)
|
||||
Put("/", HttpEntity(ContentTypes.`text/xml(UTF-8)`, "<int>42</int>")) ~> Accept(`text/xml`) ~> handleWith(times2)
|
||||
~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(ContentTypeRange(`text/xml`, iso88592), `text/html`))
|
||||
})
|
||||
|
||||
"result in an UnacceptedResponseContentTypeRejection rejection if there is no marshaller supporting the requests Accept-Charset header" in (
|
||||
Put("/", HttpEntity(`text/html`, "<int>42</int>")) ~> addHeaders(Accept(`text/xml`), `Accept-Charset`(`UTF-16`)) ~>
|
||||
Put("/", HttpEntity(ContentTypes.`text/html(UTF-8)`, "<int>42</int>")) ~> addHeaders(Accept(`text/xxml`), `Accept-Charset`(`UTF-16`)) ~>
|
||||
handleWith(times2) ~> check {
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, ContentType(`text/xml`, `UTF-8`)))
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, `text/xxml`))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -163,7 +164,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside {
|
|||
|
||||
"render JSON with UTF-8 encoding if no `Accept-Charset` request header is present" in {
|
||||
Get() ~> complete(foo) ~> check {
|
||||
responseEntity shouldEqual HttpEntity(ContentType(`application/json`, `UTF-8`), foo.toJson.prettyPrint)
|
||||
responseEntity shouldEqual HttpEntity(`application/json`, foo.toJson.prettyPrint)
|
||||
}
|
||||
}
|
||||
"reject JSON rendering if an `Accept-Charset` request header requests a non-UTF-8 encoding" in {
|
||||
|
|
|
|||
|
|
@ -34,10 +34,6 @@ class MiscDirectivesSpec extends RoutingSpec {
|
|||
"the selectPreferredLanguage directive" should {
|
||||
"Accept-Language: de, en" test { selectFrom ⇒
|
||||
selectFrom("de", "en") shouldEqual "de"
|
||||
selectFrom("en", "de") shouldEqual "de"
|
||||
}
|
||||
"Accept-Language: en, de" test { selectFrom ⇒
|
||||
selectFrom("de", "en") shouldEqual "en"
|
||||
selectFrom("en", "de") shouldEqual "en"
|
||||
}
|
||||
"Accept-Language: en, de;q=.5" test { selectFrom ⇒
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ class RangeDirectivesSpec extends RoutingSpec with Inspectors with Inside {
|
|||
def entityData() = StreamUtils.oneTimeSource(Source.single(ByteString(content)))
|
||||
|
||||
Get() ~> addHeader(Range(ByteRange(5, 10), ByteRange(0, 1), ByteRange(1, 2))) ~> {
|
||||
wrs { complete(HttpEntity.Default(MediaTypes.`text/plain`, content.length, entityData())) }
|
||||
wrs { complete(HttpEntity.Default(ContentTypes.`text/plain(UTF-8)`, content.length, entityData())) }
|
||||
} ~> check {
|
||||
header[`Content-Range`] should be(None)
|
||||
val parts = Await.result(responseAs[Multipart.ByteRanges].parts.grouped(1000).runWith(Sink.head), 1.second)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ package akka.http.scaladsl.server.directives
|
|||
|
||||
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport
|
||||
import akka.stream.scaladsl.Sink
|
||||
import org.scalatest.FreeSpec
|
||||
import scala.concurrent.{ Future, Promise }
|
||||
import akka.testkit.EventFilter
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
|
||||
import akka.http.scaladsl.marshalling._
|
||||
import akka.http.scaladsl.server._
|
||||
|
|
@ -16,9 +16,6 @@ import akka.http.scaladsl.model._
|
|||
import akka.http.impl.util._
|
||||
import headers._
|
||||
import StatusCodes._
|
||||
import MediaTypes._
|
||||
import scala.xml.NodeSeq
|
||||
import akka.testkit.EventFilter
|
||||
|
||||
class RouteDirectivesSpec extends FreeSpec with GenericRoutingSpec {
|
||||
|
||||
|
|
@ -122,7 +119,8 @@ class RouteDirectivesSpec extends FreeSpec with GenericRoutingSpec {
|
|||
} ~> check {
|
||||
response shouldEqual HttpResponse(
|
||||
status = 302,
|
||||
entity = HttpEntity(`text/html`, "The requested resource temporarily resides under <a href=\"/foo\">this URI</a>."),
|
||||
entity = HttpEntity(ContentTypes.`text/html(UTF-8)`,
|
||||
"The requested resource temporarily resides under <a href=\"/foo\">this URI</a>."),
|
||||
headers = Location("/foo") :: Nil)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import akka.http.scaladsl.util.FastFuture._
|
|||
import akka.http.impl.util._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import MediaTypes._
|
||||
import HttpCharsets._
|
||||
|
||||
class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAfterAll with ScalatestUtils {
|
||||
implicit val system = ActorSystem(getClass.getSimpleName)
|
||||
|
|
@ -28,13 +29,13 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
|
||||
"multipartGeneralUnmarshaller should correctly unmarshal 'multipart/*' content with" - {
|
||||
"an empty part" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`,
|
||||
"""--XYZABC
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
|
||||
}
|
||||
"two empty parts" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`,
|
||||
"""--XYZABC
|
||||
|--XYZABC
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
|
|
@ -42,15 +43,15 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
|
||||
}
|
||||
"a part without entity and missing header separation CRLF" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`,
|
||||
"""--XYZABC
|
||||
|Content-type: text/xml
|
||||
|Age: 12
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity.empty(MediaTypes.`text/xml`), List(Age(12))))
|
||||
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/xml(UTF-8)`), List(Age(12))))
|
||||
}
|
||||
"an implicitly typed part (without headers) (Strict)" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`,
|
||||
"""--XYZABC
|
||||
|
|
||||
|Perfectly fine part content.
|
||||
|
|
@ -63,12 +64,12 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
|Perfectly fine part content.
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n")
|
||||
val byteStrings = content.map(c ⇒ ByteString(c.toString)) // one-char ByteStrings
|
||||
Unmarshal(HttpEntity.Default(`multipart/mixed` withBoundary "XYZABC", content.length, Source(byteStrings)))
|
||||
Unmarshal(HttpEntity.Default(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`, content.length, Source(byteStrings)))
|
||||
.to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "Perfectly fine part content.")))
|
||||
}
|
||||
"one non-empty form-data part" in {
|
||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",
|
||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-" withCharset `UTF-8`,
|
||||
"""---
|
||||
|Content-type: text/plain; charset=UTF8
|
||||
|content-disposition: form-data; name="email"
|
||||
|
|
@ -80,7 +81,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
List(`Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")))))
|
||||
}
|
||||
"two different parts" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345",
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345" withCharset `UTF-8`,
|
||||
"""--12345
|
||||
|
|
||||
|first part, with a trailing newline
|
||||
|
|
@ -92,10 +93,11 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
|filecontent
|
||||
|--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, with a trailing newline\r\n")),
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "filecontent"), List(RawHeader("Content-Transfer-Encoding", "binary"))))
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, ByteString("filecontent")),
|
||||
List(RawHeader("Content-Transfer-Encoding", "binary"))))
|
||||
}
|
||||
"illegal headers" in (
|
||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC",
|
||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC" withCharset `UTF-8`,
|
||||
"""--XYZABC
|
||||
|Date: unknown
|
||||
|content-disposition: form-data; name=email
|
||||
|
|
@ -107,7 +109,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
List(RawHeader("date", "unknown"),
|
||||
`Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email"))))))
|
||||
"a full example (Strict)" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345",
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345" withCharset `UTF-8`,
|
||||
"""preamble and
|
||||
|more preamble
|
||||
|--12345
|
||||
|
|
@ -121,7 +123,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
|epilogue and
|
||||
|more epilogue""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")),
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "second part, explicitly typed")))
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, ByteString("second part, explicitly typed"))))
|
||||
}
|
||||
"a full example (Default)" in {
|
||||
val content = """preamble and
|
||||
|
|
@ -137,13 +139,13 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
|epilogue and
|
||||
|more epilogue""".stripMarginWithNewline("\r\n")
|
||||
val byteStrings = content.map(c ⇒ ByteString(c.toString)) // one-char ByteStrings
|
||||
Unmarshal(HttpEntity.Default(`multipart/mixed` withBoundary "12345", content.length, Source(byteStrings)))
|
||||
Unmarshal(HttpEntity.Default(`multipart/mixed` withBoundary "12345" withCharset `UTF-8`, content.length, Source(byteStrings)))
|
||||
.to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")),
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "second part, explicitly typed")))
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, ByteString("second part, explicitly typed"))))
|
||||
}
|
||||
"a boundary with spaces" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "simple boundary",
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "simple boundary" withCharset `UTF-8`,
|
||||
"""--simple boundary
|
||||
|--simple boundary--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
|
||||
|
|
@ -152,17 +154,17 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
|
||||
"multipartGeneralUnmarshaller should reject illegal multipart content with" - {
|
||||
"an empty entity" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC", ByteString.empty))
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`, ByteString.empty))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity"
|
||||
}
|
||||
"an entity without initial boundary" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`,
|
||||
"""this is
|
||||
|just preamble text""".stripMarginWithNewline("\r\n")))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity"
|
||||
}
|
||||
"a stray boundary" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "ABC",
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "ABC" withCharset `UTF-8`,
|
||||
"""--ABC
|
||||
|Content-type: text/plain; charset=UTF8
|
||||
|--ABCContent-type: application/json
|
||||
|
|
@ -171,7 +173,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Illegal multipart boundary in message content"
|
||||
}
|
||||
"duplicate Content-Type header" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-" withCharset `UTF-8`,
|
||||
"""---
|
||||
|Content-type: text/plain; charset=UTF8
|
||||
|Content-type: application/json
|
||||
|
|
@ -183,7 +185,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
"multipart part must not contain more than one Content-Type header"
|
||||
}
|
||||
"a missing header-separating CRLF (in Strict entity)" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-" withCharset `UTF-8`,
|
||||
"""---
|
||||
|not good here
|
||||
|-----""".stripMarginWithNewline("\r\n")))
|
||||
|
|
@ -197,27 +199,27 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
|not ok
|
||||
|-----""".stripMarginWithNewline("\r\n")
|
||||
val byteStrings = content.map(c ⇒ ByteString(c.toString)) // one-char ByteStrings
|
||||
val contentType = `multipart/form-data` withBoundary "-"
|
||||
val contentType = `multipart/form-data` withBoundary "-" withCharset `UTF-8`
|
||||
Await.result(Unmarshal(HttpEntity.Default(contentType, content.length, Source(byteStrings)))
|
||||
.to[Multipart.General]
|
||||
.flatMap(_ toStrict 1.second).failed, 1.second).getMessage shouldEqual "Illegal character ' ' in header name"
|
||||
}
|
||||
"a boundary with a trailing space" in {
|
||||
Await.result(
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "simple boundary ", ByteString.empty))
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "simple boundary " withCharset `UTF-8`, ByteString.empty))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual
|
||||
"requirement failed: 'boundary' parameter of multipart Content-Type must not end with a space char"
|
||||
}
|
||||
"a boundary with an illegal character" in {
|
||||
Await.result(
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "simple&boundary", ByteString.empty))
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "simple&boundary" withCharset `UTF-8`, ByteString.empty))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual
|
||||
"requirement failed: 'boundary' parameter of multipart Content-Type contains illegal character '&'"
|
||||
}
|
||||
}
|
||||
|
||||
"multipartByteRangesUnmarshaller should correctly unmarshal multipart/byteranges content with two different parts" in {
|
||||
Unmarshal(HttpEntity(`multipart/byteranges` withBoundary "12345",
|
||||
Unmarshal(HttpEntity(`multipart/byteranges` withBoundary "12345" withCharset `UTF-8`,
|
||||
"""--12345
|
||||
|Content-Range: bytes 0-2/26
|
||||
|Content-Type: text/plain
|
||||
|
|
@ -229,24 +231,24 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
|
|
||||
|XYZ
|
||||
|--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.ByteRanges] should haveParts(
|
||||
Multipart.ByteRanges.BodyPart.Strict(ContentRange(0, 2, 26), HttpEntity(ContentTypes.`text/plain`, "ABC")),
|
||||
Multipart.ByteRanges.BodyPart.Strict(ContentRange(23, 25, 26), HttpEntity(ContentTypes.`text/plain`, "XYZ")))
|
||||
Multipart.ByteRanges.BodyPart.Strict(ContentRange(0, 2, 26), HttpEntity(ContentTypes.`text/plain(UTF-8)`, "ABC")),
|
||||
Multipart.ByteRanges.BodyPart.Strict(ContentRange(23, 25, 26), HttpEntity(ContentTypes.`text/plain(UTF-8)`, "XYZ")))
|
||||
}
|
||||
|
||||
"multipartFormDataUnmarshaller should correctly unmarshal 'multipart/form-data' content" - {
|
||||
"with one element" in {
|
||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC",
|
||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC" withCharset `UTF-8`,
|
||||
"""--XYZABC
|
||||
|content-disposition: form-data; name=email
|
||||
|
|
||||
|test@there.com
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.FormData] should haveParts(
|
||||
Multipart.FormData.BodyPart.Strict("email", HttpEntity(ContentTypes.`application/octet-stream`, "test@there.com")))
|
||||
Multipart.FormData.BodyPart.Strict("email", HttpEntity(`application/octet-stream`, ByteString("test@there.com"))))
|
||||
}
|
||||
"with a file" in {
|
||||
Unmarshal {
|
||||
HttpEntity.Default(
|
||||
contentType = `multipart/form-data` withBoundary "XYZABC",
|
||||
contentType = `multipart/form-data` withBoundary "XYZABC" withCharset `UTF-8`,
|
||||
contentLength = 1, // not verified during unmarshalling
|
||||
data = Source {
|
||||
List(
|
||||
|
|
@ -270,8 +272,8 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|
|||
})
|
||||
})
|
||||
}.to[Multipart.FormData].flatMap(_.toStrict(1.second)) should haveParts(
|
||||
Multipart.FormData.BodyPart.Strict("email", HttpEntity(ContentTypes.`application/octet-stream`, "test@there.com")),
|
||||
Multipart.FormData.BodyPart.Strict("userfile", HttpEntity(MediaTypes.`application/pdf`, "filecontent"), Map("filename" -> "test.dat"),
|
||||
Multipart.FormData.BodyPart.Strict("email", HttpEntity(`application/octet-stream`, ByteString("test@there.com"))),
|
||||
Multipart.FormData.BodyPart.Strict("userfile", HttpEntity(`application/pdf`, ByteString("filecontent")), Map("filename" -> "test.dat"),
|
||||
List(
|
||||
RawHeader("Content-Transfer-Encoding", "binary"),
|
||||
RawHeader("Content-Additional-1", "anything"),
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ private[http] class RejectionHandlerWrapper(javaHandler: server.RejectionHandler
|
|||
case RequestEntityExpectedRejection ⇒
|
||||
handleRequestEntityExpectedRejection(ctx)
|
||||
case UnacceptedResponseContentTypeRejection(supported) ⇒
|
||||
handleUnacceptedResponseContentTypeRejection(ctx, supported.toList.toSeq.asJava)
|
||||
handleUnacceptedResponseContentTypeRejection(ctx, supported.toList.map(_.format).toSeq.asJava)
|
||||
case UnacceptedResponseEncodingRejection(supported) ⇒
|
||||
handleUnacceptedResponseEncodingRejection(ctx, supported.toList.toSeq.asJava)
|
||||
case AuthenticationFailedRejection(cause, challenge) ⇒
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ private[http] final case class RequestContextImpl(underlying: ScalaRequestContex
|
|||
case r: RouteResultImpl ⇒ r.underlying
|
||||
}(executionContext())
|
||||
def complete(text: String): RouteResult = underlying.complete(text)
|
||||
def complete(contentType: ContentType, text: String): RouteResult =
|
||||
def complete(contentType: ContentType.NonBinary, text: String): RouteResult =
|
||||
underlying.complete(HttpEntity(contentType.asScala, text))
|
||||
|
||||
def completeWithStatus(statusCode: Int): RouteResult =
|
||||
|
|
|
|||
|
|
@ -28,7 +28,14 @@ object Marshallers {
|
|||
* Creates a marshaller by specifying a media type and conversion function from ``T`` to String.
|
||||
* The charset for encoding the response will be negotiated with the client.
|
||||
*/
|
||||
def toEntityString[T](mediaType: MediaType, convert: function.Function[T, String]): Marshaller[T] =
|
||||
def toEntityString[T](mediaType: MediaType.WithOpenCharset, convert: function.Function[T, String]): Marshaller[T] =
|
||||
MarshallerImpl(_ ⇒ ScalaMarshaller.stringMarshaller(mediaType.asScala).compose[T](convert(_)))
|
||||
|
||||
/**
|
||||
* Creates a marshaller by specifying a media type and conversion function from ``T`` to String.
|
||||
* The charset for encoding the response will be negotiated with the client.
|
||||
*/
|
||||
def toEntityString[T](mediaType: MediaType.WithFixedCharset, convert: function.Function[T, String]): Marshaller[T] =
|
||||
MarshallerImpl(_ ⇒ ScalaMarshaller.stringMarshaller(mediaType.asScala).compose[T](convert(_)))
|
||||
|
||||
/**
|
||||
|
|
@ -48,7 +55,7 @@ object Marshallers {
|
|||
*/
|
||||
def toEntity[T](contentType: ContentType, convert: function.Function[T, ResponseEntity]): Marshaller[T] =
|
||||
MarshallerImpl { _ ⇒
|
||||
ScalaMarshaller.withFixedCharset(contentType.mediaType().asScala, contentType.charset().asScala)(t ⇒
|
||||
ScalaMarshaller.withFixedContentType(contentType.asScala)(t ⇒
|
||||
HttpResponse.create().withStatus(200).withEntity(convert(t)).asScala)
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +64,6 @@ object Marshallers {
|
|||
*/
|
||||
def toResponse[T](contentType: ContentType, convert: function.Function[T, HttpResponse]): Marshaller[T] =
|
||||
MarshallerImpl { _ ⇒
|
||||
ScalaMarshaller.withFixedCharset(contentType.mediaType().asScala, contentType.charset().asScala)(t ⇒
|
||||
convert(t).asScala)
|
||||
ScalaMarshaller.withFixedContentType(contentType.asScala)(t ⇒ convert(t).asScala)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ package akka.http.javadsl.server
|
|||
import java.{ lang ⇒ jl }
|
||||
|
||||
import akka.http.impl.server.PassRejectionRouteResult
|
||||
import akka.http.javadsl.model.{ ContentType, ContentTypeRange, HttpMethod }
|
||||
import akka.http.javadsl.model.{ ContentTypeRange, HttpMethod }
|
||||
import akka.http.javadsl.model.headers.{ HttpChallenge, ByteRange, HttpEncoding }
|
||||
import akka.http.scaladsl.server.Rejection
|
||||
|
||||
|
|
@ -122,9 +122,9 @@ abstract class RejectionHandler {
|
|||
/**
|
||||
* Callback called to handle 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
|
||||
* content type is accepted by the client.
|
||||
*/
|
||||
def handleUnacceptedResponseContentTypeRejection(ctx: RequestContext, supported: jl.Iterable[ContentType]): RouteResult = passRejection()
|
||||
def handleUnacceptedResponseContentTypeRejection(ctx: RequestContext, supported: jl.Iterable[String]): RouteResult = passRejection()
|
||||
|
||||
/**
|
||||
* Callback called to handle rejection created by encoding filters.
|
||||
|
|
|
|||
|
|
@ -4,11 +4,9 @@
|
|||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import scala.concurrent.{ ExecutionContext, Future }
|
||||
import akka.http.javadsl.model._
|
||||
import akka.stream.Materializer
|
||||
import akka.util.ByteString
|
||||
|
||||
import scala.concurrent.{ ExecutionContext, Future }
|
||||
|
||||
/**
|
||||
* The RequestContext represents the state of the request while it is routed through
|
||||
|
|
@ -50,7 +48,7 @@ trait RequestContext {
|
|||
/**
|
||||
* Completes the request with the given string as an entity of the given type.
|
||||
*/
|
||||
def complete(contentType: ContentType, text: String): RouteResult
|
||||
def complete(contentType: ContentType.NonBinary, text: String): RouteResult
|
||||
|
||||
/**
|
||||
* Completes the request with the given status code and no entity.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ abstract class BasicDirectives extends BasicDirectivesBase {
|
|||
/**
|
||||
* A route that completes the request with a static text
|
||||
*/
|
||||
def complete(contentType: ContentType, text: String): Route =
|
||||
def complete(contentType: ContentType.NonBinary, text: String): Route =
|
||||
new OpaqueRoute() {
|
||||
def handle(ctx: RequestContext): RouteResult =
|
||||
ctx.complete(contentType, text)
|
||||
|
|
|
|||
|
|
@ -36,11 +36,6 @@ trait FileAndResourceRoute extends Route {
|
|||
*/
|
||||
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.
|
||||
|
|
@ -55,8 +50,6 @@ object FileAndResourceRoute {
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
@ -66,8 +59,6 @@ object FileAndResourceRoute {
|
|||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@ object StrictForm {
|
|||
def unmarshallerFromFSU[T](fsu: FromStringUnmarshaller[T]): FromStrictFormFieldUnmarshaller[T] =
|
||||
Unmarshaller.withMaterializer(implicit ec ⇒ implicit mat ⇒ {
|
||||
case FromString(value) ⇒ fsu(value)
|
||||
case FromPart(value) ⇒ fsu(value.entity.data.decodeString(value.entity.contentType.charset.nioCharset.name))
|
||||
case FromPart(value) ⇒
|
||||
val charsetName = value.entity.contentType.asInstanceOf[ContentType.NonBinary].charset.nioCharset.name
|
||||
fsu(value.entity.data.decodeString(charsetName))
|
||||
})
|
||||
|
||||
@implicitNotFound("In order to unmarshal a `StrictForm.Field` to type `${T}` you need to supply a " +
|
||||
|
|
@ -76,8 +78,10 @@ object StrictForm {
|
|||
implicit def fromFSU[T](implicit fsu: FromStringUnmarshaller[T]) =
|
||||
new FieldUnmarshaller[T] {
|
||||
def unmarshalString(value: String)(implicit ec: ExecutionContext, mat: Materializer) = fsu(value)
|
||||
def unmarshalPart(value: Multipart.FormData.BodyPart.Strict)(implicit ec: ExecutionContext, mat: Materializer) =
|
||||
fsu(value.entity.data.decodeString(value.entity.contentType.charset.nioCharset.name))
|
||||
def unmarshalPart(value: Multipart.FormData.BodyPart.Strict)(implicit ec: ExecutionContext, mat: Materializer) = {
|
||||
val charsetName = value.entity.contentType.asInstanceOf[ContentType.NonBinary].charset.nioCharset.name
|
||||
fsu(value.entity.data.decodeString(charsetName))
|
||||
}
|
||||
}
|
||||
implicit def fromFEU[T](implicit feu: FromEntityUnmarshaller[T]) =
|
||||
new FieldUnmarshaller[T] {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* 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 ContentTypeOverrider[T] {
|
||||
def apply(value: T, newContentType: ContentType): T
|
||||
}
|
||||
|
||||
object ContentTypeOverrider {
|
||||
|
||||
implicit def forEntity[T <: HttpEntity]: ContentTypeOverrider[T] = new ContentTypeOverrider[T] {
|
||||
def apply(value: T, newContentType: ContentType) =
|
||||
value.withContentType(newContentType).asInstanceOf[T] // can't be expressed in types
|
||||
}
|
||||
|
||||
implicit def forHeadersAndEntity[T <: HttpEntity] = new ContentTypeOverrider[(immutable.Seq[HttpHeader], T)] {
|
||||
def apply(value: (immutable.Seq[HttpHeader], T), newContentType: ContentType) =
|
||||
value._1 -> value._2.withContentType(newContentType).asInstanceOf[T]
|
||||
}
|
||||
|
||||
implicit val forResponse = new ContentTypeOverrider[HttpResponse] {
|
||||
def apply(value: HttpResponse, newContentType: ContentType) =
|
||||
value.mapEntity(forEntity(_: ResponseEntity, newContentType))
|
||||
}
|
||||
|
||||
implicit val forRequest = new ContentTypeOverrider[HttpRequest] {
|
||||
def apply(value: HttpRequest, newContentType: ContentType) =
|
||||
value.mapEntity(forEntity(_: RequestEntity, newContentType))
|
||||
}
|
||||
}
|
||||
|
|
@ -5,16 +5,15 @@
|
|||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import scala.concurrent.{ ExecutionContext, Future }
|
||||
import akka.http.scaladsl.model.HttpCharsets._
|
||||
import akka.http.scaladsl.server.ContentNegotiator
|
||||
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)
|
||||
case class UnacceptableResponseContentTypeException(supported: Set[ContentNegotiator.Alternative])
|
||||
extends RuntimeException
|
||||
}
|
||||
|
||||
class Marshal[A](val value: A) {
|
||||
|
|
@ -25,7 +24,7 @@ class Marshal[A](val value: A) {
|
|||
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.WithFixedContentType(_, marshal) ⇒ marshal()
|
||||
case Marshalling.WithOpenCharset(_, marshal) ⇒ marshal(HttpCharsets.`UTF-8`)
|
||||
case Marshalling.Opaque(marshal) ⇒ marshal()
|
||||
}
|
||||
|
|
@ -36,40 +35,32 @@ class Marshal[A](val value: A) {
|
|||
*/
|
||||
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)
|
||||
val ctn = ContentNegotiator(request.headers)
|
||||
|
||||
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)
|
||||
val supportedAlternatives: List[ContentNegotiator.Alternative] =
|
||||
marshallings.collect {
|
||||
case Marshalling.WithFixedContentType(ct, _) ⇒ ContentNegotiator.Alternative(ct)
|
||||
case Marshalling.WithOpenCharset(mt, _) ⇒ ContentNegotiator.Alternative(mt)
|
||||
}(collection.breakOut)
|
||||
val bestMarshal = {
|
||||
if (supportedAlternatives.nonEmpty) {
|
||||
ctn.pickContentType(supportedAlternatives).flatMap {
|
||||
case best @ (_: ContentType.Binary | _: ContentType.WithFixedCharset) ⇒
|
||||
marshallings collectFirst { case Marshalling.WithFixedContentType(`best`, marshal) ⇒ marshal }
|
||||
case best @ ContentType.WithCharset(bestMT, bestCS) ⇒
|
||||
marshallings collectFirst {
|
||||
case Marshalling.WithFixedContentType(`best`, marshal) ⇒ marshal
|
||||
case Marshalling.WithOpenCharset(`bestMT`, marshal) ⇒ () ⇒ marshal(bestCS)
|
||||
}
|
||||
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
|
||||
} else None
|
||||
} orElse {
|
||||
marshallings collectFirst { case Marshalling.Opaque(marshal) ⇒ marshal }
|
||||
} getOrElse {
|
||||
throw UnacceptableResponseContentTypeException(supportedAlternatives.toSet)
|
||||
}
|
||||
|
||||
case (acc, Marshalling.Opaque(marshal)) ⇒
|
||||
if (acc.weight == 0f) new MarshallingWeight(Float.MinPositiveValue, marshal) else acc
|
||||
}
|
||||
best.marshal()
|
||||
bestMarshal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,39 +19,50 @@ sealed abstract class Marshaller[-A, +B] {
|
|||
|
||||
/**
|
||||
* 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.
|
||||
* the [[MediaType]] of the marshalling result with the given one.
|
||||
* Note that not all wrappings are legal. f the underlying [[MediaType]] has constraints with regard to the
|
||||
* charsets it allows the new [[MediaType]] must be compatible, 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)
|
||||
def wrap[C, D >: B](newMediaType: MediaType)(f: C ⇒ A)(implicit mto: ContentTypeOverrider[D]): Marshaller[C, D] =
|
||||
wrapWithEC[C, D](newMediaType)(_ ⇒ 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.
|
||||
* the [[MediaType]] of the marshalling result with the given one.
|
||||
* Note that not all wrappings are legal. f the underlying [[MediaType]] has constraints with regard to the
|
||||
* charsets it allows the new [[MediaType]] must be compatible, 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] =
|
||||
def wrapWithEC[C, D >: B](newMediaType: MediaType)(f: ExecutionContext ⇒ C ⇒ A)(implicit cto: ContentTypeOverrider[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`")
|
||||
(_, newMediaType) match {
|
||||
case (WithFixedContentType(_, marshal), newMT: MediaType.Binary) ⇒
|
||||
WithFixedContentType(newMT, () ⇒ cto(marshal(), newMT))
|
||||
case (WithFixedContentType(oldCT: ContentType.Binary, marshal), newMT: MediaType.WithFixedCharset) ⇒
|
||||
WithFixedContentType(newMT, () ⇒ cto(marshal(), newMT))
|
||||
case (WithFixedContentType(oldCT: ContentType.NonBinary, marshal), newMT: MediaType.WithFixedCharset) if oldCT.charset == newMT.charset ⇒
|
||||
WithFixedContentType(newMT, () ⇒ cto(marshal(), newMT))
|
||||
case (WithFixedContentType(oldCT: ContentType.NonBinary, marshal), newMT: MediaType.WithOpenCharset) ⇒
|
||||
val newCT = newMT withCharset oldCT.charset
|
||||
WithFixedContentType(newCT, () ⇒ cto(marshal(), newCT))
|
||||
|
||||
case (WithOpenCharset(oldMT, marshal), newMT: MediaType.WithOpenCharset) ⇒
|
||||
WithOpenCharset(newMT, cs ⇒ cto(marshal(cs), newMT withCharset cs))
|
||||
case (WithOpenCharset(oldMT, marshal), newMT: MediaType.WithFixedCharset) ⇒
|
||||
WithFixedContentType(newMT, () ⇒ cto(marshal(newMT.charset), newMT))
|
||||
|
||||
case (Opaque(marshal), newMT: MediaType.Binary) ⇒
|
||||
WithFixedContentType(newMT, () ⇒ cto(marshal(), newMT))
|
||||
case (Opaque(marshal), newMT: MediaType.WithFixedCharset) ⇒
|
||||
WithFixedContentType(newMT, () ⇒ cto(marshal(), newMT))
|
||||
|
||||
case x ⇒ sys.error(s"Illegal marshaller wrapping. Marshalling `$x` cannot be wrapped with MediaType `$newMediaType`")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -103,13 +114,13 @@ object Marshaller
|
|||
/**
|
||||
* 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)) }
|
||||
def withFixedContentType[A, B](contentType: ContentType)(marshal: A ⇒ B): Marshaller[A, B] =
|
||||
strict { value ⇒ Marshalling.WithFixedContentType(contentType, () ⇒ 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] =
|
||||
def withOpenCharset[A, B](mediaType: MediaType.WithOpenCharset)(marshal: (A, HttpCharset) ⇒ B): Marshaller[A, B] =
|
||||
strict { value ⇒ Marshalling.WithOpenCharset(mediaType, charset ⇒ marshal(value, charset)) }
|
||||
|
||||
/**
|
||||
|
|
@ -136,19 +147,19 @@ sealed trait Marshalling[+A] {
|
|||
}
|
||||
|
||||
object Marshalling {
|
||||
|
||||
/**
|
||||
* A Marshalling to a specific MediaType and charset.
|
||||
* A Marshalling to a specific [[ContentType]].
|
||||
*/
|
||||
final case class WithFixedCharset[A](mediaType: MediaType,
|
||||
charset: HttpCharset,
|
||||
final case class WithFixedContentType[A](contentType: ContentType,
|
||||
marshal: () ⇒ A) extends Marshalling[A] {
|
||||
def map[B](f: A ⇒ B): WithFixedCharset[B] = copy(marshal = () ⇒ f(marshal()))
|
||||
def map[B](f: A ⇒ B): WithFixedContentType[B] = copy(marshal = () ⇒ f(marshal()))
|
||||
}
|
||||
|
||||
/**
|
||||
* A Marshalling to a specific MediaType and a potentially flexible charset.
|
||||
* A Marshalling to a specific [[MediaType]] with a flexible charset.
|
||||
*/
|
||||
final case class WithOpenCharset[A](mediaType: MediaType,
|
||||
final case class WithOpenCharset[A](mediaType: MediaType.WithOpenCharset,
|
||||
marshal: HttpCharset ⇒ A) extends Marshalling[A] {
|
||||
def map[B](f: A ⇒ B): WithOpenCharset[B] = copy(marshal = cs ⇒ f(marshal(cs)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
/**
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
|
|
@ -13,8 +13,8 @@ trait MultipartMarshallers {
|
|||
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 ⇒
|
||||
val mediaType = value.mediaType withBoundary boundary
|
||||
Marshalling.WithOpenCharset(mediaType, { charset ⇒
|
||||
value.toEntity(charset, boundary)(log)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,51 +13,39 @@ 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) }
|
||||
}
|
||||
def byteArrayMarshaller(contentType: ContentType): ToEntityMarshaller[Array[Byte]] =
|
||||
Marshaller.withFixedContentType(contentType) { bytes ⇒ HttpEntity(contentType, 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) }
|
||||
}
|
||||
def byteStringMarshaller(contentType: ContentType): ToEntityMarshaller[ByteString] =
|
||||
Marshaller.withFixedContentType(contentType) { bytes ⇒ HttpEntity(contentType, bytes) }
|
||||
|
||||
implicit val CharArrayMarshaller: ToEntityMarshaller[Array[Char]] = charArrayMarshaller(`text/plain`)
|
||||
def charArrayMarshaller(mediaType: MediaType): ToEntityMarshaller[Array[Char]] =
|
||||
Marshaller.withOpenCharset(mediaType) { (value, charset) ⇒
|
||||
def charArrayMarshaller(mediaType: MediaType.WithOpenCharset): ToEntityMarshaller[Array[Char]] =
|
||||
Marshaller.withOpenCharset(mediaType) { (value, charset) ⇒ marshalCharArray(value, mediaType withCharset charset) }
|
||||
def charArrayMarshaller(mediaType: MediaType.WithFixedCharset): ToEntityMarshaller[Array[Char]] =
|
||||
Marshaller.withFixedContentType(mediaType) { value ⇒ marshalCharArray(value, mediaType) }
|
||||
|
||||
private def marshalCharArray(value: Array[Char], contentType: ContentType.NonBinary): HttpEntity.Strict =
|
||||
if (value.length > 0) {
|
||||
val charBuffer = CharBuffer.wrap(value)
|
||||
val byteBuffer = charset.nioCharset.encode(charBuffer)
|
||||
val byteBuffer = contentType.charset.nioCharset.encode(charBuffer)
|
||||
val array = new Array[Byte](byteBuffer.remaining())
|
||||
byteBuffer.get(array)
|
||||
HttpEntity(ContentType(mediaType, charset), array)
|
||||
HttpEntity(contentType, 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) }
|
||||
def stringMarshaller(mediaType: MediaType.WithOpenCharset): ToEntityMarshaller[String] =
|
||||
Marshaller.withOpenCharset(mediaType) { (s, cs) ⇒ HttpEntity(mediaType withCharset cs, s) }
|
||||
def stringMarshaller(mediaType: MediaType.WithFixedCharset): ToEntityMarshaller[String] =
|
||||
Marshaller.withFixedContentType(mediaType) { s ⇒ HttpEntity(mediaType, s) }
|
||||
|
||||
implicit val FormDataMarshaller: ToEntityMarshaller[FormData] =
|
||||
Marshaller.withOpenCharset(`application/x-www-form-urlencoded`) { (formData, charset) ⇒
|
||||
formData.toEntity(charset)
|
||||
}
|
||||
Marshaller.withOpenCharset(`application/x-www-form-urlencoded`) { _ toEntity _ }
|
||||
|
||||
implicit val MessageEntityMarshaller: ToEntityMarshaller[MessageEntity] = Marshaller strict { value ⇒
|
||||
Marshalling.WithFixedCharset(value.contentType.mediaType, value.contentType.charset, () ⇒ value)
|
||||
}
|
||||
implicit val MessageEntityMarshaller: ToEntityMarshaller[MessageEntity] =
|
||||
Marshaller strict { value ⇒ Marshalling.WithFixedContentType(value.contentType, () ⇒ value) }
|
||||
}
|
||||
|
||||
object PredefinedToEntityMarshallers extends PredefinedToEntityMarshallers
|
||||
|
|
|
|||
|
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.model
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import HttpCharsets.`UTF-8`
|
||||
|
||||
final class MediaTypeNegotiator(requestHeaders: Seq[HttpHeader]) {
|
||||
|
||||
/**
|
||||
* The media-ranges accepted by the client according to the given request headers, sorted by
|
||||
* 1. increasing generality (i.e. most specific first)
|
||||
* 2. decreasing q-value (only for ranges targeting a single MediaType)
|
||||
* 3. order of appearance in the `Accept` header(s)
|
||||
*/
|
||||
val acceptedMediaRanges: List[MediaRange] =
|
||||
(for {
|
||||
Accept(mediaRanges) ← requestHeaders
|
||||
range ← mediaRanges
|
||||
} yield range).sortBy { // `sortBy` is stable, i.e. upholds the original order on identical keys
|
||||
case x if x.isWildcard ⇒ 2f // most general, needs to come last
|
||||
case MediaRange.One(_, qv) ⇒ -qv // most specific, needs to come first
|
||||
case _ ⇒ 1f // simple range like `image/*`
|
||||
}.toList
|
||||
|
||||
/**
|
||||
* Returns the q-value that the client (implicitly or explicitly) attaches to the given media-type.
|
||||
* See http://tools.ietf.org/html/rfc7231#section-5.3.1 for details.
|
||||
*/
|
||||
def qValueFor(mediaType: MediaType): Float =
|
||||
acceptedMediaRanges match {
|
||||
case Nil ⇒ 1.0f
|
||||
case x ⇒ x collectFirst { case r if r matches mediaType ⇒ r.qValue } getOrElse 0f
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given [[MediaType]] is accepted by the client.
|
||||
*/
|
||||
def isAccepted(mediaType: MediaType): Boolean = qValueFor(mediaType) > 0f
|
||||
}
|
||||
|
||||
final class CharsetNegotiator(requestHeaders: Seq[HttpHeader]) {
|
||||
|
||||
/**
|
||||
* The charset-ranges accepted by the client according to given request headers, sorted by
|
||||
* 1. increasing generality (i.e. most specific first)
|
||||
* 2. decreasing q-value (only for ranges targeting a single HttpCharset)
|
||||
* 3. order of appearance in the `Accept-Charset` header(s)
|
||||
*/
|
||||
val acceptedCharsetRanges: List[HttpCharsetRange] =
|
||||
(for {
|
||||
`Accept-Charset`(charsetRanges) ← requestHeaders
|
||||
range ← charsetRanges
|
||||
} yield range).sortBy { // `sortBy` is stable, i.e. upholds the original order on identical keys
|
||||
case _: HttpCharsetRange.`*` ⇒ 1f // most general, needs to come last
|
||||
case x ⇒ -x.qValue // all others come first
|
||||
}.toList
|
||||
|
||||
/**
|
||||
* Returns the q-value that the client (implicitly or explicitly) attaches to the given charset.
|
||||
* See http://tools.ietf.org/html/rfc7231#section-5.3.1 for details.
|
||||
*/
|
||||
def qValueFor(charset: HttpCharset): Float =
|
||||
acceptedCharsetRanges match {
|
||||
case Nil ⇒ 1.0f
|
||||
case x ⇒ x collectFirst { case r if r matches charset ⇒ r.qValue } getOrElse 0f
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given charset is accepted by the client.
|
||||
*/
|
||||
def isAccepted(charset: HttpCharset): Boolean = qValueFor(charset) > 0f
|
||||
|
||||
/**
|
||||
* Picks the charset that is most preferred by the client with a bias towards UTF-8,
|
||||
* i.e. if the client accepts all charsets with equal preference then UTF-8 is picked.
|
||||
* If the client doesn't accept any charsets the method returns `None`.
|
||||
*
|
||||
* See also: http://tools.ietf.org/html/rfc7231#section-5.3.3
|
||||
*/
|
||||
def pickBest: Option[HttpCharset] =
|
||||
acceptedCharsetRanges match {
|
||||
case Nil ⇒ Some(`UTF-8`)
|
||||
case HttpCharsetRange.One(cs, _) :: _ ⇒ Some(cs)
|
||||
case HttpCharsetRange.`*`(qv) :: _ if qv > 0f ⇒ Some(`UTF-8`)
|
||||
case _ ⇒ None
|
||||
}
|
||||
}
|
||||
|
||||
final class ContentNegotiator(requestHeaders: Seq[HttpHeader]) {
|
||||
import ContentNegotiator.Alternative
|
||||
|
||||
val mtn = new MediaTypeNegotiator(requestHeaders)
|
||||
val csn = new CharsetNegotiator(requestHeaders)
|
||||
|
||||
def qValueFor(alternative: Alternative): Float =
|
||||
alternative match {
|
||||
case Alternative.ContentType(ct: ContentType.NonBinary) ⇒
|
||||
math.min(mtn.qValueFor(ct.mediaType), csn.qValueFor(ct.charset))
|
||||
case x ⇒ mtn.qValueFor(x.mediaType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks the best of the given content alternatives given the preferences
|
||||
* the client indicated in the request's `Accept` and `Accept-Charset` headers.
|
||||
* See http://tools.ietf.org/html/rfc7231#section-5.3.2 ff for details on the negotiation logic.
|
||||
*
|
||||
* If there are several best alternatives that the client has equal preference for
|
||||
* the order of the given alternatives is used as a tie breaker (first one wins).
|
||||
*
|
||||
* If none of the given alternatives is acceptable to the client the methods return `None`.
|
||||
*/
|
||||
def pickContentType(alternatives: List[Alternative]): Option[ContentType] =
|
||||
alternatives
|
||||
.map(alt ⇒ alt → qValueFor(alt))
|
||||
.sortBy(-_._2)
|
||||
.collectFirst { case (alt, q) if q > 0f ⇒ alt }
|
||||
.flatMap {
|
||||
case Alternative.ContentType(ct) ⇒ Some(ct)
|
||||
case Alternative.MediaType(mt) ⇒ csn.pickBest.map(mt.withCharset)
|
||||
}
|
||||
}
|
||||
|
||||
object ContentNegotiator {
|
||||
sealed trait Alternative {
|
||||
def mediaType: MediaType
|
||||
def format: String
|
||||
}
|
||||
object Alternative {
|
||||
implicit def apply(contentType: model.ContentType): ContentType = ContentType(contentType)
|
||||
implicit def apply(mediaType: model.MediaType): Alternative =
|
||||
mediaType match {
|
||||
case x: model.MediaType.Binary ⇒ ContentType(x)
|
||||
case x: model.MediaType.WithFixedCharset ⇒ ContentType(x)
|
||||
case x: model.MediaType.WithOpenCharset ⇒ MediaType(x)
|
||||
}
|
||||
|
||||
case class ContentType(contentType: model.ContentType) extends Alternative {
|
||||
def mediaType = contentType.mediaType
|
||||
def format = contentType.toString
|
||||
}
|
||||
case class MediaType(mediaType: model.MediaType.WithOpenCharset) extends Alternative {
|
||||
def format = mediaType.toString
|
||||
}
|
||||
}
|
||||
|
||||
def apply(requestHeaders: Seq[HttpHeader]) = new ContentNegotiator(requestHeaders)
|
||||
}
|
||||
|
||||
final class EncodingNegotiator(requestHeaders: Seq[HttpHeader]) {
|
||||
|
||||
/**
|
||||
* The encoding-ranges accepted by the client according to given request headers, sorted by
|
||||
* 1. increasing generality (i.e. most specific first)
|
||||
* 2. decreasing q-value (only for ranges targeting a single HttpEncoding)
|
||||
* 3. order of appearance in the `Accept-Encoding` header(s)
|
||||
*/
|
||||
val acceptedEncodingRanges: List[HttpEncodingRange] =
|
||||
(for {
|
||||
`Accept-Encoding`(encodingRanges) ← requestHeaders
|
||||
range ← encodingRanges
|
||||
} yield range).sortBy { // `sortBy` is stable, i.e. upholds the original order on identical keys
|
||||
case _: HttpEncodingRange.`*` ⇒ 1f // most general, needs to come last
|
||||
case x ⇒ -x.qValue // all others come first
|
||||
}.toList
|
||||
|
||||
/**
|
||||
* Returns the q-value that the client (implicitly or explicitly) attaches to the given encoding.
|
||||
* See http://tools.ietf.org/html/rfc7231#section-5.3.1 for details.
|
||||
*/
|
||||
def qValueFor(encoding: HttpEncoding): Float =
|
||||
acceptedEncodingRanges match {
|
||||
case Nil ⇒ 1.0f
|
||||
case x ⇒ x collectFirst { case r if r matches encoding ⇒ r.qValue } getOrElse 0f
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given encoding is accepted by the client.
|
||||
*/
|
||||
def isAccepted(encoding: HttpEncoding): Boolean = qValueFor(encoding) > 0f
|
||||
|
||||
/**
|
||||
* Determines whether the request has an `Accept-Encoding` clause matching the given encoding.
|
||||
*/
|
||||
def hasMatchingFor(encoding: HttpEncoding): Boolean =
|
||||
acceptedEncodingRanges.exists(_ matches encoding)
|
||||
|
||||
/**
|
||||
* Picks the best of the given encoding alternatives given the preferences
|
||||
* the client indicated in the request's `Accept-Encoding` headers.
|
||||
* See http://tools.ietf.org/html/rfc7231#section-5.3.4 for details on the negotiation logic.
|
||||
*
|
||||
* If there are several best encoding alternatives that the client has equal preference for
|
||||
* the order of the given alternatives is used as a tie breaker (first one wins).
|
||||
*
|
||||
* If none of the given alternatives is acceptable to the client the methods return `None`.
|
||||
*/
|
||||
def pickEncoding(alternatives: List[HttpEncoding]): Option[HttpEncoding] =
|
||||
alternatives
|
||||
.map(alt ⇒ alt → qValueFor(alt))
|
||||
.sortBy(-_._2)
|
||||
.collectFirst { case (alt, q) if q > 0f ⇒ alt }
|
||||
}
|
||||
|
||||
object EncodingNegotiator {
|
||||
def apply(requestHeaders: Seq[HttpHeader]) = new EncodingNegotiator(requestHeaders)
|
||||
}
|
||||
|
||||
final class LanguageNegotiator(requestHeaders: Seq[HttpHeader]) {
|
||||
|
||||
/**
|
||||
* The language-ranges accepted by the client according to given request headers, sorted by
|
||||
* 1. increasing generality (i.e. most specific first)
|
||||
* 2. decreasing q-value (only for ranges targeting a single Language)
|
||||
* 3. order of appearance in the `Accept-Language` header(s)
|
||||
*/
|
||||
val acceptedLanguageRanges: List[LanguageRange] =
|
||||
(for {
|
||||
`Accept-Language`(languageRanges) ← requestHeaders
|
||||
range ← languageRanges
|
||||
} yield range).sortBy { // `sortBy` is stable, i.e. upholds the original order on identical keys
|
||||
case _: LanguageRange.`*` ⇒ 1f // most general, needs to come last
|
||||
case x ⇒ -(2 * x.subTags.size + x.qValue) // more subtags -> more specific -> go first
|
||||
}.toList
|
||||
|
||||
/**
|
||||
* Returns the q-value that the client (implicitly or explicitly) attaches to the given language.
|
||||
* See http://tools.ietf.org/html/rfc7231#section-5.3.1 for details.
|
||||
*/
|
||||
def qValueFor(language: Language): Float =
|
||||
acceptedLanguageRanges match {
|
||||
case Nil ⇒ 1.0f
|
||||
case x ⇒ x collectFirst { case r if r matches language ⇒ r.qValue } getOrElse 0f
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given language is accepted by the client.
|
||||
*/
|
||||
def isAccepted(language: Language): Boolean = qValueFor(language) > 0f
|
||||
|
||||
/**
|
||||
* Picks the best of the given language alternatives given the preferences
|
||||
* the client indicated in the request's `Accept-Language` headers.
|
||||
* See http://tools.ietf.org/html/rfc7231#section-5.3.5 for details on the negotiation logic.
|
||||
*
|
||||
* If there are several best language alternatives that the client has equal preference for
|
||||
* the order of the given alternatives is used as a tie breaker (first one wins).
|
||||
*
|
||||
* If none of the given alternatives is acceptable to the client the methods return `None`.
|
||||
*/
|
||||
def pickLanguage(alternatives: List[Language]): Option[Language] =
|
||||
alternatives
|
||||
.map(alt ⇒ alt → qValueFor(alt))
|
||||
.sortBy(-_._2)
|
||||
.collectFirst { case (alt, q) if q > 0f ⇒ alt }
|
||||
}
|
||||
|
||||
object LanguageNegotiator {
|
||||
def apply(requestHeaders: Seq[HttpHeader]) = new LanguageNegotiator(requestHeaders)
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ case object RequestEntityExpectedRejection extends Rejection
|
|||
* 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: immutable.Set[ContentType]) extends Rejection
|
||||
case class UnacceptedResponseContentTypeRejection(supported: immutable.Set[ContentNegotiator.Alternative]) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by encoding filters.
|
||||
|
|
|
|||
|
|
@ -188,8 +188,8 @@ object RejectionHandler {
|
|||
}
|
||||
.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")))
|
||||
val msg = supported.map(_.format).mkString("Resource representation is only available with these types:\n", "\n", "")
|
||||
complete((NotAcceptable, msg))
|
||||
}
|
||||
.handleAll[UnacceptedResponseEncodingRejection] { rejections ⇒
|
||||
val supported = rejections.flatMap(_.supported)
|
||||
|
|
|
|||
|
|
@ -5,10 +5,9 @@
|
|||
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.headers.{ HttpEncodings, HttpEncoding }
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.coding._
|
||||
import akka.http.impl.util._
|
||||
|
|
@ -26,8 +25,10 @@ trait CodingDirectives {
|
|||
* if the given response encoding is not accepted by the client.
|
||||
*/
|
||||
def responseEncodingAccepted(encoding: HttpEncoding): Directive0 =
|
||||
extract(_.request.isEncodingAccepted(encoding))
|
||||
.flatMap(if (_) pass else reject(UnacceptedResponseEncodingRejection(Set(encoding))))
|
||||
extractRequest.flatMap { request ⇒
|
||||
if (EncodingNegotiator(request.headers).isAccepted(encoding)) pass
|
||||
else reject(UnacceptedResponseEncodingRejection(Set(encoding)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the response with the encoding that is requested by the client via the `Accept-
|
||||
|
|
@ -117,37 +118,18 @@ object CodingDirectives extends CodingDirectives {
|
|||
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 {
|
||||
BasicDirectives.extractRequest.flatMap { request ⇒
|
||||
val negotiator = EncodingNegotiator(request.headers)
|
||||
val encodings: List[HttpEncoding] = encoders.map(_.encoding)(collection.breakOut)
|
||||
val bestEncoder = negotiator.pickEncoding(encodings).flatMap(be ⇒ encoders.find(_.encoding == be))
|
||||
bestEncoder match {
|
||||
case Some(encoder) ⇒ mapResponse(encoder.encode(_))
|
||||
case _ ⇒ reject(UnacceptedResponseEncodingRejection(encoders.map(_.encoding).toSet))
|
||||
case _ ⇒
|
||||
if (encoders.contains(NoCoding) && !negotiator.hasMatchingFor(HttpEncodings.identity)) pass
|
||||
else reject(UnacceptedResponseEncodingRejection(encodings.toSet))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ object ContentTypeResolver {
|
|||
case x ⇒ fileName.substring(x + 1)
|
||||
}
|
||||
val mediaType = MediaTypes.forExtension(ext) getOrElse MediaTypes.`application/octet-stream`
|
||||
ContentType(mediaType) withDefaultCharset charset
|
||||
ContentType(mediaType, () ⇒ charset)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,18 +51,9 @@ trait MiscDirectives {
|
|||
* has equal preference for (even if this preference is zero!)
|
||||
* the order of the arguments is used as a tie breaker (First one wins).
|
||||
*/
|
||||
def selectPreferredLanguage(first: Language, more: Language*): Directive1[Language] = {
|
||||
val available = first :: List(more: _*) // we use List rather than Seq since element count is likely very small
|
||||
BasicDirectives.extractRequest.map { req ⇒
|
||||
val sortedWithQValues = available.zip(available.map(req.qValueForLanguage(_))).sortBy(-_._2)
|
||||
val firstBest = sortedWithQValues.head
|
||||
val moreBest = sortedWithQValues.tail.takeWhile(_._2 == firstBest._2)
|
||||
if (moreBest.nonEmpty) {
|
||||
// we have several languages that have the same qvalue, so we pick the one the `Accept-Header` lists first
|
||||
val allBest = firstBest :: moreBest
|
||||
req.acceptedLanguageRanges.flatMap(range ⇒ allBest.find(t ⇒ range.matches(t._1))).head._1
|
||||
} else firstBest._1 // we have a single best match, so pick that
|
||||
}
|
||||
def selectPreferredLanguage(first: Language, more: Language*): Directive1[Language] =
|
||||
BasicDirectives.extractRequest.map { request ⇒
|
||||
LanguageNegotiator(request.headers).pickLanguage(first :: List(more: _*)) getOrElse first
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ trait RouteDirectives {
|
|||
headers = headers.Location(uri) :: Nil,
|
||||
entity = redirectionType.htmlTemplate match {
|
||||
case "" ⇒ HttpEntity.Empty
|
||||
case template ⇒ HttpEntity(MediaTypes.`text/html`, template format uri)
|
||||
case template ⇒ HttpEntity(ContentTypes.`text/html(UTF-8)`, template format uri)
|
||||
})
|
||||
}
|
||||
//#
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ trait MultipartUnmarshallers {
|
|||
def multipartGeneralUnmarshaller(defaultCharset: HttpCharset)(implicit log: LoggingAdapter = NoLogging): FromEntityUnmarshaller[Multipart.General] =
|
||||
multipartUnmarshaller[Multipart.General, Multipart.General.BodyPart, Multipart.General.BodyPart.Strict](
|
||||
mediaRange = `multipart/*`,
|
||||
defaultContentType = ContentTypes.`text/plain` withCharset defaultCharset,
|
||||
defaultContentType = MediaTypes.`text/plain` withCharset defaultCharset,
|
||||
createBodyPart = Multipart.General.BodyPart(_, _),
|
||||
createStreamed = Multipart.General(_, _),
|
||||
createStrictBodyPart = Multipart.General.BodyPart.Strict,
|
||||
|
|
@ -45,7 +45,7 @@ trait MultipartUnmarshallers {
|
|||
def multipartByteRangesUnmarshaller(defaultCharset: HttpCharset)(implicit log: LoggingAdapter = NoLogging): FromEntityUnmarshaller[Multipart.ByteRanges] =
|
||||
multipartUnmarshaller[Multipart.ByteRanges, Multipart.ByteRanges.BodyPart, Multipart.ByteRanges.BodyPart.Strict](
|
||||
mediaRange = `multipart/byteranges`,
|
||||
defaultContentType = ContentTypes.`text/plain` withCharset defaultCharset,
|
||||
defaultContentType = MediaTypes.`text/plain` withCharset defaultCharset,
|
||||
createBodyPart = (entity, headers) ⇒ Multipart.General.BodyPart(entity, headers).toByteRangesBodyPart.get,
|
||||
createStreamed = (_, parts) ⇒ Multipart.ByteRanges(parts),
|
||||
createStrictBodyPart = (entity, headers) ⇒ Multipart.General.BodyPart.Strict(entity, headers).toByteRangesBodyPart.get,
|
||||
|
|
@ -54,9 +54,9 @@ trait MultipartUnmarshallers {
|
|||
def multipartUnmarshaller[T <: Multipart, BP <: Multipart.BodyPart, BPS <: Multipart.BodyPart.Strict](mediaRange: MediaRange,
|
||||
defaultContentType: ContentType,
|
||||
createBodyPart: (BodyPartEntity, List[HttpHeader]) ⇒ BP,
|
||||
createStreamed: (MultipartMediaType, Source[BP, Any]) ⇒ T,
|
||||
createStreamed: (MediaType.Multipart, Source[BP, Any]) ⇒ T,
|
||||
createStrictBodyPart: (HttpEntity.Strict, List[HttpHeader]) ⇒ BPS,
|
||||
createStrict: (MultipartMediaType, immutable.Seq[BPS]) ⇒ T)(implicit log: LoggingAdapter = NoLogging): FromEntityUnmarshaller[T] =
|
||||
createStrict: (MediaType.Multipart, immutable.Seq[BPS]) ⇒ T)(implicit log: LoggingAdapter = NoLogging): FromEntityUnmarshaller[T] =
|
||||
Unmarshaller { implicit ec ⇒
|
||||
entity ⇒
|
||||
if (entity.contentType.mediaType.isMultipart && mediaRange.matches(entity.contentType.mediaType)) {
|
||||
|
|
@ -68,7 +68,7 @@ trait MultipartUnmarshallers {
|
|||
val parser = new BodyPartParser(defaultContentType, boundary, log)
|
||||
FastFuture.successful {
|
||||
entity match {
|
||||
case HttpEntity.Strict(ContentType(mediaType: MultipartMediaType, _), data) ⇒
|
||||
case HttpEntity.Strict(ContentType(mediaType: MediaType.Multipart, _), data) ⇒
|
||||
val builder = new VectorBuilder[BPS]()
|
||||
val iter = new IteratorInterpreter[ByteString, BodyPartParser.Output](
|
||||
Iterator.single(data), List(parser)).iterator
|
||||
|
|
@ -93,7 +93,7 @@ trait MultipartUnmarshallers {
|
|||
case (BodyPartStart(headers, createEntity), entityParts) ⇒ createBodyPart(createEntity(entityParts), headers)
|
||||
case (ParseError(errorInfo), _) ⇒ throw ParsingException(errorInfo)
|
||||
}
|
||||
createStreamed(entity.contentType.mediaType.asInstanceOf[MultipartMediaType], bodyParts)
|
||||
createStreamed(entity.contentType.mediaType.asInstanceOf[MediaType.Multipart], bodyParts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,31 +21,34 @@ trait PredefinedFromEntityUnmarshallers extends MultipartUnmarshallers {
|
|||
|
||||
implicit def charArrayUnmarshaller: FromEntityUnmarshaller[Array[Char]] =
|
||||
byteStringUnmarshaller mapWithInput { (entity, bytes) ⇒
|
||||
val charBuffer = entity.contentType.charset.nioCharset.decode(bytes.asByteBuffer)
|
||||
if (entity.isKnownEmpty) Array.emptyCharArray
|
||||
else {
|
||||
val charBuffer = Unmarshaller.bestUnmarshallingCharsetFor(entity).nioCharset.decode(bytes.asByteBuffer)
|
||||
val array = new Array[Char](charBuffer.length())
|
||||
charBuffer.get(array)
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
implicit def stringUnmarshaller: FromEntityUnmarshaller[String] =
|
||||
byteStringUnmarshaller mapWithInput { (entity, bytes) ⇒
|
||||
// FIXME: add `ByteString::decodeString(java.nio.Charset): String` overload!!!
|
||||
bytes.decodeString(entity.contentType.charset.nioCharset.name) // ouch!!!
|
||||
if (entity.isKnownEmpty) ""
|
||||
else bytes.decodeString(Unmarshaller.bestUnmarshallingCharsetFor(entity).nioCharset.name)
|
||||
}
|
||||
|
||||
implicit def defaultUrlEncodedFormDataUnmarshaller: FromEntityUnmarshaller[FormData] =
|
||||
urlEncodedFormDataUnmarshaller(MediaTypes.`application/x-www-form-urlencoded`)
|
||||
def urlEncodedFormDataUnmarshaller(ranges: ContentTypeRange*): FromEntityUnmarshaller[FormData] =
|
||||
stringUnmarshaller.forContentTypes(ranges: _*).mapWithInput { (entity, string) ⇒
|
||||
try {
|
||||
val nioCharset = entity.contentType.definedCharset.getOrElse(HttpCharsets.`UTF-8`).nioCharset
|
||||
val query = Uri.Query(string, nioCharset)
|
||||
FormData(query)
|
||||
} catch {
|
||||
if (entity.isKnownEmpty) FormData.Empty
|
||||
else {
|
||||
try FormData(Uri.Query(string, Unmarshaller.bestUnmarshallingCharsetFor(entity).nioCharset))
|
||||
catch {
|
||||
case IllegalUriException(info) ⇒
|
||||
throw new IllegalArgumentException(info.formatPretty.replace("Query,", "form content,"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object PredefinedFromEntityUnmarshallers extends PredefinedFromEntityUnmarshallers
|
||||
|
|
@ -90,7 +90,7 @@ object Unmarshaller
|
|||
|
||||
implicit class EnhancedFromEntityUnmarshaller[A](val underlying: FromEntityUnmarshaller[A]) extends AnyVal {
|
||||
def mapWithCharset[B](f: (A, HttpCharset) ⇒ B): FromEntityUnmarshaller[B] =
|
||||
underlying.mapWithInput { (entity, data) ⇒ f(data, entity.contentType.charset) }
|
||||
underlying.mapWithInput { (entity, data) ⇒ f(data, Unmarshaller.bestUnmarshallingCharsetFor(entity)) }
|
||||
|
||||
/**
|
||||
* Modifies the underlying [[Unmarshaller]] to only accept content-types matching one of the given ranges.
|
||||
|
|
@ -114,6 +114,16 @@ object Unmarshaller
|
|||
case UnsupportedContentTypeException(supported) ⇒ underlying(entity withContentType supported.head.specimen)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the best charset for unmarshalling the given entity to a character-based representation.
|
||||
* Falls back to UTF-8 if no better alternative can be determined.
|
||||
*/
|
||||
def bestUnmarshallingCharsetFor(entity: HttpEntity): HttpCharset =
|
||||
entity.contentType match {
|
||||
case x: ContentType.NonBinary ⇒ x.charset
|
||||
case _ ⇒ HttpCharsets.`UTF-8`
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that unmarshalling failed because the entity was unexpectedly empty.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue