!htp #19034 refactor content negotiation, upgrade to new MediaType / ContentType model

This commit is contained in:
Mathias 2015-12-01 23:07:56 +01:00
parent e9240b7d86
commit 899b92faf2
46 changed files with 652 additions and 403 deletions

View file

@ -35,7 +35,7 @@ public class ModelDocTest {
Authorization authorization = Authorization.basic("user", "pass"); Authorization authorization = Authorization.basic("user", "pass");
HttpRequest complexRequest = HttpRequest complexRequest =
HttpRequest.PUT("/user") HttpRequest.PUT("/user")
.withEntity(HttpEntities.create(MediaTypes.TEXT_PLAIN.toContentType(), "abc")) .withEntity(HttpEntities.create(ContentTypes.TEXT_PLAIN_UTF8, "abc"))
.addHeader(authorization) .addHeader(authorization)
.withProtocol(HttpProtocols.HTTP_1_0); .withProtocol(HttpProtocols.HTTP_1_0);
//#construct-request //#construct-request

View file

@ -51,7 +51,7 @@ public class HighLevelServerExample extends HttpApp {
// matches the empty path // matches the empty path
pathSingleSlash().route( pathSingleSlash().route(
// return a constant string with a certain content type // return a constant string with a certain content type
complete(ContentTypes.TEXT_HTML, complete(ContentTypes.TEXT_HTML_UTF8,
"<html><body>Hello world!</body></html>") "<html><body>Hello world!</body></html>")
), ),
path("ping").route( path("ping").route(

View file

@ -156,7 +156,7 @@ public class HttpServerExampleDocTest {
.via(failureDetection) .via(failureDetection)
.map(request -> { .map(request -> {
Source<ByteString, Object> bytes = request.entity().getDataBytes(); 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() return HttpResponse.create()
.withEntity(entity); .withEntity(entity);
@ -199,7 +199,7 @@ public class HttpServerExampleDocTest {
if (uri.path().equals("/")) if (uri.path().equals("/"))
return return
HttpResponse.create() HttpResponse.create()
.withEntity(ContentTypes.TEXT_HTML, .withEntity(ContentTypes.TEXT_HTML_UTF8,
"<html><body>Hello world!</body></html>"); "<html><body>Hello world!</body></html>");
else if (uri.path().equals("/hello")) { else if (uri.path().equals("/hello")) {
String name = Util.getOrElse(uri.query().get("name"), "Mister X"); String name = Util.getOrElse(uri.query().get("name"), "Mister X");

View file

@ -150,7 +150,7 @@ class HttpServerExampleSpec extends WordSpec with Matchers {
.via(reactToConnectionFailure) .via(reactToConnectionFailure)
.map { request => .map { request =>
// simple text "echo" response: // 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 serverSource
@ -173,7 +173,7 @@ class HttpServerExampleSpec extends WordSpec with Matchers {
val requestHandler: HttpRequest => HttpResponse = { val requestHandler: HttpRequest => HttpResponse = {
case HttpRequest(GET, Uri.Path("/"), _, _, _) => 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>")) "<html><body>Hello world!</body></html>"))
case HttpRequest(GET, Uri.Path("/ping"), _, _, _) => case HttpRequest(GET, Uri.Path("/ping"), _, _, _) =>
@ -207,7 +207,7 @@ class HttpServerExampleSpec extends WordSpec with Matchers {
val requestHandler: HttpRequest => HttpResponse = { val requestHandler: HttpRequest => HttpResponse = {
case HttpRequest(GET, Uri.Path("/"), _, _, _) => 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>")) "<html><body>Hello world!</body></html>"))
case HttpRequest(GET, Uri.Path("/ping"), _, _, _) => case HttpRequest(GET, Uri.Path("/ping"), _, _, _) =>

View file

@ -32,12 +32,13 @@ class ModelSpec extends AkkaSpec {
// customize every detail of HTTP request // customize every detail of HTTP request
import HttpProtocols._ import HttpProtocols._
import MediaTypes._ import MediaTypes._
import HttpCharsets._
val userData = ByteString("abc") val userData = ByteString("abc")
val authorization = headers.Authorization(BasicHttpCredentials("user", "pass")) val authorization = headers.Authorization(BasicHttpCredentials("user", "pass"))
HttpRequest( HttpRequest(
PUT, PUT,
uri = "/user", uri = "/user",
entity = HttpEntity(`text/plain`, userData), entity = HttpEntity(`text/plain` withCharset `UTF-8`, userData),
headers = List(authorization), headers = List(authorization),
protocol = `HTTP/1.0`) protocol = `HTTP/1.0`)
//#construct-request //#construct-request

View file

@ -20,7 +20,7 @@ class CodingDirectivesExamplesSpec extends RoutingSpec {
Get("/") ~> route ~> check { Get("/") ~> route ~> check {
responseAs[String] shouldEqual "content" responseAs[String] shouldEqual "content"
} }
Get("/") ~> `Accept-Encoding`(`identity;q=MIN`) ~> route ~> check { Get("/") ~> `Accept-Encoding`(deflate) ~> route ~> check {
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip) rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
} }
} }
@ -48,9 +48,6 @@ class CodingDirectivesExamplesSpec extends RoutingSpec {
Get("/") ~> route ~> check { Get("/") ~> route ~> check {
response should haveContentEncoding(gzip) response should haveContentEncoding(gzip)
} }
Get("/") ~> `Accept-Encoding`() ~> route ~> check {
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
}
Get("/") ~> `Accept-Encoding`(gzip, deflate) ~> route ~> check { Get("/") ~> `Accept-Encoding`(gzip, deflate) ~> route ~> check {
response should haveContentEncoding(gzip) response should haveContentEncoding(gzip)
} }

View file

@ -3,15 +3,11 @@
*/ */
package docs.http.scaladsl.server.directives package docs.http.scaladsl.server.directives
import java.io.File import akka.http.scaladsl.model._
import akka.http.scaladsl.model.{ MediaTypes, HttpEntity, Multipart, StatusCodes }
import akka.stream.io.Framing import akka.stream.io.Framing
import akka.util.ByteString import akka.util.ByteString
import docs.http.scaladsl.server.RoutingSpec import docs.http.scaladsl.server.RoutingSpec
import scala.concurrent.Future import scala.concurrent.Future
import scala.util.{ Success, Failure }
class FileUploadDirectivesExamplesSpec extends RoutingSpec { class FileUploadDirectivesExamplesSpec extends RoutingSpec {
@ -32,7 +28,7 @@ class FileUploadDirectivesExamplesSpec extends RoutingSpec {
Multipart.FormData( Multipart.FormData(
Multipart.FormData.BodyPart.Strict( Multipart.FormData.BodyPart.Strict(
"csv", "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"))) Map("filename" -> "data.csv")))
Post("/", multipartForm) ~> route ~> check { Post("/", multipartForm) ~> route ~> check {
@ -68,7 +64,7 @@ class FileUploadDirectivesExamplesSpec extends RoutingSpec {
val multipartForm = val multipartForm =
Multipart.FormData(Multipart.FormData.BodyPart.Strict( Multipart.FormData(Multipart.FormData.BodyPart.Strict(
"csv", "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"))) Map("filename" -> "primes.csv")))
Post("/", multipartForm) ~> route ~> check { Post("/", multipartForm) ~> route ~> check {

View file

@ -5,10 +5,9 @@
package akka.http.scaladsl.marshallers.sprayjson package akka.http.scaladsl.marshallers.sprayjson
import scala.language.implicitConversions import scala.language.implicitConversions
import akka.stream.Materializer
import akka.http.scaladsl.marshalling.{ ToEntityMarshaller, Marshaller } import akka.http.scaladsl.marshalling.{ ToEntityMarshaller, Marshaller }
import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshaller } 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 akka.http.scaladsl.model.MediaTypes.`application/json`
import spray.json._ import spray.json._
@ -33,6 +32,6 @@ trait SprayJsonSupport {
implicit def sprayJsonMarshaller[T](implicit writer: RootJsonWriter[T], printer: JsonPrinter = PrettyPrinter): ToEntityMarshaller[T] = implicit def sprayJsonMarshaller[T](implicit writer: RootJsonWriter[T], printer: JsonPrinter = PrettyPrinter): ToEntityMarshaller[T] =
sprayJsValueMarshaller compose writer.write sprayJsValueMarshaller compose writer.write
implicit def sprayJsValueMarshaller(implicit printer: JsonPrinter = PrettyPrinter): ToEntityMarshaller[JsValue] = 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 object SprayJsonSupport extends SprayJsonSupport

View file

@ -8,7 +8,6 @@ import java.io.{ ByteArrayInputStream, InputStreamReader }
import javax.xml.parsers.{ SAXParserFactory, SAXParser } import javax.xml.parsers.{ SAXParserFactory, SAXParser }
import scala.collection.immutable import scala.collection.immutable
import scala.xml.{ XML, NodeSeq } import scala.xml.{ XML, NodeSeq }
import akka.stream.Materializer
import akka.http.scaladsl.unmarshalling._ import akka.http.scaladsl.unmarshalling._
import akka.http.scaladsl.marshalling._ import akka.http.scaladsl.marshalling._
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
@ -16,10 +15,10 @@ import MediaTypes._
trait ScalaXmlSupport { trait ScalaXmlSupport {
implicit def defaultNodeSeqMarshaller: ToEntityMarshaller[NodeSeq] = implicit def defaultNodeSeqMarshaller: ToEntityMarshaller[NodeSeq] =
Marshaller.oneOf(ScalaXmlSupport.nodeSeqContentTypes.map(nodeSeqMarshaller): _*) Marshaller.oneOf(ScalaXmlSupport.nodeSeqMediaTypes.map(nodeSeqMarshaller): _*)
def nodeSeqMarshaller(contentType: ContentType): ToEntityMarshaller[NodeSeq] = def nodeSeqMarshaller(mediaType: MediaType.NonBinary): ToEntityMarshaller[NodeSeq] =
Marshaller.StringMarshaller.wrap(contentType)(_.toString()) Marshaller.StringMarshaller.wrap(mediaType)(_.toString())
implicit def defaultNodeSeqUnmarshaller: FromEntityUnmarshaller[NodeSeq] = implicit def defaultNodeSeqUnmarshaller: FromEntityUnmarshaller[NodeSeq] =
nodeSeqUnmarshaller(ScalaXmlSupport.nodeSeqContentTypeRanges: _*) nodeSeqUnmarshaller(ScalaXmlSupport.nodeSeqContentTypeRanges: _*)
@ -35,13 +34,12 @@ trait ScalaXmlSupport {
/** /**
* Provides a SAXParser for the NodeSeqUnmarshaller to use. Override to provide a custom SAXParser implementation. * 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]]. * Will be called once for for every request to be unmarshalled. The default implementation calls [[ScalaXmlSupport.createSaferSAXParser]].
* @return
*/ */
protected def createSAXParser(): SAXParser = ScalaXmlSupport.createSaferSAXParser() protected def createSAXParser(): SAXParser = ScalaXmlSupport.createSaferSAXParser()
} }
object ScalaXmlSupport extends ScalaXmlSupport { object ScalaXmlSupport extends ScalaXmlSupport {
val nodeSeqContentTypes: immutable.Seq[ContentType] = List(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`) val nodeSeqMediaTypes: immutable.Seq[MediaType.NonBinary] = List(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`)
val nodeSeqContentTypeRanges: immutable.Seq[ContentTypeRange] = nodeSeqContentTypes.map(ContentTypeRange(_)) val nodeSeqContentTypeRanges: immutable.Seq[ContentTypeRange] = nodeSeqMediaTypes.map(ContentTypeRange(_))
/** Creates a safer SAXParser. */ /** Creates a safer SAXParser. */
def createSaferSAXParser(): SAXParser = { def createSaferSAXParser(): SAXParser = {

View file

@ -4,6 +4,7 @@
package akka.http.javadsl.server.values; package akka.http.javadsl.server.values;
import akka.http.javadsl.model.HttpCharsets;
import akka.http.javadsl.model.HttpRequest; import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.MediaTypes; import akka.http.javadsl.model.MediaTypes;
import akka.http.javadsl.server.RequestVal; import akka.http.javadsl.server.RequestVal;
@ -50,7 +51,7 @@ public class FormFieldsTest extends JUnitRouteTest {
return return
HttpRequest.POST("/test") 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) { private HttpRequest singleParameterUrlEncodedRequest(String name, String value) {
return urlEncodedRequest(entry(name, value)); return urlEncodedRequest(entry(name, value));

View file

@ -7,16 +7,15 @@ package akka.http.scaladsl.marshallers.xml
import java.io.File import java.io.File
import akka.http.scaladsl.TestUtils import akka.http.scaladsl.TestUtils
import scala.concurrent.duration._
import org.xml.sax.SAXParseException import org.xml.sax.SAXParseException
import scala.concurrent.{ Future, Await }
import scala.xml.NodeSeq import scala.xml.NodeSeq
import scala.concurrent.{ Future, Await }
import scala.concurrent.duration._
import org.scalatest.{ Inside, FreeSpec, Matchers } import org.scalatest.{ Inside, FreeSpec, Matchers }
import akka.util.ByteString
import akka.http.scaladsl.testkit.ScalatestRouteTest import akka.http.scaladsl.testkit.ScalatestRouteTest
import akka.http.scaladsl.unmarshalling.{ Unmarshaller, Unmarshal } import akka.http.scaladsl.unmarshalling.{ Unmarshaller, Unmarshal }
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import HttpCharsets._
import MediaTypes._ import MediaTypes._
class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest with Inside { class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest with Inside {
@ -25,13 +24,13 @@ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest
"NodeSeqMarshaller should" - { "NodeSeqMarshaller should" - {
"marshal xml snippets to `text/xml` content in UTF-8" in { "marshal xml snippets to `text/xml` content in UTF-8" in {
marshal(<employee><nr>Hallo</nr></employee>) shouldEqual marshal(<employee><nr>Hallo</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 `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 { "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: _*)) haveFailedWith(Unmarshaller.UnsupportedContentTypeException(nodeSeqContentTypeRanges: _*))
} }
@ -43,7 +42,7 @@ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest
| <!ELEMENT foo ANY > | <!ELEMENT foo ANY >
| <!ENTITY xxe SYSTEM "${f.toURI}">]><foo>hello&xxe;</foo>""".stripMargin | <!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 { "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; | %xpe;
| %pe; | %pe;
| ]><foo>hello&xxe;</foo>""".stripMargin | ]><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 | <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 { "gracefully fail when an entity expands to be very large" in {
val as = "a" * 50000 val as = "a" * 50000
@ -83,7 +82,7 @@ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest
| <!ENTITY a "$as"> | <!ENTITY a "$as">
| ]> | ]>
| <kaboom>$entities</kaboom>""".stripMargin | <kaboom>$entities</kaboom>""".stripMargin
shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(`text/xml`, xml)).to[NodeSeq]) shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(ContentTypes.`text/xml(UTF-8)`, xml)).to[NodeSeq])
} }
} }
} }

View file

@ -4,10 +4,10 @@
package akka.http.scaladsl.marshalling package akka.http.scaladsl.marshalling
import akka.http.scaladsl.model.MediaType.Encoding
import scala.concurrent.Await import scala.concurrent.Await
import scala.concurrent.duration._ import scala.concurrent.duration._
import akka.http.scaladsl.server.ContentNegotiator.Alternative
import akka.util.ByteString
import org.scalatest.{ Matchers, FreeSpec } import org.scalatest.{ Matchers, FreeSpec }
import akka.http.scaladsl.util.FastFuture._ import akka.http.scaladsl.util.FastFuture._
import akka.http.scaladsl.model._ 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)" - { "Content Negotiation should work properly for requests with header(s)" - {
"(without headers)" test { accept "(without headers)" test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`) accept(`text/plain` withCharset `UTF-16`) should select(`text/plain` withCharset `UTF-16`)
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `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: */*" test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`) accept(`text/plain`) should select(`text/plain` withCharset `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: */*;q=.8" test { accept "Accept: */*;q=.8" test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`) accept(`text/plain`) should select(`text/plain` withCharset `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/*" test { accept "Accept: text/*" test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`) accept(`text/plain`) should select(`text/plain` withCharset `UTF-8`)
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`) accept(`text/xml` withCharset `UTF-16`) should select(`text/xml` withCharset `UTF-16`)
accept(`audio/ogg`) should reject accept(`audio/ogg`) should reject
} }
"Accept: text/*;q=.8" test { accept "Accept: text/*;q=.8" test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`) accept(`text/plain`) should select(`text/plain` withCharset `UTF-8`)
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`) accept(`text/xml` withCharset `UTF-16`) should select(`text/xml` withCharset `UTF-16`)
accept(`audio/ogg`) should reject accept(`audio/ogg`) should reject
} }
@ -51,39 +52,43 @@ class ContentNegotiationSpec extends FreeSpec with Matchers {
accept(`audio/ogg`) should reject 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-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 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 "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(`text/plain` withCharset `UTF-8`) should reject
} }
"Accept-Charset: UTF-16, UTF-8" test { accept "Accept-Charset: UTF-16, UTF-8" test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`) accept(`text/plain`) should select(`text/plain` withCharset `UTF-16`)
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `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-Charset: UTF-8;q=.2, 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 select(`text/plain`, `UTF-8`) accept(`text/plain` withCharset `UTF-8`) should select(`text/plain` withCharset `UTF-8`)
} }
"Accept-Charset: ISO-8859-1;q=.2" test { accept "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(`text/plain` withCharset `UTF-8`) should reject
} }
"Accept-Charset: latin1;q=.1, UTF-8;q=.2" test { accept "Accept-Charset: latin1;q=.1, UTF-8;q=.2" test { accept
accept(`text/plain`) 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`, `UTF-8`) accept(`text/plain` withCharset `UTF-8`) should select(`text/plain` withCharset `UTF-8`)
} }
"Accept-Charset: *" test { accept "Accept-Charset: *" test { accept
accept(`text/plain`) should select(`text/plain`, `UTF-8`) accept(`text/plain`) should select(`text/plain` withCharset `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-Charset: *;q=0" test { accept "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-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(`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/xml, text/html;q=.5" test { accept
accept(`text/plain`) should reject accept(`text/plain`) should reject
accept(`text/xml`) should select(`text/xml`, `UTF-8`) accept(`text/xml`) should select(`text/xml` withCharset `UTF-8`)
accept(`text/html`) should select(`text/html`, `UTF-8`) accept(`text/html`) should select(`text/html` withCharset `UTF-8`)
accept(`text/html`, `text/xml`) should select(`text/xml`, `UTF-8`) accept(`text/html`, `text/xml`) should select(`text/xml` withCharset `UTF-8`)
accept(`text/xml`, `text/html`) should select(`text/xml`, `UTF-8`) accept(`text/xml`, `text/html`) should select(`text/xml` withCharset `UTF-8`)
accept(`text/plain`, `text/xml`) should select(`text/xml`, `UTF-8`) accept(`text/plain`, `text/xml`) should select(`text/xml` withCharset `UTF-8`)
accept(`text/plain`, `text/html`) should select(`text/html`, `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: text/html, text/plain;q=0.8, application/*;q=.5, *;q= .2
|Accept-Charset: UTF-16""" test { accept |Accept-Charset: UTF-16""" test { accept
accept(`text/plain`, `text/html`, `audio/ogg`) should select(`text/html`, `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`, `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`, `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`, `UTF-16`) accept(`image/gif`, `application/javascript`) should select(`application/javascript` withCharset `UTF-16`)
accept(`image/gif`, `audio/ogg`) should select(`image/gif`) 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) val request = HttpRequest(headers = headers.toVector)
body { contentTypes body { alternatives
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
// creates a pseudo marshaller for X, that applies for all the given content types // creates a pseudo marshaller for X, that applies for all the given content types
trait X trait X
object X extends X object X extends X
implicit val marshallers: ToEntityMarshaller[X] = implicit val marshallers: ToEntityMarshaller[X] =
Marshaller.oneOf(contentTypes map { Marshaller.oneOf(alternatives map {
case ct @ ContentType(mt, Some(cs)) case Alternative.ContentType(ct) Marshaller.withFixedContentType(ct)((s: X) HttpEntity(ct, ByteString("The X")))
Marshaller.withFixedCharset(mt, cs)((s: X) HttpEntity(ct, "The X")) case Alternative.MediaType(mt) Marshaller.withOpenCharset(mt)((s: X, cs) HttpEntity(mt withCharset cs, "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"))
}
}: _*) }: _*)
Await.result(Marshal(X).toResponseFor(request) Await.result(Marshal(X).toResponseFor(request)
@ -142,11 +153,10 @@ class ContentNegotiationSpec extends FreeSpec with Matchers {
} }
def reject = equal(None) def reject = equal(None)
def select(mediaType: MediaType, charset: HttpCharset) = equal(Some(ContentType(mediaType, charset))) def select(contentType: ContentType) = equal(Some(contentType))
def select(mediaType: MediaType) = equal(Some(ContentType(mediaType, None)))
implicit class AddStringToIn(example: String) { 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 = val headers =
if (example != "(without headers)") { if (example != "(without headers)") {
example.stripMarginWithNewline("\n").split('\n').toList map { rawHeader example.stripMarginWithNewline("\n").split('\n').toList map { rawHeader

View file

@ -4,14 +4,14 @@
package akka.http.scaladsl.marshalling package akka.http.scaladsl.marshalling
import akka.http.scaladsl.testkit.MarshallingTestUtils
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
import scala.collection.immutable.ListMap import scala.collection.immutable.ListMap
import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers } import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers }
import akka.util.ByteString
import akka.actor.ActorSystem import akka.actor.ActorSystem
import akka.stream.ActorMaterializer import akka.stream.ActorMaterializer
import akka.stream.scaladsl.Source 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.impl.util._
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import headers._ 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 { "FormDataMarshaller should marshal FormData instances to application/x-www-form-urlencoded content" in {
marshal(FormData(Map("name" -> "Bob", "pass" -> "hällo", "admin" -> ""))) shouldEqual 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" - { "multipartMarshaller should correctly marshal multipart content with" - {
"one empty part" in { "one empty part" in {
marshal(Multipart.General(`multipart/mixed`, Multipart.General.BodyPart.Strict(""))) shouldEqual HttpEntity( 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 string = s"""--$randomBoundary
|Content-Type: text/plain; charset=UTF-8 |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 { "one non-empty part" in {
marshal(Multipart.General(`multipart/alternative`, Multipart.General.BodyPart.Strict( 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 headers = `Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")) :: Nil))) shouldEqual
HttpEntity( HttpEntity(
contentType = ContentType(`multipart/alternative` withBoundary randomBoundary), contentType = `multipart/alternative` withBoundary randomBoundary withCharset `UTF-8`,
string = s"""--$randomBoundary string = s"""--$randomBoundary
|Content-Type: text/plain; charset=UTF-8 |Content-Type: text/plain; charset=UTF-8
|Content-Disposition: form-data; name=email |Content-Disposition: form-data; name=email
@ -74,12 +74,12 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with
} }
"two different parts" in { "two different parts" in {
marshal(Multipart.General(`multipart/related`, 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( Multipart.General.BodyPart.Strict(
HttpEntity(ContentType(`application/octet-stream`), "filecontent"), HttpEntity(`application/octet-stream`, ByteString("filecontent")),
RawHeader("Content-Transfer-Encoding", "binary") :: Nil))) shouldEqual RawHeader("Content-Transfer-Encoding", "binary") :: Nil))) shouldEqual
HttpEntity( HttpEntity(
contentType = ContentType(`multipart/related` withBoundary randomBoundary), contentType = `multipart/related` withBoundary randomBoundary withCharset `UTF-8`,
string = s"""--$randomBoundary string = s"""--$randomBoundary
|Content-Type: text/plain; charset=US-ASCII |Content-Type: text/plain; charset=US-ASCII
| |
@ -100,7 +100,7 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with
"surname" -> HttpEntity("Mike"), "surname" -> HttpEntity("Mike"),
"age" -> marshal(<int>42</int>)))) shouldEqual "age" -> marshal(<int>42</int>)))) shouldEqual
HttpEntity( HttpEntity(
contentType = ContentType(`multipart/form-data` withBoundary randomBoundary), contentType = `multipart/form-data` withBoundary randomBoundary withCharset `UTF-8`,
string = s"""--$randomBoundary string = s"""--$randomBoundary
|Content-Type: text/plain; charset=UTF-8 |Content-Type: text/plain; charset=UTF-8
|Content-Disposition: form-data; name=surname |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 { "two fields having a custom `Content-Disposition`" in {
marshal(Multipart.FormData(Source(List( 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")), Map("filename" -> "attachment.csv")),
Multipart.FormData.BodyPart("attachment[1]", HttpEntity("naice!".getBytes), Multipart.FormData.BodyPart("attachment[1]", HttpEntity("naice!".getBytes),
Map("filename" -> "attachment2.csv"), List(RawHeader("Content-Transfer-Encoding", "binary"))))))) shouldEqual Map("filename" -> "attachment2.csv"), List(RawHeader("Content-Transfer-Encoding", "binary"))))))) shouldEqual
HttpEntity( HttpEntity(
contentType = ContentType(`multipart/form-data` withBoundary randomBoundary), contentType = `multipart/form-data` withBoundary randomBoundary withCharset `UTF-8`,
string = s"""--$randomBoundary string = s"""--$randomBoundary
|Content-Type: text/csv |Content-Type: text/csv; charset=UTF-8
|Content-Disposition: form-data; filename=attachment.csv; name="attachment[0]" |Content-Disposition: form-data; filename=attachment.csv; name="attachment[0]"
| |
|name,age |name,age

View file

@ -201,7 +201,7 @@ class CodingDirectivesSpec extends RoutingSpec with Inside {
() text.grouped(8).map { chars () text.grouped(8).map { chars
Chunk(chars.mkString): ChunkStreamPart 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) ~> { Post() ~> `Accept-Encoding`(gzip) ~> {
encodeResponseWith(Gzip) { encodeResponseWith(Gzip) {
@ -348,6 +348,13 @@ class CodingDirectivesSpec extends RoutingSpec with Inside {
response should haveContentEncoding(gzip) response should haveContentEncoding(gzip)
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped) 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 { "reject the request if it has an Accept-Encoding header with an encoding that doesn't match" in {
Get("/") ~> `Accept-Encoding`(deflate) ~> { Get("/") ~> `Accept-Encoding`(deflate) ~> {

View file

@ -9,6 +9,7 @@ import java.io.File
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.{ ExecutionContext, Future } import scala.concurrent.{ ExecutionContext, Future }
import scala.util.Properties import scala.util.Properties
import akka.util.ByteString
import org.scalatest.matchers.Matcher import org.scalatest.matchers.Matcher
import org.scalatest.{ Inside, Inspectors } import org.scalatest.{ Inside, Inspectors }
import akka.http.scaladsl.model.MediaTypes._ import akka.http.scaladsl.model.MediaTypes._
@ -37,7 +38,7 @@ class FileAndResourceDirectivesSpec extends RoutingSpec with Inspectors with Ins
writeAllText("This is PDF", file) writeAllText("This is PDF", file)
Get() ~> getFromFile(file.getPath) ~> check { Get() ~> getFromFile(file.getPath) ~> check {
mediaType shouldEqual `application/pdf` mediaType shouldEqual `application/pdf`
definedCharset shouldEqual None charsetOption shouldEqual None
responseAs[String] shouldEqual "This is PDF" responseAs[String] shouldEqual "This is PDF"
headers should contain(`Last-Modified`(DateTime(file.lastModified))) headers should contain(`Last-Modified`(DateTime(file.lastModified)))
} }
@ -71,7 +72,7 @@ class FileAndResourceDirectivesSpec extends RoutingSpec with Inspectors with Ins
try { try {
writeAllText("ABCDEFGHIJKLMNOPQRSTUVWXYZ", file) writeAllText("ABCDEFGHIJKLMNOPQRSTUVWXYZ", file)
val rangeHeader = Range(ByteRange(1, 10), ByteRange.suffix(10)) 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 status shouldEqual StatusCodes.PartialContent
header[`Content-Range`] shouldEqual None header[`Content-Range`] shouldEqual None
mediaType.withParams(Map.empty) shouldEqual `multipart/byteranges` mediaType.withParams(Map.empty) shouldEqual `multipart/byteranges`

View file

@ -6,7 +6,6 @@ package akka.http.scaladsl.server.directives
import java.io.{ FileInputStream, File } import java.io.{ FileInputStream, File }
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import akka.http.scaladsl.server.{ MissingFormFieldRejection, RoutingSpec } import akka.http.scaladsl.server.{ MissingFormFieldRejection, RoutingSpec }
import akka.util.ByteString import akka.util.ByteString
@ -22,7 +21,7 @@ class FileUploadDirectivesSpec extends RoutingSpec {
val simpleMultipartUpload = val simpleMultipartUpload =
Multipart.FormData(Multipart.FormData.BodyPart.Strict( Multipart.FormData(Multipart.FormData.BodyPart.Strict(
"fieldName", "fieldName",
HttpEntity(MediaTypes.`text/xml`, xml), HttpEntity(ContentTypes.`text/xml(UTF-8)`, xml),
Map("filename" -> "age.xml"))) Map("filename" -> "age.xml")))
@volatile var file: Option[File] = None @volatile var file: Option[File] = None
@ -36,7 +35,7 @@ class FileUploadDirectivesSpec extends RoutingSpec {
} }
} ~> check { } ~> check {
file.isDefined === true 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 read(file.get) === xml
} }
} finally { } finally {
@ -72,7 +71,7 @@ class FileUploadDirectivesSpec extends RoutingSpec {
val multipartForm = val multipartForm =
Multipart.FormData(Multipart.FormData.BodyPart.Strict( Multipart.FormData(Multipart.FormData.BodyPart.Strict(
"field1", "field1",
HttpEntity(MediaTypes.`text/plain`, str1), HttpEntity(ContentTypes.`text/plain(UTF-8)`, str1),
Map("filename" -> "data1.txt"))) Map("filename" -> "data1.txt")))
Post("/", multipartForm) ~> route ~> check { Post("/", multipartForm) ~> route ~> check {
@ -93,11 +92,11 @@ class FileUploadDirectivesSpec extends RoutingSpec {
Multipart.FormData( Multipart.FormData(
Multipart.FormData.BodyPart.Strict( Multipart.FormData.BodyPart.Strict(
"field1", "field1",
HttpEntity(MediaTypes.`text/plain`, str1), HttpEntity(ContentTypes.`text/plain(UTF-8)`, str1),
Map("filename" -> "data1.txt")), Map("filename" -> "data1.txt")),
Multipart.FormData.BodyPart.Strict( Multipart.FormData.BodyPart.Strict(
"field1", "field1",
HttpEntity(MediaTypes.`text/plain`, str2), HttpEntity(ContentTypes.`text/plain(UTF-8)`, str2),
Map("filename" -> "data2.txt"))) Map("filename" -> "data2.txt")))
Post("/", multipartForm) ~> route ~> check { Post("/", multipartForm) ~> route ~> check {
@ -130,7 +129,7 @@ class FileUploadDirectivesSpec extends RoutingSpec {
val multipartForm = val multipartForm =
Multipart.FormData(Multipart.FormData.BodyPart.Strict( Multipart.FormData(Multipart.FormData.BodyPart.Strict(
"field1", "field1",
HttpEntity(MediaTypes.`text/plain`, str1), HttpEntity(ContentTypes.`text/plain(UTF-8)`, str1),
Map("filename" -> "data1.txt"))) Map("filename" -> "data1.txt")))
Post("/", multipartForm) ~> route ~> check { Post("/", multipartForm) ~> route ~> check {

View file

@ -24,18 +24,18 @@ class FormFieldDirectivesSpec extends RoutingSpec {
val multipartForm = Multipart.FormData { val multipartForm = Multipart.FormData {
Map( Map(
"firstName" -> HttpEntity("Mike"), "firstName" -> HttpEntity("Mike"),
"age" -> HttpEntity(`text/xml`, "<int>42</int>"), "age" -> HttpEntity(ContentTypes.`text/xml(UTF-8)`, "<int>42</int>"),
"VIPBoolean" -> HttpEntity("true")) "VIPBoolean" -> HttpEntity("true"))
} }
val multipartFormWithTextHtml = Multipart.FormData { val multipartFormWithTextHtml = Multipart.FormData {
Map( Map(
"firstName" -> HttpEntity("Mike"), "firstName" -> HttpEntity("Mike"),
"age" -> HttpEntity(`text/xml`, "<int>42</int>"), "age" -> HttpEntity(ContentTypes.`text/xml(UTF-8)`, "<int>42</int>"),
"VIP" -> HttpEntity(`text/html`, "<b>yes</b>"), "VIP" -> HttpEntity(ContentTypes.`text/html(UTF-8)`, "<b>yes</b>"),
"super" -> HttpEntity("no")) "super" -> HttpEntity("no"))
} }
val multipartFormWithFile = Multipart.FormData( 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"))) Map("filename" -> "age.xml")))
"The 'formFields' extraction directive" should { "The 'formFields' extraction directive" should {

View file

@ -12,10 +12,10 @@ import akka.http.scaladsl.unmarshalling._
import akka.http.scaladsl.marshalling._ import akka.http.scaladsl.marshalling._
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import spray.json.DefaultJsonProtocol._
import MediaTypes._ import MediaTypes._
import HttpCharsets._ import HttpCharsets._
import headers._ import headers._
import spray.json.DefaultJsonProtocol._
class MarshallingDirectivesSpec extends RoutingSpec with Inside { class MarshallingDirectivesSpec extends RoutingSpec with Inside {
import ScalaXmlSupport._ import ScalaXmlSupport._
@ -27,9 +27,10 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside {
case x { val i = x.text.toInt; require(i >= 0); i } 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] = implicit val IntMarshaller: ToEntityMarshaller[Int] =
Marshaller.oneOf(ContentType(`application/xhtml+xml`), ContentType(`text/xml`, `UTF-8`)) { contentType Marshaller.oneOf[MediaType.NonBinary, Int, MessageEntity](`application/xhtml+xml`, `text/xxml`) { mediaType
nodeSeqMarshaller(contentType).wrap(contentType) { (i: Int) <int>{ i }</int> } nodeSeqMarshaller(mediaType).wrap(mediaType) { (i: Int) <int>{ i }</int> }
} }
"The 'entityAs' directive" should { "The 'entityAs' directive" should {
@ -44,7 +45,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside {
} ~> check { rejection shouldEqual RequestEntityExpectedRejection } } ~> check { rejection shouldEqual RequestEntityExpectedRejection }
} }
"return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope" in { "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 } entity(as[NodeSeq]) { echoComplete }
} ~> check { } ~> check {
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`)) 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 { "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[NodeSeq]) { _ completeOk } ~
entity(as[String]) { _ validate(false, "Problem") { completeOk } } entity(as[String]) { _ validate(false, "Problem") { completeOk } }
} ~> check { rejection shouldEqual ValidationRejection("Problem") } } ~> 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 { "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 } entity(as[NodeSeq]) { _ completeOk }
} ~> check { } ~> check {
rejection shouldEqual MalformedRequestContentRejection( rejection shouldEqual MalformedRequestContentRejection(
@ -89,7 +90,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside {
} ~> check { responseAs[String] shouldEqual "None" } } ~> check { responseAs[String] shouldEqual "None" }
} }
"return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope (for Option[T]s)" in { "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 } entity(as[Option[NodeSeq]]) { echoComplete }
} ~> check { } ~> check {
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`)) 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 } 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)" responseAs[String] shouldEqual "Person(Peter Xml)"
} }
Put("/", HttpEntity(`application/json`, """{ "name": "Paul Json" }""")) ~> route ~> check { Put("/", HttpEntity(`application/json`, """{ "name": "Paul Json" }""")) ~> route ~> check {
responseAs[String] shouldEqual "Person(Paul Json)" 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`)) 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 { "return a UnacceptedResponseContentTypeRejection rejection if no acceptable marshaller is in scope" in {
Get() ~> Accept(`text/css`) ~> completeWith(instanceOf[Int]) { prod prod(42) } ~> check { 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 { "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 def times2(x: Int) = x * 2
"support proper round-trip content unmarshalling/marshalling to and from a function" in ( "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) Put("/", HttpEntity(ContentTypes.`text/html(UTF-8)`, "<int>42</int>")) ~> Accept(`text/xxml`) ~> handleWith(times2)
~> check { responseEntity shouldEqual HttpEntity(ContentType(`text/xml`, `UTF-8`), "<int>84</int>") }) ~> check { responseEntity shouldEqual HttpEntity(`text/xxml`, "<int>84</int>") })
"result in UnsupportedRequestContentTypeRejection rejection if there is no unmarshaller supporting the requests charset" in ( "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 { ~> check {
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(ContentTypeRange(`text/xml`, iso88592), `text/html`)) 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 ( "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 { 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 { "render JSON with UTF-8 encoding if no `Accept-Charset` request header is present" in {
Get() ~> complete(foo) ~> check { 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 { "reject JSON rendering if an `Accept-Charset` request header requests a non-UTF-8 encoding" in {

View file

@ -34,10 +34,6 @@ class MiscDirectivesSpec extends RoutingSpec {
"the selectPreferredLanguage directive" should { "the selectPreferredLanguage directive" should {
"Accept-Language: de, en" test { selectFrom "Accept-Language: de, en" test { selectFrom
selectFrom("de", "en") shouldEqual "de" 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" selectFrom("en", "de") shouldEqual "en"
} }
"Accept-Language: en, de;q=.5" test { selectFrom "Accept-Language: en, de;q=.5" test { selectFrom

View file

@ -121,7 +121,7 @@ class RangeDirectivesSpec extends RoutingSpec with Inspectors with Inside {
def entityData() = StreamUtils.oneTimeSource(Source.single(ByteString(content))) def entityData() = StreamUtils.oneTimeSource(Source.single(ByteString(content)))
Get() ~> addHeader(Range(ByteRange(5, 10), ByteRange(0, 1), ByteRange(1, 2))) ~> { 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 { } ~> check {
header[`Content-Range`] should be(None) header[`Content-Range`] should be(None)
val parts = Await.result(responseAs[Multipart.ByteRanges].parts.grouped(1000).runWith(Sink.head), 1.second) val parts = Await.result(responseAs[Multipart.ByteRanges].parts.grouped(1000).runWith(Sink.head), 1.second)

View file

@ -6,9 +6,9 @@ package akka.http.scaladsl.server.directives
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport
import akka.stream.scaladsl.Sink
import org.scalatest.FreeSpec import org.scalatest.FreeSpec
import scala.concurrent.{ Future, Promise } import scala.concurrent.{ Future, Promise }
import akka.testkit.EventFilter
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._ import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
import akka.http.scaladsl.marshalling._ import akka.http.scaladsl.marshalling._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
@ -16,9 +16,6 @@ import akka.http.scaladsl.model._
import akka.http.impl.util._ import akka.http.impl.util._
import headers._ import headers._
import StatusCodes._ import StatusCodes._
import MediaTypes._
import scala.xml.NodeSeq
import akka.testkit.EventFilter
class RouteDirectivesSpec extends FreeSpec with GenericRoutingSpec { class RouteDirectivesSpec extends FreeSpec with GenericRoutingSpec {
@ -122,7 +119,8 @@ class RouteDirectivesSpec extends FreeSpec with GenericRoutingSpec {
} ~> check { } ~> check {
response shouldEqual HttpResponse( response shouldEqual HttpResponse(
status = 302, 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) headers = Location("/foo") :: Nil)
} }
} }

View file

@ -18,6 +18,7 @@ import akka.http.scaladsl.util.FastFuture._
import akka.http.impl.util._ import akka.http.impl.util._
import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.model.headers._
import MediaTypes._ import MediaTypes._
import HttpCharsets._
class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAfterAll with ScalatestUtils { class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAfterAll with ScalatestUtils {
implicit val system = ActorSystem(getClass.getSimpleName) 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" - { "multipartGeneralUnmarshaller should correctly unmarshal 'multipart/*' content with" - {
"an empty part" in { "an empty part" 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( |--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`))) Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
} }
"two empty parts" in { "two empty parts" in {
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC", Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`,
"""--XYZABC """--XYZABC
|--XYZABC |--XYZABC
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts( |--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)`))) Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
} }
"a part without entity and missing header separation CRLF" in { "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 """--XYZABC
|Content-type: text/xml |Content-type: text/xml
|Age: 12 |Age: 12
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts( |--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 { "an implicitly typed part (without headers) (Strict)" in {
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC", Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`,
"""--XYZABC """--XYZABC
| |
|Perfectly fine part content. |Perfectly fine part content.
@ -63,12 +64,12 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|Perfectly fine part content. |Perfectly fine part content.
|--XYZABC--""".stripMarginWithNewline("\r\n") |--XYZABC--""".stripMarginWithNewline("\r\n")
val byteStrings = content.map(c ByteString(c.toString)) // one-char ByteStrings 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( .to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "Perfectly fine part content."))) Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "Perfectly fine part content.")))
} }
"one non-empty form-data part" in { "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-type: text/plain; charset=UTF8
|content-disposition: form-data; name="email" |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"))))) List(`Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")))))
} }
"two different parts" in { "two different parts" in {
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345", Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345" withCharset `UTF-8`,
"""--12345 """--12345
| |
|first part, with a trailing newline |first part, with a trailing newline
@ -92,10 +93,11 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|filecontent |filecontent
|--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts( |--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(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 ( "illegal headers" in (
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC", Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC" withCharset `UTF-8`,
"""--XYZABC """--XYZABC
|Date: unknown |Date: unknown
|content-disposition: form-data; name=email |content-disposition: form-data; name=email
@ -107,7 +109,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
List(RawHeader("date", "unknown"), List(RawHeader("date", "unknown"),
`Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")))))) `Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email"))))))
"a full example (Strict)" in { "a full example (Strict)" in {
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345", Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345" withCharset `UTF-8`,
"""preamble and """preamble and
|more preamble |more preamble
|--12345 |--12345
@ -121,7 +123,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|epilogue and |epilogue and
|more epilogue""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts( |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(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 { "a full example (Default)" in {
val content = """preamble and val content = """preamble and
@ -137,13 +139,13 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|epilogue and |epilogue and
|more epilogue""".stripMarginWithNewline("\r\n") |more epilogue""".stripMarginWithNewline("\r\n")
val byteStrings = content.map(c ByteString(c.toString)) // one-char ByteStrings 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( .to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")), 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 { "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
|--simple boundary--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts( |--simple boundary--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`))) 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" - { "multipartGeneralUnmarshaller should reject illegal multipart content with" - {
"an empty entity" in { "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" .to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity"
} }
"an entity without initial boundary" in { "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 """this is
|just preamble text""".stripMarginWithNewline("\r\n"))) |just preamble text""".stripMarginWithNewline("\r\n")))
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity" .to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity"
} }
"a stray boundary" in { "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 """--ABC
|Content-type: text/plain; charset=UTF8 |Content-type: text/plain; charset=UTF8
|--ABCContent-type: application/json |--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" .to[Multipart.General].failed, 1.second).getMessage shouldEqual "Illegal multipart boundary in message content"
} }
"duplicate Content-Type header" in { "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: text/plain; charset=UTF8
|Content-type: application/json |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" "multipart part must not contain more than one Content-Type header"
} }
"a missing header-separating CRLF (in Strict entity)" in { "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 |not good here
|-----""".stripMarginWithNewline("\r\n"))) |-----""".stripMarginWithNewline("\r\n")))
@ -197,27 +199,27 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
|not ok |not ok
|-----""".stripMarginWithNewline("\r\n") |-----""".stripMarginWithNewline("\r\n")
val byteStrings = content.map(c ByteString(c.toString)) // one-char ByteStrings 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))) Await.result(Unmarshal(HttpEntity.Default(contentType, content.length, Source(byteStrings)))
.to[Multipart.General] .to[Multipart.General]
.flatMap(_ toStrict 1.second).failed, 1.second).getMessage shouldEqual "Illegal character ' ' in header name" .flatMap(_ toStrict 1.second).failed, 1.second).getMessage shouldEqual "Illegal character ' ' in header name"
} }
"a boundary with a trailing space" in { "a boundary with a trailing space" in {
Await.result( 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 .to[Multipart.General].failed, 1.second).getMessage shouldEqual
"requirement failed: 'boundary' parameter of multipart Content-Type must not end with a space char" "requirement failed: 'boundary' parameter of multipart Content-Type must not end with a space char"
} }
"a boundary with an illegal character" in { "a boundary with an illegal character" in {
Await.result( 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 .to[Multipart.General].failed, 1.second).getMessage shouldEqual
"requirement failed: 'boundary' parameter of multipart Content-Type contains illegal character '&'" "requirement failed: 'boundary' parameter of multipart Content-Type contains illegal character '&'"
} }
} }
"multipartByteRangesUnmarshaller should correctly unmarshal multipart/byteranges content with two different parts" in { "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 """--12345
|Content-Range: bytes 0-2/26 |Content-Range: bytes 0-2/26
|Content-Type: text/plain |Content-Type: text/plain
@ -229,24 +231,24 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
| |
|XYZ |XYZ
|--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.ByteRanges] should haveParts( |--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(0, 2, 26), HttpEntity(ContentTypes.`text/plain(UTF-8)`, "ABC")),
Multipart.ByteRanges.BodyPart.Strict(ContentRange(23, 25, 26), HttpEntity(ContentTypes.`text/plain`, "XYZ"))) Multipart.ByteRanges.BodyPart.Strict(ContentRange(23, 25, 26), HttpEntity(ContentTypes.`text/plain(UTF-8)`, "XYZ")))
} }
"multipartFormDataUnmarshaller should correctly unmarshal 'multipart/form-data' content" - { "multipartFormDataUnmarshaller should correctly unmarshal 'multipart/form-data' content" - {
"with one element" in { "with one element" in {
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC", Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC" withCharset `UTF-8`,
"""--XYZABC """--XYZABC
|content-disposition: form-data; name=email |content-disposition: form-data; name=email
| |
|test@there.com |test@there.com
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.FormData] should haveParts( |--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 { "with a file" in {
Unmarshal { Unmarshal {
HttpEntity.Default( HttpEntity.Default(
contentType = `multipart/form-data` withBoundary "XYZABC", contentType = `multipart/form-data` withBoundary "XYZABC" withCharset `UTF-8`,
contentLength = 1, // not verified during unmarshalling contentLength = 1, // not verified during unmarshalling
data = Source { data = Source {
List( List(
@ -270,8 +272,8 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf
}) })
}) })
}.to[Multipart.FormData].flatMap(_.toStrict(1.second)) should haveParts( }.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("email", HttpEntity(`application/octet-stream`, ByteString("test@there.com"))),
Multipart.FormData.BodyPart.Strict("userfile", HttpEntity(MediaTypes.`application/pdf`, "filecontent"), Map("filename" -> "test.dat"), Multipart.FormData.BodyPart.Strict("userfile", HttpEntity(`application/pdf`, ByteString("filecontent")), Map("filename" -> "test.dat"),
List( List(
RawHeader("Content-Transfer-Encoding", "binary"), RawHeader("Content-Transfer-Encoding", "binary"),
RawHeader("Content-Additional-1", "anything"), RawHeader("Content-Additional-1", "anything"),

View file

@ -52,7 +52,7 @@ private[http] class RejectionHandlerWrapper(javaHandler: server.RejectionHandler
case RequestEntityExpectedRejection case RequestEntityExpectedRejection
handleRequestEntityExpectedRejection(ctx) handleRequestEntityExpectedRejection(ctx)
case UnacceptedResponseContentTypeRejection(supported) case UnacceptedResponseContentTypeRejection(supported)
handleUnacceptedResponseContentTypeRejection(ctx, supported.toList.toSeq.asJava) handleUnacceptedResponseContentTypeRejection(ctx, supported.toList.map(_.format).toSeq.asJava)
case UnacceptedResponseEncodingRejection(supported) case UnacceptedResponseEncodingRejection(supported)
handleUnacceptedResponseEncodingRejection(ctx, supported.toList.toSeq.asJava) handleUnacceptedResponseEncodingRejection(ctx, supported.toList.toSeq.asJava)
case AuthenticationFailedRejection(cause, challenge) case AuthenticationFailedRejection(cause, challenge)

View file

@ -29,7 +29,7 @@ private[http] final case class RequestContextImpl(underlying: ScalaRequestContex
case r: RouteResultImpl r.underlying case r: RouteResultImpl r.underlying
}(executionContext()) }(executionContext())
def complete(text: String): RouteResult = underlying.complete(text) 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)) underlying.complete(HttpEntity(contentType.asScala, text))
def completeWithStatus(statusCode: Int): RouteResult = def completeWithStatus(statusCode: Int): RouteResult =

View file

@ -28,7 +28,14 @@ object Marshallers {
* Creates a marshaller by specifying a media type and conversion function from ``T`` to String. * 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. * 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(_))) 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] = def toEntity[T](contentType: ContentType, convert: function.Function[T, ResponseEntity]): Marshaller[T] =
MarshallerImpl { _ MarshallerImpl { _
ScalaMarshaller.withFixedCharset(contentType.mediaType().asScala, contentType.charset().asScala)(t ScalaMarshaller.withFixedContentType(contentType.asScala)(t
HttpResponse.create().withStatus(200).withEntity(convert(t)).asScala) 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] = def toResponse[T](contentType: ContentType, convert: function.Function[T, HttpResponse]): Marshaller[T] =
MarshallerImpl { _ MarshallerImpl { _
ScalaMarshaller.withFixedCharset(contentType.mediaType().asScala, contentType.charset().asScala)(t ScalaMarshaller.withFixedContentType(contentType.asScala)(t convert(t).asScala)
convert(t).asScala)
} }
} }

View file

@ -7,7 +7,7 @@ package akka.http.javadsl.server
import java.{ lang jl } import java.{ lang jl }
import akka.http.impl.server.PassRejectionRouteResult 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.javadsl.model.headers.{ HttpChallenge, ByteRange, HttpEncoding }
import akka.http.scaladsl.server.Rejection import akka.http.scaladsl.server.Rejection
@ -122,9 +122,9 @@ abstract class RejectionHandler {
/** /**
* Callback called to handle rejection created by marshallers. * 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 * 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. * Callback called to handle rejection created by encoding filters.

View file

@ -4,11 +4,9 @@
package akka.http.javadsl.server package akka.http.javadsl.server
import scala.concurrent.{ ExecutionContext, Future }
import akka.http.javadsl.model._ import akka.http.javadsl.model._
import akka.stream.Materializer 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 * 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. * 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. * Completes the request with the given status code and no entity.

View file

@ -33,7 +33,7 @@ abstract class BasicDirectives extends BasicDirectivesBase {
/** /**
* A route that completes the request with a static text * 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() { new OpaqueRoute() {
def handle(ctx: RequestContext): RouteResult = def handle(ctx: RequestContext): RouteResult =
ctx.complete(contentType, text) ctx.complete(contentType, text)

View file

@ -36,11 +36,6 @@ trait FileAndResourceRoute extends Route {
*/ */
def withContentType(contentType: ContentType): 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 * Returns a variant of this route that uses the specified [[ContentTypeResolver]] to determine
* which [[ContentType]] to respond with by file name. * which [[ContentType]] to respond with by file name.
@ -55,8 +50,6 @@ object FileAndResourceRoute {
private[http] def apply(f: ContentTypeResolver Route): FileAndResourceRoute = private[http] def apply(f: ContentTypeResolver Route): FileAndResourceRoute =
new FileAndResourceRouteWithDefaultResolver(f) with FileAndResourceRoute { new FileAndResourceRouteWithDefaultResolver(f) with FileAndResourceRoute {
def withContentType(contentType: ContentType): Route = resolveContentTypeWith(StaticContentTypeResolver(contentType)) def withContentType(contentType: ContentType): Route = resolveContentTypeWith(StaticContentTypeResolver(contentType))
def withContentType(mediaType: MediaType): Route = withContentType(mediaType.toContentType)
def resolveContentTypeWith(resolver: ContentTypeResolver): Route = f(resolver) def resolveContentTypeWith(resolver: ContentTypeResolver): Route = f(resolver)
} }
@ -66,8 +59,6 @@ object FileAndResourceRoute {
private[http] def forFixedName(fileName: String)(f: ContentType Route): FileAndResourceRoute = private[http] def forFixedName(fileName: String)(f: ContentType Route): FileAndResourceRoute =
new FileAndResourceRouteWithDefaultResolver(resolver f(resolver.resolve(fileName))) with FileAndResourceRoute { new FileAndResourceRouteWithDefaultResolver(resolver f(resolver.resolve(fileName))) with FileAndResourceRoute {
def withContentType(contentType: ContentType): Route = resolveContentTypeWith(StaticContentTypeResolver(contentType)) 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)) def resolveContentTypeWith(resolver: ContentTypeResolver): Route = f(resolver.resolve(fileName))
} }
} }

View file

@ -56,7 +56,9 @@ object StrictForm {
def unmarshallerFromFSU[T](fsu: FromStringUnmarshaller[T]): FromStrictFormFieldUnmarshaller[T] = def unmarshallerFromFSU[T](fsu: FromStringUnmarshaller[T]): FromStrictFormFieldUnmarshaller[T] =
Unmarshaller.withMaterializer(implicit ec implicit mat { Unmarshaller.withMaterializer(implicit ec implicit mat {
case FromString(value) fsu(value) 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 " + @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]) = implicit def fromFSU[T](implicit fsu: FromStringUnmarshaller[T]) =
new FieldUnmarshaller[T] { new FieldUnmarshaller[T] {
def unmarshalString(value: String)(implicit ec: ExecutionContext, mat: Materializer) = fsu(value) def unmarshalString(value: String)(implicit ec: ExecutionContext, mat: Materializer) = fsu(value)
def unmarshalPart(value: Multipart.FormData.BodyPart.Strict)(implicit ec: ExecutionContext, mat: Materializer) = def unmarshalPart(value: Multipart.FormData.BodyPart.Strict)(implicit ec: ExecutionContext, mat: Materializer) = {
fsu(value.entity.data.decodeString(value.entity.contentType.charset.nioCharset.name)) 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]) = implicit def fromFEU[T](implicit feu: FromEntityUnmarshaller[T]) =
new FieldUnmarshaller[T] { new FieldUnmarshaller[T] {

View file

@ -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))
}
}

View file

@ -5,16 +5,15 @@
package akka.http.scaladsl.marshalling package akka.http.scaladsl.marshalling
import scala.concurrent.{ ExecutionContext, Future } 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.model._
import akka.http.scaladsl.util.FastFuture._ import akka.http.scaladsl.util.FastFuture._
object Marshal { object Marshal {
def apply[T](value: T): Marshal[T] = new Marshal(value) def apply[T](value: T): Marshal[T] = new Marshal(value)
case class UnacceptableResponseContentTypeException(supported: Set[ContentType]) extends RuntimeException case class UnacceptableResponseContentTypeException(supported: Set[ContentNegotiator.Alternative])
extends RuntimeException
private class MarshallingWeight(val weight: Float, val marshal: () HttpResponse)
} }
class Marshal[A](val value: A) { 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] = def to[B](implicit m: Marshaller[A, B], ec: ExecutionContext): Future[B] =
m(value).fast.map { m(value).fast.map {
_.head match { _.head match {
case Marshalling.WithFixedCharset(_, _, marshal) marshal() case Marshalling.WithFixedContentType(_, marshal) marshal()
case Marshalling.WithOpenCharset(_, marshal) marshal(HttpCharsets.`UTF-8`) case Marshalling.WithOpenCharset(_, marshal) marshal(HttpCharsets.`UTF-8`)
case Marshalling.Opaque(marshal) marshal() 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] = { def toResponseFor(request: HttpRequest)(implicit m: ToResponseMarshaller[A], ec: ExecutionContext): Future[HttpResponse] = {
import akka.http.scaladsl.marshalling.Marshal._ import akka.http.scaladsl.marshalling.Marshal._
val mediaRanges = request.acceptedMediaRanges // cache for performance val ctn = ContentNegotiator(request.headers)
val charsetRanges = request.acceptedCharsetRanges // cache for performance
def qValueMT(mediaType: MediaType) = request.qValueForMediaType(mediaType, mediaRanges)
def qValueCS(charset: HttpCharset) = request.qValueForCharset(charset, charsetRanges)
m(value).fast.map { marshallings m(value).fast.map { marshallings
val defaultMarshallingWeight = new MarshallingWeight(0f, { () val supportedAlternatives: List[ContentNegotiator.Alternative] =
val supportedContentTypes = marshallings collect { marshallings.collect {
case Marshalling.WithFixedCharset(mt, cs, _) ContentType(mt, cs) case Marshalling.WithFixedContentType(ct, _) ContentNegotiator.Alternative(ct)
case Marshalling.WithOpenCharset(mt, _) ContentType(mt) 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) { } else None
case (acc, Marshalling.WithFixedCharset(mt, cs, marshal)) } orElse {
choose(acc, mt, cs, marshal) marshallings collectFirst { case Marshalling.Opaque(marshal) marshal }
case (acc, Marshalling.WithOpenCharset(mt, marshal)) } getOrElse {
def withCharset(cs: HttpCharset) = choose(acc, mt, cs, () marshal(cs)) throw UnacceptableResponseContentTypeException(supportedAlternatives.toSet)
// 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
} }
bestMarshal()
case (acc, Marshalling.Opaque(marshal))
if (acc.weight == 0f) new MarshallingWeight(Float.MinPositiveValue, marshal) else acc
}
best.marshal()
} }
} }
} }

View file

@ -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 * Reuses this Marshaller's logic to produce a new Marshaller from another type `C` which overrides
* the produced [[ContentType]] with another one. * the [[MediaType]] of the marshalling result with the given one.
* Depending on whether the given [[ContentType]] has a defined charset or not and whether the underlying * Note that not all wrappings are legal. f the underlying [[MediaType]] has constraints with regard to the
* marshaller marshals with a fixed charset it can happen, that the wrapping becomes illegal. * charsets it allows the new [[MediaType]] must be compatible, since akka-http will never recode entities.
* For example, a marshaller producing content encoded with UTF-16 cannot be wrapped with a [[ContentType]]
* that has a defined charset of UTF-8, since akka-http will never recode entities.
* If the wrapping is illegal the [[Future]] produced by the resulting marshaller will contain a [[RuntimeException]]. * 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] = def wrap[C, D >: B](newMediaType: MediaType)(f: C A)(implicit mto: ContentTypeOverrider[D]): Marshaller[C, D] =
wrapWithEC[C, D](contentType)(_ f) wrapWithEC[C, D](newMediaType)(_ f)
/** /**
* Reuses this Marshaller's logic to produce a new Marshaller from another type `C` which overrides * Reuses this Marshaller's logic to produce a new Marshaller from another type `C` which overrides
* the produced [[ContentType]] with another one. * the [[MediaType]] of the marshalling result with the given one.
* Depending on whether the given [[ContentType]] has a defined charset or not and whether the underlying * Note that not all wrappings are legal. f the underlying [[MediaType]] has constraints with regard to the
* marshaller marshals with a fixed charset it can happen, that the wrapping becomes illegal. * charsets it allows the new [[MediaType]] must be compatible, since akka-http will never recode entities.
* For example, a marshaller producing content encoded with UTF-16 cannot be wrapped with a [[ContentType]]
* that has a defined charset of UTF-8, since akka-http will never recode entities.
* If the wrapping is illegal the [[Future]] produced by the resulting marshaller will contain a [[RuntimeException]]. * 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 Marshaller { implicit ec
value value
import Marshalling._ import Marshalling._
this(f(ec)(value)).fast map { this(f(ec)(value)).fast map {
_ map { _ map {
case WithFixedCharset(_, cs, marshal) if contentType.hasOpenCharset || contentType.charset == cs (_, newMediaType) match {
WithFixedCharset(contentType.mediaType, cs, () mto(marshal(), contentType.mediaType)) case (WithFixedContentType(_, marshal), newMT: MediaType.Binary)
case WithOpenCharset(_, marshal) if contentType.hasOpenCharset WithFixedContentType(newMT, () cto(marshal(), newMT))
WithOpenCharset(contentType.mediaType, cs mto(marshal(cs), contentType.mediaType)) case (WithFixedContentType(oldCT: ContentType.Binary, marshal), newMT: MediaType.WithFixedCharset)
case WithOpenCharset(_, marshal) WithFixedContentType(newMT, () cto(marshal(), newMT))
WithFixedCharset(contentType.mediaType, contentType.charset, () mto(marshal(contentType.charset), contentType.mediaType)) case (WithFixedContentType(oldCT: ContentType.NonBinary, marshal), newMT: MediaType.WithFixedCharset) if oldCT.charset == newMT.charset
case Opaque(marshal) if contentType.definedCharset.isEmpty Opaque(() mto(marshal(), contentType.mediaType)) WithFixedContentType(newMT, () cto(marshal(), newMT))
case x sys.error(s"Illegal marshaller wrapping. Marshalling `$x` cannot be wrapped with ContentType `$contentType`") 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. * 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] = def withFixedContentType[A, B](contentType: ContentType)(marshal: A B): Marshaller[A, B] =
strict { value Marshalling.WithFixedCharset(mediaType, charset, () marshal(value)) } strict { value Marshalling.WithFixedContentType(contentType, () marshal(value)) }
/** /**
* Helper for creating a synchronous [[Marshaller]] to content with a negotiable charset from the given function. * 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)) } strict { value Marshalling.WithOpenCharset(mediaType, charset marshal(value, charset)) }
/** /**
@ -136,19 +147,19 @@ sealed trait Marshalling[+A] {
} }
object Marshalling { object Marshalling {
/** /**
* A Marshalling to a specific MediaType and charset. * A Marshalling to a specific [[ContentType]].
*/ */
final case class WithFixedCharset[A](mediaType: MediaType, final case class WithFixedContentType[A](contentType: ContentType,
charset: HttpCharset,
marshal: () A) extends Marshalling[A] { 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] { marshal: HttpCharset A) extends Marshalling[A] {
def map[B](f: A B): WithOpenCharset[B] = copy(marshal = cs f(marshal(cs))) def map[B](f: A B): WithOpenCharset[B] = copy(marshal = cs f(marshal(cs)))
} }

View file

@ -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))
}
}

View file

@ -13,8 +13,8 @@ trait MultipartMarshallers {
implicit def multipartMarshaller[T <: Multipart](implicit log: LoggingAdapter = NoLogging): ToEntityMarshaller[T] = implicit def multipartMarshaller[T <: Multipart](implicit log: LoggingAdapter = NoLogging): ToEntityMarshaller[T] =
Marshaller strict { value Marshaller strict { value
val boundary = randomBoundary() val boundary = randomBoundary()
val contentType = ContentType(value.mediaType withBoundary boundary) val mediaType = value.mediaType withBoundary boundary
Marshalling.WithOpenCharset(contentType.mediaType, { charset Marshalling.WithOpenCharset(mediaType, { charset
value.toEntity(charset, boundary)(log) value.toEntity(charset, boundary)(log)
}) })
} }

View file

@ -13,51 +13,39 @@ import akka.util.ByteString
trait PredefinedToEntityMarshallers extends MultipartMarshallers { trait PredefinedToEntityMarshallers extends MultipartMarshallers {
implicit val ByteArrayMarshaller: ToEntityMarshaller[Array[Byte]] = byteArrayMarshaller(`application/octet-stream`) implicit val ByteArrayMarshaller: ToEntityMarshaller[Array[Byte]] = byteArrayMarshaller(`application/octet-stream`)
def byteArrayMarshaller(mediaType: MediaType, charset: HttpCharset): ToEntityMarshaller[Array[Byte]] = { def byteArrayMarshaller(contentType: ContentType): ToEntityMarshaller[Array[Byte]] =
val ct = ContentType(mediaType, charset) Marshaller.withFixedContentType(contentType) { bytes HttpEntity(contentType, bytes) }
Marshaller.withFixedCharset(ct.mediaType, ct.definedCharset.get) { bytes HttpEntity(ct, bytes) }
}
def byteArrayMarshaller(mediaType: MediaType): ToEntityMarshaller[Array[Byte]] = {
val ct = ContentType(mediaType)
// since we don't want to recode we simply ignore the charset determined by content negotiation here
Marshaller.withOpenCharset(ct.mediaType) { (bytes, _) HttpEntity(ct, bytes) }
}
implicit val ByteStringMarshaller: ToEntityMarshaller[ByteString] = byteStringMarshaller(`application/octet-stream`) implicit val ByteStringMarshaller: ToEntityMarshaller[ByteString] = byteStringMarshaller(`application/octet-stream`)
def byteStringMarshaller(mediaType: MediaType, charset: HttpCharset): ToEntityMarshaller[ByteString] = { def byteStringMarshaller(contentType: ContentType): ToEntityMarshaller[ByteString] =
val ct = ContentType(mediaType, charset) Marshaller.withFixedContentType(contentType) { bytes HttpEntity(contentType, bytes) }
Marshaller.withFixedCharset(ct.mediaType, ct.definedCharset.get) { bytes HttpEntity(ct, bytes) }
}
def byteStringMarshaller(mediaType: MediaType): ToEntityMarshaller[ByteString] = {
val ct = ContentType(mediaType)
// since we don't want to recode we simply ignore the charset determined by content negotiation here
Marshaller.withOpenCharset(ct.mediaType) { (bytes, _) HttpEntity(ct, bytes) }
}
implicit val CharArrayMarshaller: ToEntityMarshaller[Array[Char]] = charArrayMarshaller(`text/plain`) implicit val CharArrayMarshaller: ToEntityMarshaller[Array[Char]] = charArrayMarshaller(`text/plain`)
def charArrayMarshaller(mediaType: MediaType): ToEntityMarshaller[Array[Char]] = def charArrayMarshaller(mediaType: MediaType.WithOpenCharset): ToEntityMarshaller[Array[Char]] =
Marshaller.withOpenCharset(mediaType) { (value, charset) 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) { if (value.length > 0) {
val charBuffer = CharBuffer.wrap(value) 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()) val array = new Array[Byte](byteBuffer.remaining())
byteBuffer.get(array) byteBuffer.get(array)
HttpEntity(ContentType(mediaType, charset), array) HttpEntity(contentType, array)
} else HttpEntity.Empty } else HttpEntity.Empty
}
implicit val StringMarshaller: ToEntityMarshaller[String] = stringMarshaller(`text/plain`) implicit val StringMarshaller: ToEntityMarshaller[String] = stringMarshaller(`text/plain`)
def stringMarshaller(mediaType: MediaType): ToEntityMarshaller[String] = def stringMarshaller(mediaType: MediaType.WithOpenCharset): ToEntityMarshaller[String] =
Marshaller.withOpenCharset(mediaType) { (s, cs) HttpEntity(ContentType(mediaType, cs), s) } 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] = implicit val FormDataMarshaller: ToEntityMarshaller[FormData] =
Marshaller.withOpenCharset(`application/x-www-form-urlencoded`) { (formData, charset) Marshaller.withOpenCharset(`application/x-www-form-urlencoded`) { _ toEntity _ }
formData.toEntity(charset)
}
implicit val MessageEntityMarshaller: ToEntityMarshaller[MessageEntity] = Marshaller strict { value implicit val MessageEntityMarshaller: ToEntityMarshaller[MessageEntity] =
Marshalling.WithFixedCharset(value.contentType.mediaType, value.contentType.charset, () value) Marshaller strict { value Marshalling.WithFixedContentType(value.contentType, () value) }
}
} }
object PredefinedToEntityMarshallers extends PredefinedToEntityMarshallers object PredefinedToEntityMarshallers extends PredefinedToEntityMarshallers

View file

@ -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)
}

View file

@ -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 * 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
*/ */
case class UnacceptedResponseContentTypeRejection(supported: immutable.Set[ContentType]) extends Rejection case class UnacceptedResponseContentTypeRejection(supported: immutable.Set[ContentNegotiator.Alternative]) extends Rejection
/** /**
* Rejection created by encoding filters. * Rejection created by encoding filters.

View file

@ -188,8 +188,8 @@ object RejectionHandler {
} }
.handleAll[UnacceptedResponseContentTypeRejection] { rejections .handleAll[UnacceptedResponseContentTypeRejection] { rejections
val supported = rejections.flatMap(_.supported) val supported = rejections.flatMap(_.supported)
complete((NotAcceptable, "Resource representation is only available with these Content-Types:\n" + val msg = supported.map(_.format).mkString("Resource representation is only available with these types:\n", "\n", "")
supported.map(_.value).mkString("\n"))) complete((NotAcceptable, msg))
} }
.handleAll[UnacceptedResponseEncodingRejection] { rejections .handleAll[UnacceptedResponseEncodingRejection] { rejections
val supported = rejections.flatMap(_.supported) val supported = rejections.flatMap(_.supported)

View file

@ -5,10 +5,9 @@
package akka.http.scaladsl.server package akka.http.scaladsl.server
package directives package directives
import scala.annotation.tailrec
import scala.collection.immutable import scala.collection.immutable
import scala.util.control.NonFatal 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.model._
import akka.http.scaladsl.coding._ import akka.http.scaladsl.coding._
import akka.http.impl.util._ import akka.http.impl.util._
@ -26,8 +25,10 @@ trait CodingDirectives {
* if the given response encoding is not accepted by the client. * if the given response encoding is not accepted by the client.
*/ */
def responseEncodingAccepted(encoding: HttpEncoding): Directive0 = def responseEncodingAccepted(encoding: HttpEncoding): Directive0 =
extract(_.request.isEncodingAccepted(encoding)) extractRequest.flatMap { request
.flatMap(if (_) pass else reject(UnacceptedResponseEncodingRejection(Set(encoding)))) 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- * 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 def theseOrDefault[T >: Coder](these: Seq[T]): Seq[T] = if (these.isEmpty) DefaultCoders else these
import BasicDirectives._ import BasicDirectives._
import HeaderDirectives._
import RouteDirectives._ import RouteDirectives._
private def _encodeResponse(encoders: immutable.Seq[Encoder]): Directive0 = private def _encodeResponse(encoders: immutable.Seq[Encoder]): Directive0 =
optionalHeaderValueByType(classOf[`Accept-Encoding`]) flatMap { accept BasicDirectives.extractRequest.flatMap { request
val acceptedEncoder = accept match { val negotiator = EncodingNegotiator(request.headers)
case None val encodings: List[HttpEncoding] = encoders.map(_.encoding)(collection.breakOut)
// use first defined encoder when Accept-Encoding is missing val bestEncoder = negotiator.pickEncoding(encodings).flatMap(be encoders.find(_.encoding == be))
encoders.headOption bestEncoder match {
case Some(`Accept-Encoding`(encodings))
// provide fallback to identity
val withIdentity =
if (encodings.exists {
case HttpEncodingRange.One(HttpEncodings.identity, _) true
case _ false
}) encodings
else encodings :+ HttpEncodings.`identity;q=MIN`
// sort client-accepted encodings by q-Value (and orig. order) and find first matching encoder
@tailrec def find(encodings: List[HttpEncodingRange]): Option[Encoder] = encodings match {
case encoding :: rest
encoders.find(e encoding.matches(e.encoding)) match {
case None find(rest)
case x x
}
case _ None
}
find(withIdentity.sortBy(e (-e.qValue, withIdentity.indexOf(e))).toList)
}
acceptedEncoder match {
case Some(encoder) mapResponse(encoder.encode(_)) case 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))
} }
} }
} }

View file

@ -265,7 +265,7 @@ object ContentTypeResolver {
case x fileName.substring(x + 1) case x fileName.substring(x + 1)
} }
val mediaType = MediaTypes.forExtension(ext) getOrElse MediaTypes.`application/octet-stream` val mediaType = MediaTypes.forExtension(ext) getOrElse MediaTypes.`application/octet-stream`
ContentType(mediaType) withDefaultCharset charset ContentType(mediaType, () charset)
} }
} }

View file

@ -51,18 +51,9 @@ trait MiscDirectives {
* has equal preference for (even if this preference is zero!) * has equal preference for (even if this preference is zero!)
* the order of the arguments is used as a tie breaker (First one wins). * the order of the arguments is used as a tie breaker (First one wins).
*/ */
def selectPreferredLanguage(first: Language, more: Language*): Directive1[Language] = { 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 { request
BasicDirectives.extractRequest.map { req LanguageNegotiator(request.headers).pickLanguage(first :: List(more: _*)) getOrElse first
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
}
} }
} }

View file

@ -34,7 +34,7 @@ trait RouteDirectives {
headers = headers.Location(uri) :: Nil, headers = headers.Location(uri) :: Nil,
entity = redirectionType.htmlTemplate match { entity = redirectionType.htmlTemplate match {
case "" HttpEntity.Empty case "" HttpEntity.Empty
case template HttpEntity(MediaTypes.`text/html`, template format uri) case template HttpEntity(ContentTypes.`text/html(UTF-8)`, template format uri)
}) })
} }
//# //#

View file

@ -25,7 +25,7 @@ trait MultipartUnmarshallers {
def multipartGeneralUnmarshaller(defaultCharset: HttpCharset)(implicit log: LoggingAdapter = NoLogging): FromEntityUnmarshaller[Multipart.General] = def multipartGeneralUnmarshaller(defaultCharset: HttpCharset)(implicit log: LoggingAdapter = NoLogging): FromEntityUnmarshaller[Multipart.General] =
multipartUnmarshaller[Multipart.General, Multipart.General.BodyPart, Multipart.General.BodyPart.Strict]( multipartUnmarshaller[Multipart.General, Multipart.General.BodyPart, Multipart.General.BodyPart.Strict](
mediaRange = `multipart/*`, mediaRange = `multipart/*`,
defaultContentType = ContentTypes.`text/plain` withCharset defaultCharset, defaultContentType = MediaTypes.`text/plain` withCharset defaultCharset,
createBodyPart = Multipart.General.BodyPart(_, _), createBodyPart = Multipart.General.BodyPart(_, _),
createStreamed = Multipart.General(_, _), createStreamed = Multipart.General(_, _),
createStrictBodyPart = Multipart.General.BodyPart.Strict, createStrictBodyPart = Multipart.General.BodyPart.Strict,
@ -45,7 +45,7 @@ trait MultipartUnmarshallers {
def multipartByteRangesUnmarshaller(defaultCharset: HttpCharset)(implicit log: LoggingAdapter = NoLogging): FromEntityUnmarshaller[Multipart.ByteRanges] = def multipartByteRangesUnmarshaller(defaultCharset: HttpCharset)(implicit log: LoggingAdapter = NoLogging): FromEntityUnmarshaller[Multipart.ByteRanges] =
multipartUnmarshaller[Multipart.ByteRanges, Multipart.ByteRanges.BodyPart, Multipart.ByteRanges.BodyPart.Strict]( multipartUnmarshaller[Multipart.ByteRanges, Multipart.ByteRanges.BodyPart, Multipart.ByteRanges.BodyPart.Strict](
mediaRange = `multipart/byteranges`, 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, createBodyPart = (entity, headers) Multipart.General.BodyPart(entity, headers).toByteRangesBodyPart.get,
createStreamed = (_, parts) Multipart.ByteRanges(parts), createStreamed = (_, parts) Multipart.ByteRanges(parts),
createStrictBodyPart = (entity, headers) Multipart.General.BodyPart.Strict(entity, headers).toByteRangesBodyPart.get, 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, def multipartUnmarshaller[T <: Multipart, BP <: Multipart.BodyPart, BPS <: Multipart.BodyPart.Strict](mediaRange: MediaRange,
defaultContentType: ContentType, defaultContentType: ContentType,
createBodyPart: (BodyPartEntity, List[HttpHeader]) BP, createBodyPart: (BodyPartEntity, List[HttpHeader]) BP,
createStreamed: (MultipartMediaType, Source[BP, Any]) T, createStreamed: (MediaType.Multipart, Source[BP, Any]) T,
createStrictBodyPart: (HttpEntity.Strict, List[HttpHeader]) BPS, 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 Unmarshaller { implicit ec
entity entity
if (entity.contentType.mediaType.isMultipart && mediaRange.matches(entity.contentType.mediaType)) { if (entity.contentType.mediaType.isMultipart && mediaRange.matches(entity.contentType.mediaType)) {
@ -68,7 +68,7 @@ trait MultipartUnmarshallers {
val parser = new BodyPartParser(defaultContentType, boundary, log) val parser = new BodyPartParser(defaultContentType, boundary, log)
FastFuture.successful { FastFuture.successful {
entity match { entity match {
case HttpEntity.Strict(ContentType(mediaType: MultipartMediaType, _), data) case HttpEntity.Strict(ContentType(mediaType: MediaType.Multipart, _), data)
val builder = new VectorBuilder[BPS]() val builder = new VectorBuilder[BPS]()
val iter = new IteratorInterpreter[ByteString, BodyPartParser.Output]( val iter = new IteratorInterpreter[ByteString, BodyPartParser.Output](
Iterator.single(data), List(parser)).iterator Iterator.single(data), List(parser)).iterator
@ -93,7 +93,7 @@ trait MultipartUnmarshallers {
case (BodyPartStart(headers, createEntity), entityParts) createBodyPart(createEntity(entityParts), headers) case (BodyPartStart(headers, createEntity), entityParts) createBodyPart(createEntity(entityParts), headers)
case (ParseError(errorInfo), _) throw ParsingException(errorInfo) case (ParseError(errorInfo), _) throw ParsingException(errorInfo)
} }
createStreamed(entity.contentType.mediaType.asInstanceOf[MultipartMediaType], bodyParts) createStreamed(entity.contentType.mediaType.asInstanceOf[MediaType.Multipart], bodyParts)
} }
} }
} }

View file

@ -21,31 +21,34 @@ trait PredefinedFromEntityUnmarshallers extends MultipartUnmarshallers {
implicit def charArrayUnmarshaller: FromEntityUnmarshaller[Array[Char]] = implicit def charArrayUnmarshaller: FromEntityUnmarshaller[Array[Char]] =
byteStringUnmarshaller mapWithInput { (entity, bytes) 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()) val array = new Array[Char](charBuffer.length())
charBuffer.get(array) charBuffer.get(array)
array array
} }
}
implicit def stringUnmarshaller: FromEntityUnmarshaller[String] = implicit def stringUnmarshaller: FromEntityUnmarshaller[String] =
byteStringUnmarshaller mapWithInput { (entity, bytes) byteStringUnmarshaller mapWithInput { (entity, bytes)
// FIXME: add `ByteString::decodeString(java.nio.Charset): String` overload!!! if (entity.isKnownEmpty) ""
bytes.decodeString(entity.contentType.charset.nioCharset.name) // ouch!!! else bytes.decodeString(Unmarshaller.bestUnmarshallingCharsetFor(entity).nioCharset.name)
} }
implicit def defaultUrlEncodedFormDataUnmarshaller: FromEntityUnmarshaller[FormData] = implicit def defaultUrlEncodedFormDataUnmarshaller: FromEntityUnmarshaller[FormData] =
urlEncodedFormDataUnmarshaller(MediaTypes.`application/x-www-form-urlencoded`) urlEncodedFormDataUnmarshaller(MediaTypes.`application/x-www-form-urlencoded`)
def urlEncodedFormDataUnmarshaller(ranges: ContentTypeRange*): FromEntityUnmarshaller[FormData] = def urlEncodedFormDataUnmarshaller(ranges: ContentTypeRange*): FromEntityUnmarshaller[FormData] =
stringUnmarshaller.forContentTypes(ranges: _*).mapWithInput { (entity, string) stringUnmarshaller.forContentTypes(ranges: _*).mapWithInput { (entity, string)
try { if (entity.isKnownEmpty) FormData.Empty
val nioCharset = entity.contentType.definedCharset.getOrElse(HttpCharsets.`UTF-8`).nioCharset else {
val query = Uri.Query(string, nioCharset) try FormData(Uri.Query(string, Unmarshaller.bestUnmarshallingCharsetFor(entity).nioCharset))
FormData(query) catch {
} catch {
case IllegalUriException(info) case IllegalUriException(info)
throw new IllegalArgumentException(info.formatPretty.replace("Query,", "form content,")) throw new IllegalArgumentException(info.formatPretty.replace("Query,", "form content,"))
} }
} }
} }
}
object PredefinedFromEntityUnmarshallers extends PredefinedFromEntityUnmarshallers object PredefinedFromEntityUnmarshallers extends PredefinedFromEntityUnmarshallers

View file

@ -90,7 +90,7 @@ object Unmarshaller
implicit class EnhancedFromEntityUnmarshaller[A](val underlying: FromEntityUnmarshaller[A]) extends AnyVal { implicit class EnhancedFromEntityUnmarshaller[A](val underlying: FromEntityUnmarshaller[A]) extends AnyVal {
def mapWithCharset[B](f: (A, HttpCharset) B): FromEntityUnmarshaller[B] = 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. * 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) 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. * Signals that unmarshalling failed because the entity was unexpectedly empty.
*/ */