From 3520d7e82794de82dfccde82ef0b24b0511a876a Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 1 Dec 2015 23:05:12 +0100 Subject: [PATCH 1/5] =htk fix potential test failure obfuscation --- .../scala/akka/http/scaladsl/testkit/RouteTest.scala | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/akka-http-testkit/src/main/scala/akka/http/scaladsl/testkit/RouteTest.scala b/akka-http-testkit/src/main/scala/akka/http/scaladsl/testkit/RouteTest.scala index 9cadec4b36..9be8ef5ae2 100644 --- a/akka-http-testkit/src/main/scala/akka/http/scaladsl/testkit/RouteTest.scala +++ b/akka-http-testkit/src/main/scala/akka/http/scaladsl/testkit/RouteTest.scala @@ -52,22 +52,24 @@ trait RouteTest extends RequestBuilding with WSTestRequestBuilding with RouteTes def check[T](body: ⇒ T): RouteTestResult ⇒ T = result ⇒ dynRR.withValue(result.awaitResult)(body) + private def responseSafe = if (dynRR.value ne null) dynRR.value.response else "" + def handled: Boolean = result.handled def response: HttpResponse = result.response def responseEntity: HttpEntity = result.entity def chunks: immutable.Seq[HttpEntity.ChunkStreamPart] = result.chunks def entityAs[T: FromEntityUnmarshaller: ClassTag](implicit timeout: Duration = 1.second): T = { - def msg(e: Throwable) = s"Could not unmarshal entity to type '${implicitly[ClassTag[T]]}' for `entityAs` assertion: $e\n\nResponse was: $response" + def msg(e: Throwable) = s"Could not unmarshal entity to type '${implicitly[ClassTag[T]]}' for `entityAs` assertion: $e\n\nResponse was: $responseSafe" Await.result(Unmarshal(responseEntity).to[T].fast.recover[T] { case error ⇒ failTest(msg(error)) }, timeout) } def responseAs[T: FromResponseUnmarshaller: ClassTag](implicit timeout: Duration = 1.second): T = { - def msg(e: Throwable) = s"Could not unmarshal response to type '${implicitly[ClassTag[T]]}' for `responseAs` assertion: $e\n\nResponse was: $response" + def msg(e: Throwable) = s"Could not unmarshal response to type '${implicitly[ClassTag[T]]}' for `responseAs` assertion: $e\n\nResponse was: $responseSafe" Await.result(Unmarshal(response).to[T].fast.recover[T] { case error ⇒ failTest(msg(error)) }, timeout) } def contentType: ContentType = responseEntity.contentType def mediaType: MediaType = contentType.mediaType - def charset: HttpCharset = contentType.charset - def definedCharset: Option[HttpCharset] = contentType.definedCharset + def charsetOption: Option[HttpCharset] = contentType.charsetOption + def charset: HttpCharset = charsetOption getOrElse sys.error("Binary entity does not have charset") def headers: immutable.Seq[HttpHeader] = response.headers def header[T <: HttpHeader: ClassTag]: Option[T] = response.header[T] def header(name: String): Option[HttpHeader] = response.headers.find(_.is(name.toLowerCase)) From e9240b7d86c176bf6b4638e4576f4b1c5339591a Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 1 Dec 2015 23:06:39 +0100 Subject: [PATCH 2/5] !htc refactor MediaType and ContentType model for better type-safety and cleanliness --- .../akka/http/javadsl/model/ContentType.java | 34 +- .../akka/http/javadsl/model/ContentTypes.java | 30 +- .../akka/http/javadsl/model/FormData.java | 2 +- .../http/javadsl/model/HttpCharsetRange.java | 4 - .../akka/http/javadsl/model/HttpEntities.java | 6 +- .../akka/http/javadsl/model/HttpEntity.java | 20 +- .../akka/http/javadsl/model/HttpMessage.java | 2 +- .../akka/http/javadsl/model/MediaType.java | 62 +- .../akka/http/javadsl/model/MediaTypes.java | 347 +++++---- .../http/impl/model/parser/AcceptHeader.scala | 2 +- .../impl/model/parser/CommonActions.scala | 18 +- .../impl/model/parser/ContentTypeHeader.scala | 10 +- .../http/impl/model/parser/LinkHeader.scala | 2 +- .../akka/http/impl/util/JavaMapping.scala | 8 + .../http/scaladsl/model/ContentType.scala | 107 +-- .../akka/http/scaladsl/model/FormData.scala | 4 +- .../http/scaladsl/model/HttpCharset.scala | 2 - .../akka/http/scaladsl/model/HttpEntity.scala | 2 +- .../http/scaladsl/model/HttpMessage.scala | 107 +-- .../akka/http/scaladsl/model/MediaRange.scala | 163 +++++ .../akka/http/scaladsl/model/MediaType.scala | 674 ++++++++---------- .../akka/http/scaladsl/model/Multipart.scala | 18 +- .../scaladsl/model/headers/HttpEncoding.scala | 5 +- .../engine/parsing/RequestParserSpec.scala | 2 +- .../rendering/RequestRendererSpec.scala | 23 +- .../rendering/ResponseRendererSpec.scala | 4 +- .../impl/engine/server/HttpServerSpec.scala | 19 +- .../impl/model/parser/HttpHeaderSpec.scala | 21 +- .../akka/http/scaladsl/ClientServerSpec.scala | 10 +- .../scala/akka/http/scaladsl/TestServer.scala | 2 +- .../http/HttpModelIntegrationSpec.scala | 4 +- 31 files changed, 895 insertions(+), 819 deletions(-) create mode 100644 akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaRange.scala diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/ContentType.java b/akka-http-core/src/main/java/akka/http/javadsl/model/ContentType.java index b6178c88d1..9d7d2cf34a 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/ContentType.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/ContentType.java @@ -9,33 +9,33 @@ import akka.japi.Option; /** * Represents an Http content-type. A content-type consists of a media-type and an optional charset. */ -public abstract class ContentType { - /** - * Returns the media-type of this content-type. - */ - public abstract MediaType mediaType(); +public interface ContentType { /** - * Returns the charset of this content-type. + * The media-type of this content-type. */ - public abstract HttpCharset charset(); + MediaType mediaType(); /** - * Returns the optionally defined charset of this content-type. + * True if this ContentType is non-textual. */ - public abstract Option getDefinedCharset(); + boolean binary(); /** - * Creates a content-type from a media-type and a charset. + * Returns the charset if this ContentType is non-binary. */ - public static ContentType create(MediaType mediaType, HttpCharset charset) { - return akka.http.scaladsl.model.ContentType.apply((akka.http.scaladsl.model.MediaType) mediaType, (akka.http.scaladsl.model.HttpCharset) charset); + Option getCharsetOption(); + + interface Binary extends ContentType { } - /** - * Creates a content-type from a media-type without specifying a charset. - */ - public static ContentType create(MediaType mediaType) { - return akka.http.scaladsl.model.ContentType.apply((akka.http.scaladsl.model.MediaType) mediaType); + interface NonBinary extends ContentType { + HttpCharset charset(); + } + + interface WithFixedCharset extends NonBinary { + } + + interface WithCharset extends NonBinary { } } diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/ContentTypes.java b/akka-http-core/src/main/java/akka/http/javadsl/model/ContentTypes.java index 3b29bcbb1d..2abd492741 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/ContentTypes.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/ContentTypes.java @@ -4,6 +4,8 @@ package akka.http.javadsl.model; +import akka.http.scaladsl.model.ContentType$; + /** * Contains the set of predefined content-types for convenience. *

@@ -11,14 +13,26 @@ package akka.http.javadsl.model; * you can obtain it from a {@link MediaType} by using: {@code MediaTypes.TEXT_HTML.toContentType()} */ public final class ContentTypes { - public static final ContentType APPLICATION_JSON = MediaTypes.APPLICATION_JSON.toContentType(); - public static final ContentType APPLICATION_OCTET_STREAM = MediaTypes.APPLICATION_OCTET_STREAM.toContentType(); + public static final ContentType.WithFixedCharset APPLICATION_JSON = MediaTypes.APPLICATION_JSON.toContentType(); + public static final ContentType.Binary APPLICATION_OCTET_STREAM = MediaTypes.APPLICATION_OCTET_STREAM.toContentType(); - public static final ContentType TEXT_PLAIN = MediaTypes.TEXT_PLAIN.toContentType(); - public static final ContentType TEXT_PLAIN_UTF8 = akka.http.scaladsl.model.ContentTypes.text$divplain$u0028UTF$minus8$u0029(); - public static final ContentType TEXT_HTML = MediaTypes.TEXT_HTML.toContentType(); - public static final ContentType TEXT_XML = MediaTypes.TEXT_XML.toContentType(); + public static final ContentType.WithCharset TEXT_PLAIN_UTF8 = + akka.http.scaladsl.model.ContentTypes.text$divplain$u0028UTF$minus8$u0029(); + public static final ContentType.WithCharset TEXT_HTML_UTF8 = + akka.http.scaladsl.model.ContentTypes.text$divhtml$u0028UTF$minus8$u0029(); + public static final ContentType.WithCharset TEXT_XML_UTF8 = + akka.http.scaladsl.model.ContentTypes.text$divxml$u0028UTF$minus8$u0029(); - public static final ContentType APPLICATION_X_WWW_FORM_URLENCODED = MediaTypes.APPLICATION_X_WWW_FORM_URLENCODED.toContentType(); - public static final ContentType MULTIPART_FORM_DATA = MediaTypes.MULTIPART_FORM_DATA.toContentType(); + public static ContentType.Binary create(MediaType.Binary mediaType) { + return ContentType$.MODULE$.apply((akka.http.scaladsl.model.MediaType.Binary) mediaType); + } + + public static ContentType.WithFixedCharset create(MediaType.WithFixedCharset mediaType) { + return ContentType$.MODULE$.apply((akka.http.scaladsl.model.MediaType.WithFixedCharset) mediaType); + } + + public static ContentType.WithCharset create(MediaType.WithOpenCharset mediaType, HttpCharset charset) { + return ContentType$.MODULE$.apply((akka.http.scaladsl.model.MediaType.WithOpenCharset) mediaType, + (akka.http.scaladsl.model.HttpCharset) charset); + } } diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/FormData.java b/akka-http-core/src/main/java/akka/http/javadsl/model/FormData.java index 1221b631d1..46c0b22843 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/FormData.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/FormData.java @@ -29,7 +29,7 @@ public final class FormData { * Converts this FormData to a RequestEntity using the given encoding. */ public RequestEntity toEntity(HttpCharset charset) { - return HttpEntities.create(ContentType.create(MediaTypes.APPLICATION_X_WWW_FORM_URLENCODED, charset), fields.render(charset)); + return HttpEntities.create(ContentTypes.create(MediaTypes.APPLICATION_X_WWW_FORM_URLENCODED, charset), fields.render(charset)); } /** diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpCharsetRange.java b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpCharsetRange.java index 6dd777c9b5..2691d739cf 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpCharsetRange.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpCharsetRange.java @@ -9,10 +9,6 @@ package akka.http.javadsl.model; * charset. {@link HttpCharsetRanges} contains static constructors for HttpCharsetRanges. */ public abstract class HttpCharsetRange { - /** - * Returns if this range matches all charsets. - */ - public abstract boolean matchesAll(); /** * The qValue for this range. diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpEntities.java b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpEntities.java index 13ef29a3ec..9644b8a4a0 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpEntities.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpEntities.java @@ -28,8 +28,8 @@ public final class HttpEntities { return HttpEntity$.MODULE$.apply(bytes); } - public static HttpEntityStrict create(ContentType contentType, String string) { - return HttpEntity$.MODULE$.apply((akka.http.scaladsl.model.ContentType) contentType, string); + public static HttpEntityStrict create(ContentType.NonBinary contentType, String string) { + return HttpEntity$.MODULE$.apply((akka.http.scaladsl.model.ContentType.NonBinary) contentType, string); } public static HttpEntityStrict create(ContentType contentType, byte[] bytes) { @@ -52,7 +52,7 @@ public final class HttpEntities { return new akka.http.scaladsl.model.HttpEntity.Default((akka.http.scaladsl.model.ContentType) contentType, contentLength, data.asScala()); } - public static HttpEntity.Chunked create(ContentType contentType, Source data) { + public static HttpEntityChunked create(ContentType contentType, Source data) { return HttpEntity.Chunked$.MODULE$.fromData((akka.http.scaladsl.model.ContentType) contentType, data.asScala()); } diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpEntity.java b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpEntity.java index 9c2627afac..e208e8de84 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpEntity.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpEntity.java @@ -40,48 +40,48 @@ public interface HttpEntity { /** * Returns the content-type of this entity */ - public abstract ContentType contentType(); + ContentType contentType(); /** * The empty entity. */ - public static final HttpEntityStrict EMPTY = HttpEntity$.MODULE$.Empty(); + HttpEntityStrict EMPTY = HttpEntity$.MODULE$.Empty(); /** * Returns if this entity is known to be empty. Open-ended entity types like * HttpEntityChunked and HttpCloseDelimited will always return false here. */ - public abstract boolean isKnownEmpty(); + boolean isKnownEmpty(); /** * Returns if this entity is a subtype of HttpEntityChunked. */ - public abstract boolean isChunked(); + boolean isChunked(); /** * Returns if this entity is a subtype of HttpEntityDefault. */ - public abstract boolean isDefault(); + boolean isDefault(); /** * Returns if this entity is a subtype of HttpEntityCloseDelimited. */ - public abstract boolean isCloseDelimited(); + boolean isCloseDelimited(); /** * Returns if this entity is a subtype of HttpEntityIndefiniteLength. */ - public abstract boolean isIndefiniteLength(); + boolean isIndefiniteLength(); /** * Returns Some(contentLength) if the length is defined and none otherwise. */ - public abstract Option getContentLengthOption(); + Option getContentLengthOption(); /** * Returns a stream of data bytes this entity consists of. */ - public abstract Source getDataBytes(); + Source getDataBytes(); /** * Returns a future of a strict entity that contains the same data as this entity @@ -92,5 +92,5 @@ public interface HttpEntity { * Use getDataBytes and stream processing instead if the expected data is big or * is likely to take a long time. */ - public abstract Future toStrict(long timeoutMillis, Materializer materializer); + Future toStrict(long timeoutMillis, Materializer materializer); } diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpMessage.java b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpMessage.java index 6493b7bad4..61f64c25bb 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/HttpMessage.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/HttpMessage.java @@ -89,7 +89,7 @@ public interface HttpMessage { /** * Returns a copy of Self message with a new entity. */ - Self withEntity(ContentType type, String string); + Self withEntity(ContentType.NonBinary type, String string); /** * Returns a copy of Self message with a new entity. diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/MediaType.java b/akka-http-core/src/main/java/akka/http/javadsl/model/MediaType.java index 25a6e0aba1..8f330fce21 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/MediaType.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/MediaType.java @@ -7,33 +7,61 @@ package akka.http.javadsl.model; /** * Represents an Http media-type. A media-type consists of a main-type and a sub-type. */ -public abstract class MediaType { - /** - * Returns the main-type of this media-type. - */ - public abstract String mainType(); +public interface MediaType { /** - * Returns the sub-type of this media-type. + * The main-type of this media-type. */ - public abstract String subType(); + String mainType(); + + /** + * The sub-type of this media-type. + */ + String subType(); + + /** + * True when this media-type is generally compressible. + */ + boolean compressible(); + + /** + * True when this media-type is not character-based. + */ + boolean binary(); + + boolean isApplication(); + boolean isAudio(); + boolean isImage(); + boolean isMessage(); + boolean isMultipart(); + boolean isText(); + boolean isVideo(); /** * Creates a media-range from this media-type. */ - public MediaRange toRange() { - return MediaRanges.create(this); - } - - /** - * Creates a ContentType from this media-type - */ - public ContentType toContentType() { return ContentType.create(this); } + MediaRange toRange(); /** * Creates a media-range from this media-type with a given qValue. */ - public MediaRange toRange(float qValue) { - return MediaRanges.create(this, qValue); + MediaRange toRange(float qValue); + + interface Binary extends MediaType { + ContentType.Binary toContentType(); + } + + interface NonBinary extends MediaType { + } + + interface WithFixedCharset extends NonBinary { + ContentType.WithFixedCharset toContentType(); + } + + interface WithOpenCharset extends NonBinary { + ContentType.WithCharset toContentType(HttpCharset charset); + } + + interface Multipart extends WithOpenCharset { } } diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/MediaTypes.java b/akka-http-core/src/main/java/akka/http/javadsl/model/MediaTypes.java index f1d5caefd3..c7daac4c5a 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/MediaTypes.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/MediaTypes.java @@ -7,189 +7,188 @@ package akka.http.javadsl.model; import akka.http.impl.util.Util; import akka.http.scaladsl.model.MediaTypes$; import akka.japi.Option; - -import java.util.Map; +import scala.collection.immutable.List; /** * Contains the set of predefined media-types. */ public abstract class MediaTypes { - public static final MediaType APPLICATION_ATOM_XML = akka.http.scaladsl.model.MediaTypes.application$divatom$plusxml(); - public static final MediaType APPLICATION_BASE64 = akka.http.scaladsl.model.MediaTypes.application$divbase64(); - public static final MediaType APPLICATION_EXCEL = akka.http.scaladsl.model.MediaTypes.application$divexcel(); - public static final MediaType APPLICATION_FONT_WOFF = akka.http.scaladsl.model.MediaTypes.application$divfont$minuswoff(); - public static final MediaType APPLICATION_GNUTAR = akka.http.scaladsl.model.MediaTypes.application$divgnutar(); - public static final MediaType APPLICATION_JAVA_ARCHIVE = akka.http.scaladsl.model.MediaTypes.application$divjava$minusarchive(); - public static final MediaType APPLICATION_JAVASCRIPT = akka.http.scaladsl.model.MediaTypes.application$divjavascript(); - public static final MediaType APPLICATION_JSON = akka.http.scaladsl.model.MediaTypes.application$divjson(); - public static final MediaType APPLICATION_JSON_PATCH_JSON = akka.http.scaladsl.model.MediaTypes.application$divjson$minuspatch$plusjson(); - public static final MediaType APPLICATION_LHA = akka.http.scaladsl.model.MediaTypes.application$divlha(); - public static final MediaType APPLICATION_LZX = akka.http.scaladsl.model.MediaTypes.application$divlzx(); - public static final MediaType APPLICATION_MSPOWERPOINT = akka.http.scaladsl.model.MediaTypes.application$divmspowerpoint(); - public static final MediaType APPLICATION_MSWORD = akka.http.scaladsl.model.MediaTypes.application$divmsword(); - public static final MediaType APPLICATION_OCTET_STREAM = akka.http.scaladsl.model.MediaTypes.application$divoctet$minusstream(); - public static final MediaType APPLICATION_PDF = akka.http.scaladsl.model.MediaTypes.application$divpdf(); - public static final MediaType APPLICATION_POSTSCRIPT = akka.http.scaladsl.model.MediaTypes.application$divpostscript(); - public static final MediaType APPLICATION_RSS_XML = akka.http.scaladsl.model.MediaTypes.application$divrss$plusxml(); - public static final MediaType APPLICATION_SOAP_XML = akka.http.scaladsl.model.MediaTypes.application$divsoap$plusxml(); - public static final MediaType APPLICATION_VND_API_JSON = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eapi$plusjson(); - public static final MediaType APPLICATION_VND_GOOGLE_EARTH_KML_XML = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Egoogle$minusearth$u002Ekml$plusxml(); - public static final MediaType APPLICATION_VND_GOOGLE_EARTH_KMZ = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Egoogle$minusearth$u002Ekmz(); - public static final MediaType APPLICATION_VND_MS_FONTOBJECT = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Ems$minusfontobject(); - public static final MediaType APPLICATION_VND_OASIS_OPENDOCUMENT_CHART = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Echart(); - public static final MediaType APPLICATION_VND_OASIS_OPENDOCUMENT_DATABASE = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Edatabase(); - public static final MediaType APPLICATION_VND_OASIS_OPENDOCUMENT_FORMULA = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Eformula(); - public static final MediaType APPLICATION_VND_OASIS_OPENDOCUMENT_GRAPHICS = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Egraphics(); - public static final MediaType APPLICATION_VND_OASIS_OPENDOCUMENT_IMAGE = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Eimage(); - public static final MediaType APPLICATION_VND_OASIS_OPENDOCUMENT_PRESENTATION = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Epresentation(); - public static final MediaType APPLICATION_VND_OASIS_OPENDOCUMENT_SPREADSHEET = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Espreadsheet(); - public static final MediaType APPLICATION_VND_OASIS_OPENDOCUMENT_TEXT = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Etext(); - public static final MediaType APPLICATION_VND_OASIS_OPENDOCUMENT_TEXT_MASTER = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Etext$minusmaster(); - public static final MediaType APPLICATION_VND_OASIS_OPENDOCUMENT_TEXT_WEB = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Etext$minusweb(); - public static final MediaType APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_PRESENTATIONML_PRESENTATION = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Epresentation(); - public static final MediaType APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_PRESENTATIONML_SLIDE = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Eslide(); - public static final MediaType APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_PRESENTATIONML_SLIDESHOW = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Eslideshow(); - public static final MediaType APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_PRESENTATIONML_TEMPLATE = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Etemplate(); - public static final MediaType APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_SPREADSHEETML_SHEET = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Esheet(); - public static final MediaType APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_SPREADSHEETML_TEMPLATE = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Etemplate(); - public static final MediaType APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_WORDPROCESSINGML_DOCUMENT = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Edocument(); - public static final MediaType APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_WORDPROCESSINGML_TEMPLATE = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Etemplate(); - public static final MediaType APPLICATION_X_7Z_COMPRESSED = akka.http.scaladsl.model.MediaTypes.application$divx$minus7z$minuscompressed(); - public static final MediaType APPLICATION_X_ACE_COMPRESSED = akka.http.scaladsl.model.MediaTypes.application$divx$minusace$minuscompressed(); - public static final MediaType APPLICATION_X_APPLE_DISKIMAGE = akka.http.scaladsl.model.MediaTypes.application$divx$minusapple$minusdiskimage(); - public static final MediaType APPLICATION_X_ARC_COMPRESSED = akka.http.scaladsl.model.MediaTypes.application$divx$minusarc$minuscompressed(); - public static final MediaType APPLICATION_X_BZIP = akka.http.scaladsl.model.MediaTypes.application$divx$minusbzip(); - public static final MediaType APPLICATION_X_BZIP2 = akka.http.scaladsl.model.MediaTypes.application$divx$minusbzip2(); - public static final MediaType APPLICATION_X_CHROME_EXTENSION = akka.http.scaladsl.model.MediaTypes.application$divx$minuschrome$minusextension(); - public static final MediaType APPLICATION_X_COMPRESS = akka.http.scaladsl.model.MediaTypes.application$divx$minuscompress(); - public static final MediaType APPLICATION_X_COMPRESSED = akka.http.scaladsl.model.MediaTypes.application$divx$minuscompressed(); - public static final MediaType APPLICATION_X_DEBIAN_PACKAGE = akka.http.scaladsl.model.MediaTypes.application$divx$minusdebian$minuspackage(); - public static final MediaType APPLICATION_X_DVI = akka.http.scaladsl.model.MediaTypes.application$divx$minusdvi(); - public static final MediaType APPLICATION_X_FONT_TRUETYPE = akka.http.scaladsl.model.MediaTypes.application$divx$minusfont$minustruetype(); - public static final MediaType APPLICATION_X_FONT_OPENTYPE = akka.http.scaladsl.model.MediaTypes.application$divx$minusfont$minusopentype(); - public static final MediaType APPLICATION_X_GTAR = akka.http.scaladsl.model.MediaTypes.application$divx$minusgtar(); - public static final MediaType APPLICATION_X_GZIP = akka.http.scaladsl.model.MediaTypes.application$divx$minusgzip(); - public static final MediaType APPLICATION_X_LATEX = akka.http.scaladsl.model.MediaTypes.application$divx$minuslatex(); - public static final MediaType APPLICATION_X_RAR_COMPRESSED = akka.http.scaladsl.model.MediaTypes.application$divx$minusrar$minuscompressed(); - public static final MediaType APPLICATION_X_REDHAT_PACKAGE_MANAGER = akka.http.scaladsl.model.MediaTypes.application$divx$minusredhat$minuspackage$minusmanager(); - public static final MediaType APPLICATION_X_SHOCKWAVE_FLASH = akka.http.scaladsl.model.MediaTypes.application$divx$minusshockwave$minusflash(); - public static final MediaType APPLICATION_X_TAR = akka.http.scaladsl.model.MediaTypes.application$divx$minustar(); - public static final MediaType APPLICATION_X_TEX = akka.http.scaladsl.model.MediaTypes.application$divx$minustex(); - public static final MediaType APPLICATION_X_TEXINFO = akka.http.scaladsl.model.MediaTypes.application$divx$minustexinfo(); - public static final MediaType APPLICATION_X_VRML = akka.http.scaladsl.model.MediaTypes.application$divx$minusvrml(); - public static final MediaType APPLICATION_X_WWW_FORM_URLENCODED = akka.http.scaladsl.model.MediaTypes.application$divx$minuswww$minusform$minusurlencoded(); - public static final MediaType APPLICATION_X_X509_CA_CERT = akka.http.scaladsl.model.MediaTypes.application$divx$minusx509$minusca$minuscert(); - public static final MediaType APPLICATION_X_XPINSTALL = akka.http.scaladsl.model.MediaTypes.application$divx$minusxpinstall(); - public static final MediaType APPLICATION_XHTML_XML = akka.http.scaladsl.model.MediaTypes.application$divxhtml$plusxml(); - public static final MediaType APPLICATION_XML_DTD = akka.http.scaladsl.model.MediaTypes.application$divxml$minusdtd(); - public static final MediaType APPLICATION_XML = akka.http.scaladsl.model.MediaTypes.application$divxml(); - public static final MediaType APPLICATION_ZIP = akka.http.scaladsl.model.MediaTypes.application$divzip(); - public static final MediaType AUDIO_AIFF = akka.http.scaladsl.model.MediaTypes.audio$divaiff(); - public static final MediaType AUDIO_BASIC = akka.http.scaladsl.model.MediaTypes.audio$divbasic(); - public static final MediaType AUDIO_MIDI = akka.http.scaladsl.model.MediaTypes.audio$divmidi(); - public static final MediaType AUDIO_MOD = akka.http.scaladsl.model.MediaTypes.audio$divmod(); - public static final MediaType AUDIO_MPEG = akka.http.scaladsl.model.MediaTypes.audio$divmpeg(); - public static final MediaType AUDIO_OGG = akka.http.scaladsl.model.MediaTypes.audio$divogg(); - public static final MediaType AUDIO_VOC = akka.http.scaladsl.model.MediaTypes.audio$divvoc(); - public static final MediaType AUDIO_VORBIS = akka.http.scaladsl.model.MediaTypes.audio$divvorbis(); - public static final MediaType AUDIO_VOXWARE = akka.http.scaladsl.model.MediaTypes.audio$divvoxware(); - public static final MediaType AUDIO_WAV = akka.http.scaladsl.model.MediaTypes.audio$divwav(); - public static final MediaType AUDIO_X_REALAUDIO = akka.http.scaladsl.model.MediaTypes.audio$divx$minusrealaudio(); - public static final MediaType AUDIO_X_PSID = akka.http.scaladsl.model.MediaTypes.audio$divx$minuspsid(); - public static final MediaType AUDIO_XM = akka.http.scaladsl.model.MediaTypes.audio$divxm(); - public static final MediaType AUDIO_WEBM = akka.http.scaladsl.model.MediaTypes.audio$divwebm(); - public static final MediaType IMAGE_GIF = akka.http.scaladsl.model.MediaTypes.image$divgif(); - public static final MediaType IMAGE_JPEG = akka.http.scaladsl.model.MediaTypes.image$divjpeg(); - public static final MediaType IMAGE_PICT = akka.http.scaladsl.model.MediaTypes.image$divpict(); - public static final MediaType IMAGE_PNG = akka.http.scaladsl.model.MediaTypes.image$divpng(); - public static final MediaType IMAGE_SVG_XML = akka.http.scaladsl.model.MediaTypes.image$divsvg$plusxml(); - public static final MediaType IMAGE_TIFF = akka.http.scaladsl.model.MediaTypes.image$divtiff(); - public static final MediaType IMAGE_X_ICON = akka.http.scaladsl.model.MediaTypes.image$divx$minusicon(); - public static final MediaType IMAGE_X_MS_BMP = akka.http.scaladsl.model.MediaTypes.image$divx$minusms$minusbmp(); - public static final MediaType IMAGE_X_PCX = akka.http.scaladsl.model.MediaTypes.image$divx$minuspcx(); - public static final MediaType IMAGE_X_PICT = akka.http.scaladsl.model.MediaTypes.image$divx$minuspict(); - public static final MediaType IMAGE_X_QUICKTIME = akka.http.scaladsl.model.MediaTypes.image$divx$minusquicktime(); - public static final MediaType IMAGE_X_RGB = akka.http.scaladsl.model.MediaTypes.image$divx$minusrgb(); - public static final MediaType IMAGE_X_XBITMAP = akka.http.scaladsl.model.MediaTypes.image$divx$minusxbitmap(); - public static final MediaType IMAGE_X_XPIXMAP = akka.http.scaladsl.model.MediaTypes.image$divx$minusxpixmap(); - public static final MediaType IMAGE_WEBP = akka.http.scaladsl.model.MediaTypes.image$divwebp(); - public static final MediaType MESSAGE_HTTP = akka.http.scaladsl.model.MediaTypes.message$divhttp(); - public static final MediaType MESSAGE_DELIVERY_STATUS = akka.http.scaladsl.model.MediaTypes.message$divdelivery$minusstatus(); - public static final MediaType MESSAGE_RFC822 = akka.http.scaladsl.model.MediaTypes.message$divrfc822(); - public static final MediaType MULTIPART_MIXED = akka.http.scaladsl.model.MediaTypes.multipart$divmixed(); - public static final MediaType MULTIPART_ALTERNATIVE = akka.http.scaladsl.model.MediaTypes.multipart$divalternative(); - public static final MediaType MULTIPART_RELATED = akka.http.scaladsl.model.MediaTypes.multipart$divrelated(); - public static final MediaType MULTIPART_FORM_DATA = akka.http.scaladsl.model.MediaTypes.multipart$divform$minusdata(); - public static final MediaType MULTIPART_SIGNED = akka.http.scaladsl.model.MediaTypes.multipart$divsigned(); - public static final MediaType MULTIPART_ENCRYPTED = akka.http.scaladsl.model.MediaTypes.multipart$divencrypted(); - public static final MediaType MULTIPART_BYTERANGES = akka.http.scaladsl.model.MediaTypes.multipart$divbyteranges(); - public static final MediaType TEXT_ASP = akka.http.scaladsl.model.MediaTypes.text$divasp(); - public static final MediaType TEXT_CACHE_MANIFEST = akka.http.scaladsl.model.MediaTypes.text$divcache$minusmanifest(); - public static final MediaType TEXT_CALENDAR = akka.http.scaladsl.model.MediaTypes.text$divcalendar(); - public static final MediaType TEXT_CSS = akka.http.scaladsl.model.MediaTypes.text$divcss(); - public static final MediaType TEXT_CSV = akka.http.scaladsl.model.MediaTypes.text$divcsv(); - public static final MediaType TEXT_HTML = akka.http.scaladsl.model.MediaTypes.text$divhtml(); - public static final MediaType TEXT_MCF = akka.http.scaladsl.model.MediaTypes.text$divmcf(); - public static final MediaType TEXT_PLAIN = akka.http.scaladsl.model.MediaTypes.text$divplain(); - public static final MediaType TEXT_RICHTEXT = akka.http.scaladsl.model.MediaTypes.text$divrichtext(); - public static final MediaType TEXT_TAB_SEPARATED_VALUES = akka.http.scaladsl.model.MediaTypes.text$divtab$minusseparated$minusvalues(); - public static final MediaType TEXT_URI_LIST = akka.http.scaladsl.model.MediaTypes.text$divuri$minuslist(); - public static final MediaType TEXT_VND_WAP_WML = akka.http.scaladsl.model.MediaTypes.text$divvnd$u002Ewap$u002Ewml(); - public static final MediaType TEXT_VND_WAP_WMLSCRIPT = akka.http.scaladsl.model.MediaTypes.text$divvnd$u002Ewap$u002Ewmlscript(); - public static final MediaType TEXT_X_ASM = akka.http.scaladsl.model.MediaTypes.text$divx$minusasm(); - public static final MediaType TEXT_X_C = akka.http.scaladsl.model.MediaTypes.text$divx$minusc(); - public static final MediaType TEXT_X_COMPONENT = akka.http.scaladsl.model.MediaTypes.text$divx$minuscomponent(); - public static final MediaType TEXT_X_H = akka.http.scaladsl.model.MediaTypes.text$divx$minush(); - public static final MediaType TEXT_X_JAVA_SOURCE = akka.http.scaladsl.model.MediaTypes.text$divx$minusjava$minussource(); - public static final MediaType TEXT_X_PASCAL = akka.http.scaladsl.model.MediaTypes.text$divx$minuspascal(); - public static final MediaType TEXT_X_SCRIPT = akka.http.scaladsl.model.MediaTypes.text$divx$minusscript(); - public static final MediaType TEXT_X_SCRIPTCSH = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptcsh(); - public static final MediaType TEXT_X_SCRIPTELISP = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptelisp(); - public static final MediaType TEXT_X_SCRIPTKSH = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptksh(); - public static final MediaType TEXT_X_SCRIPTLISP = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptlisp(); - public static final MediaType TEXT_X_SCRIPTPERL = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptperl(); - public static final MediaType TEXT_X_SCRIPTPERL_MODULE = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptperl$minusmodule(); - public static final MediaType TEXT_X_SCRIPTPHYTON = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptphyton(); - public static final MediaType TEXT_X_SCRIPTREXX = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptrexx(); - public static final MediaType TEXT_X_SCRIPTSCHEME = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptscheme(); - public static final MediaType TEXT_X_SCRIPTSH = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptsh(); - public static final MediaType TEXT_X_SCRIPTTCL = akka.http.scaladsl.model.MediaTypes.text$divx$minusscripttcl(); - public static final MediaType TEXT_X_SCRIPTTCSH = akka.http.scaladsl.model.MediaTypes.text$divx$minusscripttcsh(); - public static final MediaType TEXT_X_SCRIPTZSH = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptzsh(); - public static final MediaType TEXT_X_SERVER_PARSED_HTML = akka.http.scaladsl.model.MediaTypes.text$divx$minusserver$minusparsed$minushtml(); - public static final MediaType TEXT_X_SETEXT = akka.http.scaladsl.model.MediaTypes.text$divx$minussetext(); - public static final MediaType TEXT_X_SGML = akka.http.scaladsl.model.MediaTypes.text$divx$minussgml(); - public static final MediaType TEXT_X_SPEECH = akka.http.scaladsl.model.MediaTypes.text$divx$minusspeech(); - public static final MediaType TEXT_X_UUENCODE = akka.http.scaladsl.model.MediaTypes.text$divx$minusuuencode(); - public static final MediaType TEXT_X_VCALENDAR = akka.http.scaladsl.model.MediaTypes.text$divx$minusvcalendar(); - public static final MediaType TEXT_X_VCARD = akka.http.scaladsl.model.MediaTypes.text$divx$minusvcard(); - public static final MediaType TEXT_XML = akka.http.scaladsl.model.MediaTypes.text$divxml(); - public static final MediaType VIDEO_AVS_VIDEO = akka.http.scaladsl.model.MediaTypes.video$divavs$minusvideo(); - public static final MediaType VIDEO_DIVX = akka.http.scaladsl.model.MediaTypes.video$divdivx(); - public static final MediaType VIDEO_GL = akka.http.scaladsl.model.MediaTypes.video$divgl(); - public static final MediaType VIDEO_MP4 = akka.http.scaladsl.model.MediaTypes.video$divmp4(); - public static final MediaType VIDEO_MPEG = akka.http.scaladsl.model.MediaTypes.video$divmpeg(); - public static final MediaType VIDEO_OGG = akka.http.scaladsl.model.MediaTypes.video$divogg(); - public static final MediaType VIDEO_QUICKTIME = akka.http.scaladsl.model.MediaTypes.video$divquicktime(); - public static final MediaType VIDEO_X_DV = akka.http.scaladsl.model.MediaTypes.video$divx$minusdv(); - public static final MediaType VIDEO_X_FLV = akka.http.scaladsl.model.MediaTypes.video$divx$minusflv(); - public static final MediaType VIDEO_X_MOTION_JPEG = akka.http.scaladsl.model.MediaTypes.video$divx$minusmotion$minusjpeg(); - public static final MediaType VIDEO_X_MS_ASF = akka.http.scaladsl.model.MediaTypes.video$divx$minusms$minusasf(); - public static final MediaType VIDEO_X_MSVIDEO = akka.http.scaladsl.model.MediaTypes.video$divx$minusmsvideo(); - public static final MediaType VIDEO_X_SGI_MOVIE = akka.http.scaladsl.model.MediaTypes.video$divx$minussgi$minusmovie(); - public static final MediaType VIDEO_WEBM = akka.http.scaladsl.model.MediaTypes.video$divwebm(); + public static final MediaType.WithOpenCharset APPLICATION_ATOM_XML = akka.http.scaladsl.model.MediaTypes.application$divatom$plusxml(); + public static final MediaType.WithOpenCharset APPLICATION_BASE64 = akka.http.scaladsl.model.MediaTypes.application$divbase64(); + public static final MediaType.Binary APPLICATION_EXCEL = akka.http.scaladsl.model.MediaTypes.application$divexcel(); + public static final MediaType.Binary APPLICATION_FONT_WOFF = akka.http.scaladsl.model.MediaTypes.application$divfont$minuswoff(); + public static final MediaType.Binary APPLICATION_GNUTAR = akka.http.scaladsl.model.MediaTypes.application$divgnutar(); + public static final MediaType.Binary APPLICATION_JAVA_ARCHIVE = akka.http.scaladsl.model.MediaTypes.application$divjava$minusarchive(); + public static final MediaType.WithOpenCharset APPLICATION_JAVASCRIPT = akka.http.scaladsl.model.MediaTypes.application$divjavascript(); + public static final MediaType.WithFixedCharset APPLICATION_JSON = akka.http.scaladsl.model.MediaTypes.application$divjson(); + public static final MediaType.WithFixedCharset APPLICATION_JSON_PATCH_JSON = akka.http.scaladsl.model.MediaTypes.application$divjson$minuspatch$plusjson(); + public static final MediaType.Binary APPLICATION_LHA = akka.http.scaladsl.model.MediaTypes.application$divlha(); + public static final MediaType.Binary APPLICATION_LZX = akka.http.scaladsl.model.MediaTypes.application$divlzx(); + public static final MediaType.Binary APPLICATION_MSPOWERPOINT = akka.http.scaladsl.model.MediaTypes.application$divmspowerpoint(); + public static final MediaType.Binary APPLICATION_MSWORD = akka.http.scaladsl.model.MediaTypes.application$divmsword(); + public static final MediaType.Binary APPLICATION_OCTET_STREAM = akka.http.scaladsl.model.MediaTypes.application$divoctet$minusstream(); + public static final MediaType.Binary APPLICATION_PDF = akka.http.scaladsl.model.MediaTypes.application$divpdf(); + public static final MediaType.Binary APPLICATION_POSTSCRIPT = akka.http.scaladsl.model.MediaTypes.application$divpostscript(); + public static final MediaType.WithOpenCharset APPLICATION_RSS_XML = akka.http.scaladsl.model.MediaTypes.application$divrss$plusxml(); + public static final MediaType.WithOpenCharset APPLICATION_SOAP_XML = akka.http.scaladsl.model.MediaTypes.application$divsoap$plusxml(); + public static final MediaType.WithFixedCharset APPLICATION_VND_API_JSON = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eapi$plusjson(); + public static final MediaType.WithOpenCharset APPLICATION_VND_GOOGLE_EARTH_KML_XML = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Egoogle$minusearth$u002Ekml$plusxml(); + public static final MediaType.Binary APPLICATION_VND_GOOGLE_EARTH_KMZ = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Egoogle$minusearth$u002Ekmz(); + public static final MediaType.Binary APPLICATION_VND_MS_FONTOBJECT = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Ems$minusfontobject(); + public static final MediaType.Binary APPLICATION_VND_OASIS_OPENDOCUMENT_CHART = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Echart(); + public static final MediaType.Binary APPLICATION_VND_OASIS_OPENDOCUMENT_DATABASE = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Edatabase(); + public static final MediaType.Binary APPLICATION_VND_OASIS_OPENDOCUMENT_FORMULA = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Eformula(); + public static final MediaType.Binary APPLICATION_VND_OASIS_OPENDOCUMENT_GRAPHICS = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Egraphics(); + public static final MediaType.Binary APPLICATION_VND_OASIS_OPENDOCUMENT_IMAGE = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Eimage(); + public static final MediaType.Binary APPLICATION_VND_OASIS_OPENDOCUMENT_PRESENTATION = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Epresentation(); + public static final MediaType.Binary APPLICATION_VND_OASIS_OPENDOCUMENT_SPREADSHEET = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Espreadsheet(); + public static final MediaType.Binary APPLICATION_VND_OASIS_OPENDOCUMENT_TEXT = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Etext(); + public static final MediaType.Binary APPLICATION_VND_OASIS_OPENDOCUMENT_TEXT_MASTER = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Etext$minusmaster(); + public static final MediaType.Binary APPLICATION_VND_OASIS_OPENDOCUMENT_TEXT_WEB = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eoasis$u002Eopendocument$u002Etext$minusweb(); + public static final MediaType.Binary APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_PRESENTATIONML_PRESENTATION = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Epresentation(); + public static final MediaType.Binary APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_PRESENTATIONML_SLIDE = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Eslide(); + public static final MediaType.Binary APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_PRESENTATIONML_SLIDESHOW = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Eslideshow(); + public static final MediaType.Binary APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_PRESENTATIONML_TEMPLATE = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Epresentationml$u002Etemplate(); + public static final MediaType.Binary APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_SPREADSHEETML_SHEET = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Esheet(); + public static final MediaType.Binary APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_SPREADSHEETML_TEMPLATE = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Espreadsheetml$u002Etemplate(); + public static final MediaType.Binary APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_WORDPROCESSINGML_DOCUMENT = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Edocument(); + public static final MediaType.Binary APPLICATION_VND_OPENXMLFORMATS_OFFICEDOCUMENT_WORDPROCESSINGML_TEMPLATE = akka.http.scaladsl.model.MediaTypes.application$divvnd$u002Eopenxmlformats$minusofficedocument$u002Ewordprocessingml$u002Etemplate(); + public static final MediaType.Binary APPLICATION_X_7Z_COMPRESSED = akka.http.scaladsl.model.MediaTypes.application$divx$minus7z$minuscompressed(); + public static final MediaType.Binary APPLICATION_X_ACE_COMPRESSED = akka.http.scaladsl.model.MediaTypes.application$divx$minusace$minuscompressed(); + public static final MediaType.Binary APPLICATION_X_APPLE_DISKIMAGE = akka.http.scaladsl.model.MediaTypes.application$divx$minusapple$minusdiskimage(); + public static final MediaType.Binary APPLICATION_X_ARC_COMPRESSED = akka.http.scaladsl.model.MediaTypes.application$divx$minusarc$minuscompressed(); + public static final MediaType.Binary APPLICATION_X_BZIP = akka.http.scaladsl.model.MediaTypes.application$divx$minusbzip(); + public static final MediaType.Binary APPLICATION_X_BZIP2 = akka.http.scaladsl.model.MediaTypes.application$divx$minusbzip2(); + public static final MediaType.Binary APPLICATION_X_CHROME_EXTENSION = akka.http.scaladsl.model.MediaTypes.application$divx$minuschrome$minusextension(); + public static final MediaType.Binary APPLICATION_X_COMPRESS = akka.http.scaladsl.model.MediaTypes.application$divx$minuscompress(); + public static final MediaType.Binary APPLICATION_X_COMPRESSED = akka.http.scaladsl.model.MediaTypes.application$divx$minuscompressed(); + public static final MediaType.Binary APPLICATION_X_DEBIAN_PACKAGE = akka.http.scaladsl.model.MediaTypes.application$divx$minusdebian$minuspackage(); + public static final MediaType.Binary APPLICATION_X_DVI = akka.http.scaladsl.model.MediaTypes.application$divx$minusdvi(); + public static final MediaType.Binary APPLICATION_X_FONT_TRUETYPE = akka.http.scaladsl.model.MediaTypes.application$divx$minusfont$minustruetype(); + public static final MediaType.Binary APPLICATION_X_FONT_OPENTYPE = akka.http.scaladsl.model.MediaTypes.application$divx$minusfont$minusopentype(); + public static final MediaType.Binary APPLICATION_X_GTAR = akka.http.scaladsl.model.MediaTypes.application$divx$minusgtar(); + public static final MediaType.Binary APPLICATION_X_GZIP = akka.http.scaladsl.model.MediaTypes.application$divx$minusgzip(); + public static final MediaType.WithOpenCharset APPLICATION_X_LATEX = akka.http.scaladsl.model.MediaTypes.application$divx$minuslatex(); + public static final MediaType.Binary APPLICATION_X_RAR_COMPRESSED = akka.http.scaladsl.model.MediaTypes.application$divx$minusrar$minuscompressed(); + public static final MediaType.Binary APPLICATION_X_REDHAT_PACKAGE_MANAGER = akka.http.scaladsl.model.MediaTypes.application$divx$minusredhat$minuspackage$minusmanager(); + public static final MediaType.Binary APPLICATION_X_SHOCKWAVE_FLASH = akka.http.scaladsl.model.MediaTypes.application$divx$minusshockwave$minusflash(); + public static final MediaType.Binary APPLICATION_X_TAR = akka.http.scaladsl.model.MediaTypes.application$divx$minustar(); + public static final MediaType.Binary APPLICATION_X_TEX = akka.http.scaladsl.model.MediaTypes.application$divx$minustex(); + public static final MediaType.Binary APPLICATION_X_TEXINFO = akka.http.scaladsl.model.MediaTypes.application$divx$minustexinfo(); + public static final MediaType.WithOpenCharset APPLICATION_X_VRML = akka.http.scaladsl.model.MediaTypes.application$divx$minusvrml(); + public static final MediaType.WithOpenCharset APPLICATION_X_WWW_FORM_URLENCODED = akka.http.scaladsl.model.MediaTypes.application$divx$minuswww$minusform$minusurlencoded(); + public static final MediaType.Binary APPLICATION_X_X509_CA_CERT = akka.http.scaladsl.model.MediaTypes.application$divx$minusx509$minusca$minuscert(); + public static final MediaType.Binary APPLICATION_X_XPINSTALL = akka.http.scaladsl.model.MediaTypes.application$divx$minusxpinstall(); + public static final MediaType.WithOpenCharset APPLICATION_XHTML_XML = akka.http.scaladsl.model.MediaTypes.application$divxhtml$plusxml(); + public static final MediaType.WithOpenCharset APPLICATION_XML_DTD = akka.http.scaladsl.model.MediaTypes.application$divxml$minusdtd(); + public static final MediaType.WithOpenCharset APPLICATION_XML = akka.http.scaladsl.model.MediaTypes.application$divxml(); + public static final MediaType.Binary APPLICATION_ZIP = akka.http.scaladsl.model.MediaTypes.application$divzip(); + + public static final MediaType.Binary AUDIO_AIFF = akka.http.scaladsl.model.MediaTypes.audio$divaiff(); + public static final MediaType.Binary AUDIO_BASIC = akka.http.scaladsl.model.MediaTypes.audio$divbasic(); + public static final MediaType.Binary AUDIO_MIDI = akka.http.scaladsl.model.MediaTypes.audio$divmidi(); + public static final MediaType.Binary AUDIO_MOD = akka.http.scaladsl.model.MediaTypes.audio$divmod(); + public static final MediaType.Binary AUDIO_MPEG = akka.http.scaladsl.model.MediaTypes.audio$divmpeg(); + public static final MediaType.Binary AUDIO_OGG = akka.http.scaladsl.model.MediaTypes.audio$divogg(); + public static final MediaType.Binary AUDIO_VOC = akka.http.scaladsl.model.MediaTypes.audio$divvoc(); + public static final MediaType.Binary AUDIO_VORBIS = akka.http.scaladsl.model.MediaTypes.audio$divvorbis(); + public static final MediaType.Binary AUDIO_VOXWARE = akka.http.scaladsl.model.MediaTypes.audio$divvoxware(); + public static final MediaType.Binary AUDIO_WAV = akka.http.scaladsl.model.MediaTypes.audio$divwav(); + public static final MediaType.Binary AUDIO_X_REALAUDIO = akka.http.scaladsl.model.MediaTypes.audio$divx$minusrealaudio(); + public static final MediaType.Binary AUDIO_X_PSID = akka.http.scaladsl.model.MediaTypes.audio$divx$minuspsid(); + public static final MediaType.Binary AUDIO_XM = akka.http.scaladsl.model.MediaTypes.audio$divxm(); + public static final MediaType.Binary AUDIO_WEBM = akka.http.scaladsl.model.MediaTypes.audio$divwebm(); + + public static final MediaType.Binary IMAGE_GIF = akka.http.scaladsl.model.MediaTypes.image$divgif(); + public static final MediaType.Binary IMAGE_JPEG = akka.http.scaladsl.model.MediaTypes.image$divjpeg(); + public static final MediaType.Binary IMAGE_PICT = akka.http.scaladsl.model.MediaTypes.image$divpict(); + public static final MediaType.Binary IMAGE_PNG = akka.http.scaladsl.model.MediaTypes.image$divpng(); + public static final MediaType.Binary IMAGE_SVG_XML = akka.http.scaladsl.model.MediaTypes.image$divsvg$plusxml(); + public static final MediaType.Binary IMAGE_TIFF = akka.http.scaladsl.model.MediaTypes.image$divtiff(); + public static final MediaType.Binary IMAGE_X_ICON = akka.http.scaladsl.model.MediaTypes.image$divx$minusicon(); + public static final MediaType.Binary IMAGE_X_MS_BMP = akka.http.scaladsl.model.MediaTypes.image$divx$minusms$minusbmp(); + public static final MediaType.Binary IMAGE_X_PCX = akka.http.scaladsl.model.MediaTypes.image$divx$minuspcx(); + public static final MediaType.Binary IMAGE_X_PICT = akka.http.scaladsl.model.MediaTypes.image$divx$minuspict(); + public static final MediaType.Binary IMAGE_X_QUICKTIME = akka.http.scaladsl.model.MediaTypes.image$divx$minusquicktime(); + public static final MediaType.Binary IMAGE_X_RGB = akka.http.scaladsl.model.MediaTypes.image$divx$minusrgb(); + public static final MediaType.Binary IMAGE_X_XBITMAP = akka.http.scaladsl.model.MediaTypes.image$divx$minusxbitmap(); + public static final MediaType.Binary IMAGE_X_XPIXMAP = akka.http.scaladsl.model.MediaTypes.image$divx$minusxpixmap(); + public static final MediaType.Binary IMAGE_WEBP = akka.http.scaladsl.model.MediaTypes.image$divwebp(); + + public static final MediaType.Binary MESSAGE_HTTP = akka.http.scaladsl.model.MediaTypes.message$divhttp(); + public static final MediaType.Binary MESSAGE_DELIVERY_STATUS = akka.http.scaladsl.model.MediaTypes.message$divdelivery$minusstatus(); + public static final MediaType.Binary MESSAGE_RFC822 = akka.http.scaladsl.model.MediaTypes.message$divrfc822(); + + public static final MediaType.WithOpenCharset MULTIPART_MIXED = akka.http.scaladsl.model.MediaTypes.multipart$divmixed(); + public static final MediaType.WithOpenCharset MULTIPART_ALTERNATIVE = akka.http.scaladsl.model.MediaTypes.multipart$divalternative(); + public static final MediaType.WithOpenCharset MULTIPART_RELATED = akka.http.scaladsl.model.MediaTypes.multipart$divrelated(); + public static final MediaType.WithOpenCharset MULTIPART_FORM_DATA = akka.http.scaladsl.model.MediaTypes.multipart$divform$minusdata(); + public static final MediaType.WithOpenCharset MULTIPART_SIGNED = akka.http.scaladsl.model.MediaTypes.multipart$divsigned(); + public static final MediaType.WithOpenCharset MULTIPART_ENCRYPTED = akka.http.scaladsl.model.MediaTypes.multipart$divencrypted(); + public static final MediaType.WithOpenCharset MULTIPART_BYTERANGES = akka.http.scaladsl.model.MediaTypes.multipart$divbyteranges(); + + public static final MediaType.WithOpenCharset TEXT_ASP = akka.http.scaladsl.model.MediaTypes.text$divasp(); + public static final MediaType.WithOpenCharset TEXT_CACHE_MANIFEST = akka.http.scaladsl.model.MediaTypes.text$divcache$minusmanifest(); + public static final MediaType.WithOpenCharset TEXT_CALENDAR = akka.http.scaladsl.model.MediaTypes.text$divcalendar(); + public static final MediaType.WithOpenCharset TEXT_CSS = akka.http.scaladsl.model.MediaTypes.text$divcss(); + public static final MediaType.WithOpenCharset TEXT_CSV = akka.http.scaladsl.model.MediaTypes.text$divcsv(); + public static final MediaType.WithOpenCharset TEXT_HTML = akka.http.scaladsl.model.MediaTypes.text$divhtml(); + public static final MediaType.WithOpenCharset TEXT_MCF = akka.http.scaladsl.model.MediaTypes.text$divmcf(); + public static final MediaType.WithOpenCharset TEXT_PLAIN = akka.http.scaladsl.model.MediaTypes.text$divplain(); + public static final MediaType.WithOpenCharset TEXT_RICHTEXT = akka.http.scaladsl.model.MediaTypes.text$divrichtext(); + public static final MediaType.WithOpenCharset TEXT_TAB_SEPARATED_VALUES = akka.http.scaladsl.model.MediaTypes.text$divtab$minusseparated$minusvalues(); + public static final MediaType.WithOpenCharset TEXT_URI_LIST = akka.http.scaladsl.model.MediaTypes.text$divuri$minuslist(); + public static final MediaType.WithOpenCharset TEXT_VND_WAP_WML = akka.http.scaladsl.model.MediaTypes.text$divvnd$u002Ewap$u002Ewml(); + public static final MediaType.WithOpenCharset TEXT_VND_WAP_WMLSCRIPT = akka.http.scaladsl.model.MediaTypes.text$divvnd$u002Ewap$u002Ewmlscript(); + public static final MediaType.WithOpenCharset TEXT_X_ASM = akka.http.scaladsl.model.MediaTypes.text$divx$minusasm(); + public static final MediaType.WithOpenCharset TEXT_X_C = akka.http.scaladsl.model.MediaTypes.text$divx$minusc(); + public static final MediaType.WithOpenCharset TEXT_X_COMPONENT = akka.http.scaladsl.model.MediaTypes.text$divx$minuscomponent(); + public static final MediaType.WithOpenCharset TEXT_X_H = akka.http.scaladsl.model.MediaTypes.text$divx$minush(); + public static final MediaType.WithOpenCharset TEXT_X_JAVA_SOURCE = akka.http.scaladsl.model.MediaTypes.text$divx$minusjava$minussource(); + public static final MediaType.WithOpenCharset TEXT_X_PASCAL = akka.http.scaladsl.model.MediaTypes.text$divx$minuspascal(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPT = akka.http.scaladsl.model.MediaTypes.text$divx$minusscript(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTCSH = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptcsh(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTELISP = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptelisp(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTKSH = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptksh(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTLISP = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptlisp(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTPERL = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptperl(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTPERL_MODULE = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptperl$minusmodule(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTPHYTON = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptphyton(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTREXX = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptrexx(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTSCHEME = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptscheme(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTSH = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptsh(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTTCL = akka.http.scaladsl.model.MediaTypes.text$divx$minusscripttcl(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTTCSH = akka.http.scaladsl.model.MediaTypes.text$divx$minusscripttcsh(); + public static final MediaType.WithOpenCharset TEXT_X_SCRIPTZSH = akka.http.scaladsl.model.MediaTypes.text$divx$minusscriptzsh(); + public static final MediaType.WithOpenCharset TEXT_X_SERVER_PARSED_HTML = akka.http.scaladsl.model.MediaTypes.text$divx$minusserver$minusparsed$minushtml(); + public static final MediaType.WithOpenCharset TEXT_X_SETEXT = akka.http.scaladsl.model.MediaTypes.text$divx$minussetext(); + public static final MediaType.WithOpenCharset TEXT_X_SGML = akka.http.scaladsl.model.MediaTypes.text$divx$minussgml(); + public static final MediaType.WithOpenCharset TEXT_X_SPEECH = akka.http.scaladsl.model.MediaTypes.text$divx$minusspeech(); + public static final MediaType.WithOpenCharset TEXT_X_UUENCODE = akka.http.scaladsl.model.MediaTypes.text$divx$minusuuencode(); + public static final MediaType.WithOpenCharset TEXT_X_VCALENDAR = akka.http.scaladsl.model.MediaTypes.text$divx$minusvcalendar(); + public static final MediaType.WithOpenCharset TEXT_X_VCARD = akka.http.scaladsl.model.MediaTypes.text$divx$minusvcard(); + public static final MediaType.WithOpenCharset TEXT_XML = akka.http.scaladsl.model.MediaTypes.text$divxml(); + + public static final MediaType.Binary VIDEO_AVS_VIDEO = akka.http.scaladsl.model.MediaTypes.video$divavs$minusvideo(); + public static final MediaType.Binary VIDEO_DIVX = akka.http.scaladsl.model.MediaTypes.video$divdivx(); + public static final MediaType.Binary VIDEO_GL = akka.http.scaladsl.model.MediaTypes.video$divgl(); + public static final MediaType.Binary VIDEO_MP4 = akka.http.scaladsl.model.MediaTypes.video$divmp4(); + public static final MediaType.Binary VIDEO_MPEG = akka.http.scaladsl.model.MediaTypes.video$divmpeg(); + public static final MediaType.Binary VIDEO_OGG = akka.http.scaladsl.model.MediaTypes.video$divogg(); + public static final MediaType.Binary VIDEO_QUICKTIME = akka.http.scaladsl.model.MediaTypes.video$divquicktime(); + public static final MediaType.Binary VIDEO_X_DV = akka.http.scaladsl.model.MediaTypes.video$divx$minusdv(); + public static final MediaType.Binary VIDEO_X_FLV = akka.http.scaladsl.model.MediaTypes.video$divx$minusflv(); + public static final MediaType.Binary VIDEO_X_MOTION_JPEG = akka.http.scaladsl.model.MediaTypes.video$divx$minusmotion$minusjpeg(); + public static final MediaType.Binary VIDEO_X_MS_ASF = akka.http.scaladsl.model.MediaTypes.video$divx$minusms$minusasf(); + public static final MediaType.Binary VIDEO_X_MSVIDEO = akka.http.scaladsl.model.MediaTypes.video$divx$minusmsvideo(); + public static final MediaType.Binary VIDEO_X_SGI_MOVIE = akka.http.scaladsl.model.MediaTypes.video$divx$minussgi$minusmovie(); + public static final MediaType.Binary VIDEO_WEBM = akka.http.scaladsl.model.MediaTypes.video$divwebm(); /** * Creates a custom media type. */ - public static MediaType custom( - String mainType, - String subType, - boolean compressible, - akka.http.scaladsl.model.MediaType.Encoding encoding, - Iterable fileExtensions, - Map params) { - return akka.http.scaladsl.model.MediaType.custom(mainType, subType, encoding, compressible, Util.convertIterable(fileExtensions), Util.convertMapToScala(params), false); + public static MediaType custom(String value, boolean binary, boolean compressible) { + return akka.http.scaladsl.model.MediaType.custom(value, binary, compressible, List.empty()); } /** diff --git a/akka-http-core/src/main/scala/akka/http/impl/model/parser/AcceptHeader.scala b/akka-http-core/src/main/scala/akka/http/impl/model/parser/AcceptHeader.scala index 1f9a613481..0579637df6 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/model/parser/AcceptHeader.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/model/parser/AcceptHeader.scala @@ -27,7 +27,7 @@ private[parser] trait AcceptHeader { this: Parser with CommonRules with CommonAc } } else { val (p, q) = MediaRange.splitOffQValue(params.toMap) - MediaRange(getMediaType(main, sub, p), q) + MediaRange(getMediaType(main, sub, p contains "charset", p), q) } } } diff --git a/akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonActions.scala b/akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonActions.scala index ca32ff847f..5a0e50842c 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonActions.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonActions.scala @@ -12,22 +12,28 @@ private[parser] trait CommonActions { type StringMapBuilder = scala.collection.mutable.Builder[(String, String), Map[String, String]] - def getMediaType(mainType: String, subType: String, params: Map[String, String]): MediaType = { + def getMediaType(mainType: String, subType: String, charsetDefined: Boolean, + params: Map[String, String]): MediaType = { + val subLower = subType.toRootLowerCase mainType.toRootLowerCase match { - case "multipart" ⇒ subType.toRootLowerCase match { + case "multipart" ⇒ subLower match { case "mixed" ⇒ multipart.mixed(params) case "alternative" ⇒ multipart.alternative(params) case "related" ⇒ multipart.related(params) case "form-data" ⇒ multipart.`form-data`(params) case "signed" ⇒ multipart.signed(params) case "encrypted" ⇒ multipart.encrypted(params) - case custom ⇒ multipart(custom, params) + case custom ⇒ MediaType.customMultipart(custom, params) } case mainLower ⇒ - MediaTypes.getForKey((mainLower, subType.toRootLowerCase)) match { + MediaTypes.getForKey((mainLower, subLower)) match { case Some(registered) ⇒ if (params.isEmpty) registered else registered.withParams(params) - case None ⇒ MediaType.custom(mainType, subType, encoding = MediaType.Encoding.Open, - params = params, allowArbitrarySubtypes = true) + case None ⇒ + if (charsetDefined) + MediaType.customWithOpenCharset(mainLower, subType, params = params, allowArbitrarySubtypes = true) + else + MediaType.customBinary(mainLower, subType, compressible = true, params = params, + allowArbitrarySubtypes = true) } } } diff --git a/akka-http-core/src/main/scala/akka/http/impl/model/parser/ContentTypeHeader.scala b/akka-http-core/src/main/scala/akka/http/impl/model/parser/ContentTypeHeader.scala index 73713d51b9..9b2cba6a5c 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/model/parser/ContentTypeHeader.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/model/parser/ContentTypeHeader.scala @@ -23,8 +23,14 @@ private[parser] trait ContentTypeHeader { this: Parser with CommonRules with Com params match { case Nil ⇒ val parameters = if (builder eq null) Map.empty[String, String] else builder.result() - val mediaType = getMediaType(main, sub, parameters) - ContentType(mediaType, charset) + getMediaType(main, sub, charset.isDefined, parameters) match { + case x: MediaType.Binary ⇒ ContentType.Binary(x) + case x: MediaType.WithFixedCharset ⇒ ContentType.WithFixedCharset(x) + case x: MediaType.WithOpenCharset ⇒ + // if we have an open charset media-type but no charset parameter we default to UTF-8 + val cs = if (charset.isDefined) charset.get else HttpCharsets.`UTF-8` + ContentType.WithCharset(x, cs) + } case Seq(("charset", value), tail @ _*) ⇒ contentType(main, sub, tail, Some(getCharset(value)), builder) diff --git a/akka-http-core/src/main/scala/akka/http/impl/model/parser/LinkHeader.scala b/akka-http-core/src/main/scala/akka/http/impl/model/parser/LinkHeader.scala index a629e0bb5d..06ac655691 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/model/parser/LinkHeader.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/model/parser/LinkHeader.scala @@ -62,7 +62,7 @@ private[parser] trait LinkHeader { this: Parser with CommonRules with CommonActi } } - def `link-media-type` = rule { `media-type` ~> ((mt, st, pm) ⇒ getMediaType(mt, st, pm.toMap)) } + def `link-media-type` = rule { `media-type` ~> ((mt, st, pm) ⇒ getMediaType(mt, st, pm contains "charset", pm.toMap)) } // filter out subsequent `rel`, `media`, `title`, `type` and `type*` params @tailrec private def sanitize(params: Seq[LinkParam], result: Seq[LinkParam] = Nil, seenRel: Boolean = false, diff --git a/akka-http-core/src/main/scala/akka/http/impl/util/JavaMapping.scala b/akka-http-core/src/main/scala/akka/http/impl/util/JavaMapping.scala index 11509255d8..26efac5a19 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/util/JavaMapping.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/util/JavaMapping.scala @@ -155,6 +155,10 @@ private[http] object JavaMapping { implicit object DateTime extends Inherited[jm.DateTime, akka.http.scaladsl.model.DateTime] implicit object ContentType extends Inherited[jm.ContentType, sm.ContentType] + implicit object ContentTypeBinary extends Inherited[jm.ContentType.Binary, sm.ContentType.Binary] + implicit object ContentTypeNonBinary extends Inherited[jm.ContentType.NonBinary, sm.ContentType.NonBinary] + implicit object ContentTypeWithFixedCharset extends Inherited[jm.ContentType.WithFixedCharset, sm.ContentType.WithFixedCharset] + implicit object ContentTypeWithCharset extends Inherited[jm.ContentType.WithCharset, sm.ContentType.WithCharset] implicit object ContentTypeRange extends Inherited[jm.ContentTypeRange, sm.ContentTypeRange] implicit object Host extends Inherited[jm.Host, sm.Uri.Host] implicit object HttpCharset extends Inherited[jm.HttpCharset, sm.HttpCharset] @@ -167,6 +171,10 @@ private[http] object JavaMapping { implicit object HttpResponse extends Inherited[jm.HttpResponse, sm.HttpResponse] implicit object MediaRange extends Inherited[jm.MediaRange, sm.MediaRange] implicit object MediaType extends Inherited[jm.MediaType, sm.MediaType] + implicit object MediaTypeBinary extends Inherited[jm.MediaType.Binary, sm.MediaType.Binary] + implicit object MediaTypeNonBinary extends Inherited[jm.MediaType.NonBinary, sm.MediaType.NonBinary] + implicit object MediaTypeFixedCharset extends Inherited[jm.MediaType.WithFixedCharset, sm.MediaType.WithFixedCharset] + implicit object MediaTypeOpenCharset extends Inherited[jm.MediaType.WithOpenCharset, sm.MediaType.WithOpenCharset] implicit object StatusCode extends Inherited[jm.StatusCode, sm.StatusCode] implicit object ContentRange extends Inherited[jm.ContentRange, sm.ContentRange] diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/ContentType.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/ContentType.scala index b97d1dee7c..22b79288bd 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/ContentType.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/ContentType.scala @@ -12,7 +12,10 @@ import akka.http.impl.util.JavaMapping.Implicits._ final case class ContentTypeRange(mediaRange: MediaRange, charsetRange: HttpCharsetRange) extends jm.ContentTypeRange with ValueRenderable { def matches(contentType: jm.ContentType) = - mediaRange.matches(contentType.mediaType) && charsetRange.matches(contentType.charset) + contentType match { + case ContentType.Binary(mt) ⇒ mediaRange.matches(mt) + case x: ContentType.NonBinary ⇒ mediaRange.matches(x.mediaType) && charsetRange.matches(x.charset) + } def render[R <: Rendering](r: R): r.type = charsetRange match { case HttpCharsetRange.`*` ⇒ r ~~ mediaRange @@ -22,7 +25,7 @@ final case class ContentTypeRange(mediaRange: MediaRange, charsetRange: HttpChar /** * Returns a [[ContentType]] instance which fits this range. */ - def specimen: ContentType = ContentType(mediaRange.specimen, charsetRange.specimen) + def specimen: ContentType = ContentType(mediaRange.specimen, () ⇒ charsetRange.specimen) } object ContentTypeRange { @@ -31,74 +34,82 @@ object ContentTypeRange { implicit def apply(mediaType: MediaType): ContentTypeRange = apply(mediaType, HttpCharsetRange.`*`) implicit def apply(mediaRange: MediaRange): ContentTypeRange = apply(mediaRange, HttpCharsetRange.`*`) implicit def apply(contentType: ContentType): ContentTypeRange = - contentType.definedCharset match { - case Some(charset) ⇒ apply(contentType.mediaType, charset) - case None ⇒ ContentTypeRange(contentType.mediaType) + contentType match { + case ContentType.Binary(mt) ⇒ ContentTypeRange(mt) + case ContentType.WithFixedCharset(mt) ⇒ ContentTypeRange(mt) + case ContentType.WithCharset(mt, cs) ⇒ ContentTypeRange(mt, cs) } } -abstract case class ContentType private (mediaType: MediaType, definedCharset: Option[HttpCharset]) extends jm.ContentType with ValueRenderable { - private[http] def render[R <: Rendering](r: R): r.type = definedCharset match { - case Some(cs) ⇒ r ~~ mediaType ~~ ContentType.`; charset=` ~~ cs - case _ ⇒ r ~~ mediaType - } - def charset: HttpCharset = definedCharset orElse mediaType.encoding.charset getOrElse HttpCharsets.`UTF-8` +/** + * A `ContentType` represents a specific MediaType / HttpCharset combination. + * + * If the MediaType is not flexible with regard to the charset used, e.g. because it's a binary MediaType or + * the charset is fixed, then the `ContentType` is a simple wrapper. + */ +sealed trait ContentType extends jm.ContentType with ValueRenderable { + def mediaType: MediaType + def charsetOption: Option[HttpCharset] - def hasOpenCharset: Boolean = definedCharset.isEmpty && mediaType.encoding == MediaType.Encoding.Open - - def withMediaType(mediaType: MediaType) = - if (mediaType != this.mediaType) ContentType(mediaType, definedCharset) else this - def withCharset(charset: HttpCharset) = - if (definedCharset.isEmpty || charset != definedCharset.get) ContentType(mediaType, charset) else this - def withoutDefinedCharset = - if (definedCharset.isDefined) ContentType(mediaType, None) else this - def withDefaultCharset(charset: HttpCharset) = - if (mediaType.encoding == MediaType.Encoding.Open && definedCharset.isEmpty) ContentType(mediaType, charset) else this + private[http] def render[R <: Rendering](r: R): r.type = r ~~ mediaType /** Java API */ - def getDefinedCharset: JOption[jm.HttpCharset] = definedCharset.asJava + def getCharsetOption: JOption[jm.HttpCharset] = charsetOption.asJava } object ContentType { - private[http] case object `; charset=` extends SingletonValueRenderable - - implicit def apply(mediaType: MediaType): ContentType = apply(mediaType, None) - - def apply(mediaType: MediaType, charset: HttpCharset): ContentType = apply(mediaType, Some(charset)) - - def apply(mediaType: MediaType, charset: Option[HttpCharset]): ContentType = { - val definedCharset = - charset match { - case None ⇒ None - case Some(cs) ⇒ mediaType.encoding match { - case MediaType.Encoding.Open ⇒ charset - case MediaType.Encoding.Fixed(`cs`) ⇒ None - case x ⇒ throw new IllegalArgumentException( - s"MediaType $mediaType has a $x encoding and doesn't allow a custom `charset` $cs") - } - } - new ContentType(mediaType, definedCharset) {} + final case class Binary(mediaType: MediaType.Binary) extends jm.ContentType.Binary with ContentType { + def binary = true + def charsetOption = None } + sealed trait NonBinary extends jm.ContentType.NonBinary with ContentType { + def binary = false + def charset: HttpCharset + def charsetOption = Some(charset) + } + + final case class WithFixedCharset(val mediaType: MediaType.WithFixedCharset) + extends jm.ContentType.WithFixedCharset with NonBinary { + def charset = mediaType.charset + } + + final case class WithCharset(val mediaType: MediaType.WithOpenCharset, val charset: HttpCharset) + extends jm.ContentType.WithCharset with NonBinary { + + private[http] override def render[R <: Rendering](r: R): r.type = + super.render(r) ~~ ContentType.`; charset=` ~~ charset + } + + implicit def apply(mediaType: MediaType.Binary): Binary = Binary(mediaType) + implicit def apply(mediaType: MediaType.WithFixedCharset): WithFixedCharset = WithFixedCharset(mediaType) + def apply(mediaType: MediaType.WithOpenCharset, charset: HttpCharset): WithCharset = WithCharset(mediaType, charset) + def apply(mediaType: MediaType, charset: () ⇒ HttpCharset): ContentType = + mediaType match { + case x: MediaType.Binary ⇒ ContentType(x) + case x: MediaType.WithFixedCharset ⇒ ContentType(x) + case x: MediaType.WithOpenCharset ⇒ ContentType(x, charset()) + } + + def unapply(contentType: ContentType): Option[(MediaType, Option[HttpCharset])] = + Some(contentType.mediaType → contentType.charsetOption) + /** * Tries to parse a ``ContentType`` value from the given String. Returns ``Right(contentType)`` if successful and * ``Left(errors)`` otherwise. */ def parse(value: String): Either[List[ErrorInfo], ContentType] = headers.`Content-Type`.parseFromValueString(value).right.map(_.contentType) + + private[http] case object `; charset=` extends SingletonValueRenderable } object ContentTypes { val `application/json` = ContentType(MediaTypes.`application/json`) val `application/octet-stream` = ContentType(MediaTypes.`application/octet-stream`) - - val `text/plain` = ContentType(MediaTypes.`text/plain`) - val `text/plain(UTF-8)` = ContentType(MediaTypes.`text/plain`, HttpCharsets.`UTF-8`) - val `text/html` = ContentType(MediaTypes.`text/html`) - val `text/xml` = ContentType(MediaTypes.`text/xml`) - - val `application/x-www-form-urlencoded` = ContentType(MediaTypes.`application/x-www-form-urlencoded`) - val `multipart/form-data` = ContentType(MediaTypes.`multipart/form-data`) + val `text/plain(UTF-8)` = MediaTypes.`text/plain` withCharset HttpCharsets.`UTF-8` + val `text/html(UTF-8)` = MediaTypes.`text/html` withCharset HttpCharsets.`UTF-8` + val `text/xml(UTF-8)` = MediaTypes.`text/xml` withCharset HttpCharsets.`UTF-8` // used for explicitly suppressing the rendering of Content-Type headers on requests and responses val NoContentType = ContentType(MediaTypes.NoMediaType) diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/FormData.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/FormData.scala index 811e7ad2b0..a42e6ab24d 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/FormData.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/FormData.scala @@ -6,8 +6,6 @@ package akka.http.scaladsl.model import akka.http.impl.model.parser.CharacterClasses import akka.http.impl.util.StringRendering -import akka.http.javadsl.{ model ⇒ jm } -import akka.http.scaladsl.model.HttpCharsets._ import akka.http.scaladsl.model.MediaTypes._ /** @@ -19,7 +17,7 @@ final case class FormData(fields: Uri.Query) { def toEntity(charset: HttpCharset): akka.http.scaladsl.model.RequestEntity = { val render: StringRendering = UriRendering.renderQuery(new StringRendering, this.fields, charset.nioCharset, CharacterClasses.unreserved) - HttpEntity(ContentType(`application/x-www-form-urlencoded`, `UTF-8`), render.get) + HttpEntity(`application/x-www-form-urlencoded` withCharset charset, render.get) } } diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpCharset.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpCharset.scala index 870857e561..8ebf0d7724 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpCharset.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpCharset.scala @@ -37,7 +37,6 @@ object HttpCharsetRange { require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") final def render[R <: Rendering](r: R): r.type = if (qValue < 1.0f) r ~~ "*;q=" ~~ qValue else r ~~ '*' def matches(charset: HttpCharset) = true - def matchesAll: Boolean = true def specimen: HttpCharset = HttpCharsets.`UTF-8` def withQValue(qValue: Float) = if (qValue == 1.0f) `*` else if (qValue != this.qValue) `*`(qValue.toFloat) else this @@ -47,7 +46,6 @@ object HttpCharsetRange { final case class One(charset: HttpCharset, qValue: Float) extends HttpCharsetRange { require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") def matches(charset: HttpCharset) = this.charset.value.equalsIgnoreCase(charset.value) - def matchesAll: Boolean = false def specimen: HttpCharset = charset def withQValue(qValue: Float) = One(charset, qValue) def render[R <: Rendering](r: R): r.type = if (qValue < 1.0f) r ~~ charset ~~ ";q=" ~~ qValue else r ~~ charset diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala index 83aea6087c..a68484c92f 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpEntity.scala @@ -178,7 +178,7 @@ object HttpEntity { implicit def apply(string: String): Strict = apply(ContentTypes.`text/plain(UTF-8)`, string) implicit def apply(bytes: Array[Byte]): Strict = apply(ContentTypes.`application/octet-stream`, bytes) implicit def apply(data: ByteString): Strict = apply(ContentTypes.`application/octet-stream`, data) - def apply(contentType: ContentType, string: String): Strict = + def apply(contentType: ContentType.NonBinary, string: String): Strict = if (string.isEmpty) empty(contentType) else apply(contentType, ByteString(string.getBytes(contentType.charset.nioCharset))) def apply(contentType: ContentType, bytes: Array[Byte]): Strict = if (bytes.length == 0) empty(contentType) else apply(contentType, ByteString(bytes)) diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMessage.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMessage.scala index 8b6262ee1c..9a94586ddd 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMessage.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpMessage.scala @@ -100,7 +100,8 @@ sealed trait HttpMessage extends jm.HttpMessage { def withEntity(string: String): Self = withEntity(HttpEntity(string)) def withEntity(bytes: Array[Byte]): Self = withEntity(HttpEntity(bytes)) def withEntity(bytes: ByteString): Self = withEntity(HttpEntity(bytes)) - def withEntity(contentType: jm.ContentType, string: String): Self = withEntity(HttpEntity(contentType.asInstanceOf[ContentType], string)) + def withEntity(contentType: jm.ContentType.NonBinary, string: String): Self = + withEntity(HttpEntity(contentType.asInstanceOf[ContentType.NonBinary], string)) def withEntity(contentType: jm.ContentType, bytes: Array[Byte]): Self = withEntity(HttpEntity(contentType.asInstanceOf[ContentType], bytes)) def withEntity(contentType: jm.ContentType, bytes: ByteString): Self = withEntity(HttpEntity(contentType.asInstanceOf[ContentType], bytes)) def withEntity(contentType: jm.ContentType, file: java.io.File): Self = withEntity(HttpEntity(contentType.asInstanceOf[ContentType], file)) @@ -163,115 +164,11 @@ final case class HttpRequest(method: HttpMethod = HttpMethods.GET, def withEffectiveUri(securedConnection: Boolean, defaultHostHeader: Host = Host.empty): HttpRequest = copy(uri = effectiveUri(securedConnection, defaultHostHeader)) - /** - * The media-ranges accepted by the client according to the `Accept` request header. - * The returned ranges are sorted by decreasing q-value. - */ - def acceptedMediaRanges: immutable.Seq[MediaRange] = - (for { - Accept(mediaRanges) ← headers - range ← mediaRanges - } yield range).sortBy(-_.qValue) - - /** - * The charset-ranges accepted by the client according to the `Accept-Charset` request header. - * The returned ranges are sorted by decreasing q-value. - */ - def acceptedCharsetRanges: immutable.Seq[HttpCharsetRange] = - (for { - `Accept-Charset`(charsetRanges) ← headers - range ← charsetRanges - } yield range).sortBy(-_.qValue) - - /** - * The encoding-ranges accepted by the client according to the `Accept-Encoding` request header. - * The returned ranges are sorted by decreasing q-value. - */ - def acceptedEncodingRanges: immutable.Seq[HttpEncodingRange] = - (for { - `Accept-Encoding`(encodingRanges) ← headers - range ← encodingRanges - } yield range).sortBy(-_.qValue) - - /** - * The language-ranges accepted by the client according to the `Accept-Language` request header. - * The returned ranges are sorted by increasing generality (i.e. most specific first). - */ - def acceptedLanguageRanges: immutable.Seq[LanguageRange] = - (for { - `Accept-Language`(languageRanges) ← headers - range ← languageRanges - } yield range).sortBy { - case _: LanguageRange.`*` ⇒ 0 // most general, needs to come last - case x ⇒ -(x.subTags.size + 1) // more subtags -> more specific -> go first - } - /** * All cookies provided by the client in one or more `Cookie` headers. */ def cookies: immutable.Seq[HttpCookiePair] = for (`Cookie`(cookies) ← headers; cookie ← cookies) yield cookie - /** - * Determines whether the given media-type is accepted by the client. - */ - def isMediaTypeAccepted(mediaType: MediaType, ranges: Seq[MediaRange] = acceptedMediaRanges): Boolean = - qValueForMediaType(mediaType, ranges) > 0f - - /** - * Returns the q-value that the client (implicitly or explicitly) attaches to the given media-type. - */ - def qValueForMediaType(mediaType: MediaType, ranges: Seq[MediaRange] = acceptedMediaRanges): Float = - ranges match { - case Nil ⇒ 1.0f // http://tools.ietf.org/html/rfc7231#section-5.3.1 - case x ⇒ x collectFirst { case r if r matches mediaType ⇒ r.qValue } getOrElse 0f - } - - /** - * Determines whether the given charset is accepted by the client. - */ - def isCharsetAccepted(charset: HttpCharset, ranges: Seq[HttpCharsetRange] = acceptedCharsetRanges): Boolean = - qValueForCharset(charset, ranges) > 0f - - /** - * Returns the q-value that the client (implicitly or explicitly) attaches to the given charset. - */ - def qValueForCharset(charset: HttpCharset, ranges: Seq[HttpCharsetRange] = acceptedCharsetRanges): Float = - ranges match { - case Nil ⇒ 1.0f // http://tools.ietf.org/html/rfc7231#section-5.3.1 - case x ⇒ x collectFirst { case r if r matches charset ⇒ r.qValue } getOrElse 0f - } - - /** - * Determines whether the given encoding is accepted by the client. - */ - def isEncodingAccepted(encoding: HttpEncoding, ranges: Seq[HttpEncodingRange] = acceptedEncodingRanges): Boolean = - qValueForEncoding(encoding, ranges) > 0f - - /** - * Returns the q-value that the client (implicitly or explicitly) attaches to the given encoding. - */ - def qValueForEncoding(encoding: HttpEncoding, ranges: Seq[HttpEncodingRange] = acceptedEncodingRanges): Float = - ranges match { - case Nil ⇒ 1.0f // http://tools.ietf.org/html/rfc7231#section-5.3.1 - case x ⇒ x collectFirst { case r if r matches encoding ⇒ r.qValue } getOrElse 0f - } - - /** - * Determines whether the given language is accepted by the client. - */ - def isLanguageAccepted(language: Language, ranges: Seq[LanguageRange] = acceptedLanguageRanges): Boolean = - qValueForLanguage(language, ranges) > 0f - - /** - * Returns the q-value that the client (implicitly or explicitly) attaches to the given language. - * Note: The given ranges must be sorted by increasing generality (i.e. most specific first)! - */ - def qValueForLanguage(language: Language, ranges: Seq[LanguageRange] = acceptedLanguageRanges): Float = - ranges match { - case Nil ⇒ 1.0f // http://tools.ietf.org/html/rfc7231#section-5.3.1 - case x ⇒ x collectFirst { case r if r matches language ⇒ r.qValue } getOrElse 0f - } - /** * Determines whether this request can be safely retried, which is the case only of the request method is idempotent. */ diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaRange.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaRange.scala new file mode 100644 index 0000000000..4a526073da --- /dev/null +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaRange.scala @@ -0,0 +1,163 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.scaladsl.model + +import language.implicitConversions +import java.util +import akka.http.impl.util._ +import akka.http.javadsl.{ model ⇒ jm } + +sealed abstract class MediaRange extends jm.MediaRange with Renderable with WithQValue[MediaRange] { + def value: String + def mainType: String + def params: Map[String, String] + def qValue: Float + def matches(mediaType: MediaType): Boolean + def isApplication = false + def isAudio = false + def isImage = false + def isMessage = false + def isMultipart = false + def isText = false + def isVideo = false + def isWildcard = mainType == "*" + + /** + * Returns a copy of this instance with the params replaced by the given ones. + * If the given map contains a "q" value the `qValue` member is (also) updated. + */ + def withParams(params: Map[String, String]): MediaRange + + /** + * Constructs a `ContentTypeRange` from this instance and the given charset. + */ + def withCharsetRange(charsetRange: HttpCharsetRange): ContentTypeRange = ContentTypeRange(this, charsetRange) + + /** + * Returns a [[MediaType]] instance which fits this range. + */ + def specimen: MediaType + + /** Java API */ + def getParams: util.Map[String, String] = { + import collection.JavaConverters._ + params.asJava + } + /** Java API */ + def matches(mediaType: jm.MediaType): Boolean = { + import akka.http.impl.util.JavaMapping.Implicits._ + matches(mediaType.asScala) + } +} + +object MediaRange { + private[http] def splitOffQValue(params: Map[String, String], defaultQ: Float = 1.0f): (Map[String, String], Float) = + params.get("q") match { + case Some(x) ⇒ (params - "q") -> (try x.toFloat catch { case _: NumberFormatException ⇒ 1.0f }) + case None ⇒ params -> defaultQ + } + + private final case class Custom(mainType: String, params: Map[String, String], qValue: Float) + extends MediaRange with ValueRenderable { + require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") + def matches(mediaType: MediaType) = mainType == "*" || mediaType.mainType == mainType + def withParams(params: Map[String, String]) = custom(mainType, params, qValue) + def withQValue(qValue: Float) = if (qValue != this.qValue) custom(mainType, params, qValue) else this + def render[R <: Rendering](r: R): r.type = { + r ~~ mainType ~~ '/' ~~ '*' + if (qValue < 1.0f) r ~~ ";q=" ~~ qValue + if (params.nonEmpty) params foreach { case (k, v) ⇒ r ~~ ';' ~~ ' ' ~~ k ~~ '=' ~~# v } + r + } + override def isApplication = mainType == "application" + override def isAudio = mainType == "audio" + override def isImage = mainType == "image" + override def isMessage = mainType == "message" + override def isMultipart = mainType == "multipart" + override def isText = mainType == "text" + override def isVideo = mainType == "video" + def specimen = MediaType.customBinary(mainType, "custom", compressible = true) + } + + def custom(mainType: String, params: Map[String, String] = Map.empty, qValue: Float = 1.0f): MediaRange = { + val (ps, q) = splitOffQValue(params, qValue) + Custom(mainType.toRootLowerCase, ps, q) + } + + final case class One(mediaType: MediaType, qValue: Float) extends MediaRange with ValueRenderable { + require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") + def mainType = mediaType.mainType + def params = mediaType.params + override def isApplication = mediaType.isApplication + override def isAudio = mediaType.isAudio + override def isImage = mediaType.isImage + override def isMessage = mediaType.isMessage + override def isMultipart = mediaType.isMultipart + override def isText = mediaType.isText + override def isVideo = mediaType.isVideo + def matches(mediaType: MediaType) = + this.mediaType.mainType == mediaType.mainType && this.mediaType.subType == mediaType.subType + def withParams(params: Map[String, String]) = copy(mediaType = mediaType.withParams(params)) + def withQValue(qValue: Float) = copy(qValue = qValue) + def render[R <: Rendering](r: R): r.type = if (qValue < 1.0f) r ~~ mediaType ~~ ";q=" ~~ qValue else r ~~ mediaType + def specimen = mediaType + } + + implicit def apply(mediaType: MediaType): MediaRange = apply(mediaType, 1.0f) + def apply(mediaType: MediaType, qValue: Float = 1.0f): MediaRange = One(mediaType, qValue) +} + +object MediaRanges extends ObjectRegistry[String, MediaRange] { + + sealed abstract case class PredefinedMediaRange(value: String) extends MediaRange with LazyValueBytesRenderable { + val mainType = value takeWhile (_ != '/') + register(mainType, this) + def params = Map.empty + def qValue = 1.0f + def withParams(params: Map[String, String]) = MediaRange.custom(mainType, params) + def withQValue(qValue: Float) = if (qValue != 1.0f) MediaRange.custom(mainType, params, qValue) else this + } + + val `*/*` = new PredefinedMediaRange("*/*") { + def matches(mediaType: MediaType) = true + def specimen = MediaTypes.`text/plain` + } + val `*/*;q=MIN` = `*/*`.withQValue(Float.MinPositiveValue) + val `application/*` = new PredefinedMediaRange("application/*") { + def matches(mediaType: MediaType) = mediaType.isApplication + override def isApplication = true + def specimen = MediaTypes.`application/json` + } + val `audio/*` = new PredefinedMediaRange("audio/*") { + def matches(mediaType: MediaType) = mediaType.isAudio + override def isAudio = true + def specimen = MediaTypes.`audio/ogg` + } + val `image/*` = new PredefinedMediaRange("image/*") { + def matches(mediaType: MediaType) = mediaType.isImage + override def isImage = true + def specimen = MediaTypes.`image/png` + } + val `message/*` = new PredefinedMediaRange("message/*") { + def matches(mediaType: MediaType) = mediaType.isMessage + override def isMessage = true + def specimen = MediaTypes.`message/rfc822` + } + val `multipart/*` = new PredefinedMediaRange("multipart/*") { + def matches(mediaType: MediaType) = mediaType.isMultipart + override def isMultipart = true + def specimen = MediaTypes.`multipart/form-data` + } + val `text/*` = new PredefinedMediaRange("text/*") { + def matches(mediaType: MediaType) = mediaType.isText + override def isText = true + def specimen = MediaTypes.`text/plain` + } + val `video/*` = new PredefinedMediaRange("video/*") { + def matches(mediaType: MediaType) = mediaType.isVideo + override def isVideo = true + def specimen = MediaTypes.`video/mp4` + } +} \ No newline at end of file diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaType.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaType.scala index cab4431c69..36e32c5968 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaType.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaType.scala @@ -5,243 +5,115 @@ package akka.http.scaladsl.model import language.implicitConversions -import java.util -import scala.collection.immutable import akka.http.impl.util._ import akka.http.javadsl.{ model ⇒ jm } +import akka.http.impl.util.JavaMapping.Implicits._ -sealed abstract class MediaRange extends jm.MediaRange with Renderable with WithQValue[MediaRange] { - def value: String - def mainType: String +/** + * A MediaType describes the type of the content of an HTTP message entity. + * + * While knowledge of the MediaType alone suffices for being able to properly interpret binary content this + * is not generally the case for non-binary (i.e. character-based) content, which also requires the definition + * of a specific character encoding ([[HttpCharset]]). + * Therefore [[MediaType]] instances are frequently encountered as a member of a [[ContentType]], which + * groups a [[MediaType]] with a potentially required [[HttpCharset]] to hold everything required for being + * able to interpret an [[HttpEntity]]. + * + * MediaTypes come in three basic forms: + * + * 1. Binary: These do not need an additional [[HttpCharset]] to be able to form a [[ContentType]]. Therefore + * they can be implicitly converted to the latter. + * + * 2. WithOpenCharset: Most character-based MediaTypes are of this form, which can be combined with all + * [[HttpCharset]] instances to form a [[ContentType]]. + * + * 3. WithFixedCharset: Some character-based MediaTypes prescribe a single, clearly defined charset and as such, + * similarly to binary MediaTypes, do not require the addition of an [[HttpCharset]] instances to form a + * [[ContentType]]. The most prominent example is probably `application/json` which must always be UTF-8 encoded. + * Like binary MediaTypes `WithFixedCharset` types can be implicitly converted to a [[ContentType]]. + */ +sealed abstract class MediaType extends jm.MediaType with LazyValueBytesRenderable with WithQValue[MediaRange] { + + def fileExtensions: List[String] def params: Map[String, String] - def qValue: Float - def matches(mediaType: MediaType): Boolean - def isApplication = false - def isAudio = false - def isImage = false - def isMessage = false - def isMultipart = false - def isText = false - def isVideo = false - def isWildcard = mainType == "*" - /** - * Returns a copy of this instance with the params replaced by the given ones. - * If the given map contains a "q" value the `qValue` member is (also) updated. - */ - def withParams(params: Map[String, String]): MediaRange + override def isApplication: Boolean = false + override def isAudio: Boolean = false + override def isImage: Boolean = false + override def isMessage: Boolean = false + override def isMultipart: Boolean = false + override def isText: Boolean = false + override def isVideo: Boolean = false - /** - * Constructs a `ContentTypeRange` from this instance and the given charset. - */ - def withCharset(charsetRange: HttpCharsetRange): ContentTypeRange = ContentTypeRange(this, charsetRange) - - /** - * Returns a [[MediaType]] instance which fits this range. - */ - def specimen: MediaType - - /** Java API */ - def getParams: util.Map[String, String] = { - import collection.JavaConverters._ - params.asJava - } - /** Java API */ - def matches(mediaType: jm.MediaType): Boolean = { - import akka.http.impl.util.JavaMapping.Implicits._ - matches(mediaType.asScala) - } -} - -object MediaRange { - private[http] def splitOffQValue(params: Map[String, String], defaultQ: Float = 1.0f): (Map[String, String], Float) = - params.get("q") match { - case Some(x) ⇒ (params - "q") -> (try x.toFloat catch { case _: NumberFormatException ⇒ 1.0f }) - case None ⇒ params -> defaultQ - } - - private final case class Custom(mainType: String, params: Map[String, String], qValue: Float) - extends MediaRange with ValueRenderable { - require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") - def matches(mediaType: MediaType) = mainType == "*" || mediaType.mainType == mainType - def withParams(params: Map[String, String]) = custom(mainType, params, qValue) - def withQValue(qValue: Float) = if (qValue != this.qValue) custom(mainType, params, qValue) else this - def render[R <: Rendering](r: R): r.type = { - r ~~ mainType ~~ '/' ~~ '*' - if (qValue < 1.0f) r ~~ ";q=" ~~ qValue - if (params.nonEmpty) params foreach { case (k, v) ⇒ r ~~ ';' ~~ ' ' ~~ k ~~ '=' ~~# v } - r - } - override def isApplication = mainType == "application" - override def isAudio = mainType == "audio" - override def isImage = mainType == "image" - override def isMessage = mainType == "message" - override def isMultipart = mainType == "multipart" - override def isText = mainType == "text" - override def isVideo = mainType == "video" - def specimen = MediaType.custom(mainType, "custom", MediaType.Encoding.Binary) - } - - def custom(mainType: String, params: Map[String, String] = Map.empty, qValue: Float = 1.0f): MediaRange = { - val (ps, q) = splitOffQValue(params, qValue) - Custom(mainType.toRootLowerCase, ps, q) - } - - final case class One(mediaType: MediaType, qValue: Float) extends MediaRange with ValueRenderable { - require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") - def mainType = mediaType.mainType - def params = mediaType.params - override def isApplication = mediaType.isApplication - override def isAudio = mediaType.isAudio - override def isImage = mediaType.isImage - override def isMessage = mediaType.isMessage - override def isMultipart = mediaType.isMultipart - override def isText = mediaType.isText - override def isVideo = mediaType.isVideo - def matches(mediaType: MediaType) = - this.mediaType.mainType == mediaType.mainType && this.mediaType.subType == mediaType.subType - def withParams(params: Map[String, String]) = copy(mediaType = mediaType.withParams(params)) - def withQValue(qValue: Float) = copy(qValue = qValue) - def render[R <: Rendering](r: R): r.type = if (qValue < 1.0f) r ~~ mediaType ~~ ";q=" ~~ qValue else r ~~ mediaType - def specimen = mediaType - } - - implicit def apply(mediaType: MediaType): MediaRange = apply(mediaType, 1.0f) - def apply(mediaType: MediaType, qValue: Float = 1.0f): MediaRange = One(mediaType, qValue) -} - -object MediaRanges extends ObjectRegistry[String, MediaRange] { - - sealed abstract case class PredefinedMediaRange(value: String) extends MediaRange with LazyValueBytesRenderable { - val mainType = value takeWhile (_ != '/') - register(mainType, this) - def params = Map.empty - def qValue = 1.0f - def withParams(params: Map[String, String]) = MediaRange.custom(mainType, params) - def withQValue(qValue: Float) = if (qValue != 1.0f) MediaRange.custom(mainType, params, qValue) else this - } - - val `*/*` = new PredefinedMediaRange("*/*") { - def matches(mediaType: MediaType) = true - def specimen = MediaTypes.`text/plain` - } - val `*/*;q=MIN` = `*/*`.withQValue(Float.MinPositiveValue) - val `application/*` = new PredefinedMediaRange("application/*") { - def matches(mediaType: MediaType) = mediaType.isApplication - override def isApplication = true - def specimen = MediaTypes.`application/json` - } - val `audio/*` = new PredefinedMediaRange("audio/*") { - def matches(mediaType: MediaType) = mediaType.isAudio - override def isAudio = true - def specimen = MediaTypes.`audio/ogg` - } - val `image/*` = new PredefinedMediaRange("image/*") { - def matches(mediaType: MediaType) = mediaType.isImage - override def isImage = true - def specimen = MediaTypes.`image/png` - } - val `message/*` = new PredefinedMediaRange("message/*") { - def matches(mediaType: MediaType) = mediaType.isMessage - override def isMessage = true - def specimen = MediaTypes.`message/rfc822` - } - val `multipart/*` = new PredefinedMediaRange("multipart/*") { - def matches(mediaType: MediaType) = mediaType.isMultipart - override def isMultipart = true - def specimen = MediaTypes.`multipart/form-data` - } - val `text/*` = new PredefinedMediaRange("text/*") { - def matches(mediaType: MediaType) = mediaType.isText - override def isText = true - def specimen = MediaTypes.`text/plain` - } - val `video/*` = new PredefinedMediaRange("video/*") { - def matches(mediaType: MediaType) = mediaType.isVideo - override def isVideo = true - def specimen = MediaTypes.`video/mp4` - } -} - -sealed abstract case class MediaType private[http] (value: String)(val mainType: String, - val subType: String, - val compressible: Boolean, - val encoding: MediaType.Encoding, - val fileExtensions: immutable.Seq[String], - val params: Map[String, String]) - extends jm.MediaType with LazyValueBytesRenderable with WithQValue[MediaRange] { - def isApplication = false - def isAudio = false - def isImage = false - def isMessage = false - def isMultipart = false - def isText = false - def isVideo = false - - /** - * Returns a copy of this instance with the params replaced by the given ones. - */ def withParams(params: Map[String, String]): MediaType - /** - * Constructs a `ContentType` from this instance and the given charset. - */ - def withCharset(charset: HttpCharset): ContentType = ContentType(this, charset) - def withQValue(qValue: Float): MediaRange = MediaRange(this, qValue.toFloat) -} -class MultipartMediaType private[http] (_value: String, _subType: String, _params: Map[String, String]) - extends MediaType(_value)("multipart", _subType, compressible = true, encoding = MediaType.Encoding.Open, Nil, _params) { - override def isMultipart = true - def withBoundary(boundary: String): MultipartMediaType = withParams { - if (boundary.isEmpty) params - "boundary" else params.updated("boundary", boundary) - } - def withParams(params: Map[String, String]) = MediaTypes.multipart(subType, params) -} + override def equals(that: Any): Boolean = + that match { + case x: MediaType ⇒ value equalsIgnoreCase x.value + case _ ⇒ false + } -sealed abstract class NonMultipartMediaType private[http] (_value: String, _mainType: String, _subType: String, - _compressible: Boolean, _encoding: MediaType.Encoding, - _fileExtensions: immutable.Seq[String], - _params: Map[String, String]) - extends MediaType(_value)(_mainType, _subType, _compressible, _encoding, _fileExtensions, _params) { - private[http] def this(mainType: String, subType: String, compressible: Boolean, encoding: MediaType.Encoding, - fileExtensions: immutable.Seq[String]) = - this(mainType + '/' + subType, mainType, subType, compressible, encoding, fileExtensions, Map.empty) - def withParams(params: Map[String, String]) = - MediaType.custom(mainType, subType, encoding, compressible, fileExtensions, params) + override def hashCode(): Int = value.hashCode + + /** + * JAVA API + */ + def toRange = jm.MediaRanges.create(this) + def toRange(qValue: Float) = jm.MediaRanges.create(this, qValue) } object MediaType { - sealed abstract class Encoding(val charset: Option[HttpCharset]) - object Encoding { - /** - * Indicates that the media type is non-textual and a character encoding therefore has no meaning. - */ - case object Binary extends Encoding(None) - /** - * Indicates that the media-type allow for flexible character encoding through a `charset` parameter. - */ - case object Open extends Encoding(None) + def applicationBinary(subType: String, compressible: Boolean, fileExtensions: String*): Binary = + new Binary("application/" + subType, "application", subType, compressible, fileExtensions.toList) { + override def isApplication = true + } - /** - * Indicates that a media-type is textual and mandates a clearly defined character encoding. - */ - final case class Fixed(cs: HttpCharset) extends Encoding(Some(cs)) - } + def applicationWithFixedCharset(subType: String, charset: HttpCharset, + fileExtensions: String*): WithFixedCharset = + new WithFixedCharset("application/" + subType, "application", subType, charset, fileExtensions.toList) { + override def isApplication = true + } - /** - * Create a custom media type. - */ - def custom(mainType: String, subType: String, encoding: MediaType.Encoding, - compressible: Boolean = false, fileExtensions: immutable.Seq[String] = Nil, - params: Map[String, String] = Map.empty, allowArbitrarySubtypes: Boolean = false): MediaType = { - require(mainType != "multipart", "Cannot create a MultipartMediaType here, use `multipart.apply` instead!") + def applicationWithOpenCharset(subType: String, fileExtensions: String*): WithOpenCharset = + new NonMultipartWithOpenCharset("application/" + subType, "application", subType, fileExtensions.toList) { + override def isApplication = true + } + + def audio(subType: String, compressible: Boolean, fileExtensions: String*): Binary = + new Binary("audio/" + subType, "audio", subType, compressible, fileExtensions.toList) { + override def isAudio = true + } + + def image(subType: String, compressible: Boolean, fileExtensions: String*): Binary = + new Binary("image/" + subType, "image", subType, compressible, fileExtensions.toList) { + override def isImage = true + } + + def message(subType: String, compressible: Boolean, fileExtensions: String*): Binary = + new Binary("message/" + subType, "message", subType, compressible, fileExtensions.toList) { + override def isMessage = true + } + + def text(subType: String, fileExtensions: String*): WithOpenCharset = + new NonMultipartWithOpenCharset("text/" + subType, "text", subType, fileExtensions.toList) { + override def isText = true + } + + def video(subType: String, compressible: Boolean, fileExtensions: String*): Binary = + new Binary("video/" + subType, "video", subType, compressible, fileExtensions.toList) { + override def isVideo = true + } + + def customBinary(mainType: String, subType: String, compressible: Boolean, fileExtensions: List[String] = Nil, + params: Map[String, String] = Map.empty, allowArbitrarySubtypes: Boolean = false): Binary = { + require(mainType != "multipart", "Cannot create a MediaType.Multipart here, use `customMultipart` instead!") require(allowArbitrarySubtypes || subType != "*", "Cannot create a MediaRange here, use `MediaRange.custom` instead!") - val r = new StringRendering ~~ mainType ~~ '/' ~~ subType - if (params.nonEmpty) params foreach { case (k, v) ⇒ r ~~ ';' ~~ ' ' ~~ k ~~ '=' ~~# v } - new NonMultipartMediaType(r.get, mainType, subType, compressible, encoding, fileExtensions, params) { + val _params = params + new Binary(renderValue(mainType, subType, params), mainType, subType, compressible, fileExtensions) { + override def params = _params override def isApplication = mainType == "application" override def isAudio = mainType == "audio" override def isImage = mainType == "image" @@ -251,26 +123,135 @@ object MediaType { } } - def custom(value: String, encoding: MediaType.Encoding): MediaType = { + def customWithFixedCharset(mainType: String, subType: String, charset: HttpCharset, fileExtensions: List[String] = Nil, + params: Map[String, String] = Map.empty, + allowArbitrarySubtypes: Boolean = false): WithFixedCharset = { + require(mainType != "multipart", "Cannot create a MediaType.Multipart here, use `customMultipart` instead!") + require(allowArbitrarySubtypes || subType != "*", "Cannot create a MediaRange here, use `MediaRange.custom` instead!") + val _params = params + new WithFixedCharset(renderValue(mainType, subType, params), mainType, subType, charset, fileExtensions) { + override def params = _params + override def isApplication = mainType == "application" + override def isAudio = mainType == "audio" + override def isImage = mainType == "image" + override def isMessage = mainType == "message" + override def isText = mainType == "text" + override def isVideo = mainType == "video" + } + } + + def customWithOpenCharset(mainType: String, subType: String, fileExtensions: List[String] = Nil, + params: Map[String, String] = Map.empty, + allowArbitrarySubtypes: Boolean = false): WithOpenCharset = { + require(mainType != "multipart", "Cannot create a MediaType.Multipart here, use `customMultipart` instead!") + require(allowArbitrarySubtypes || subType != "*", "Cannot create a MediaRange here, use `MediaRange.custom` instead!") + val _params = params + new NonMultipartWithOpenCharset(renderValue(mainType, subType, params), mainType, subType, fileExtensions) { + override def params = _params + override def isApplication = mainType == "application" + override def isAudio = mainType == "audio" + override def isImage = mainType == "image" + override def isMessage = mainType == "message" + override def isText = mainType == "text" + override def isVideo = mainType == "video" + } + } + + def customMultipart(subType: String, params: Map[String, String]): Multipart = { + require(subType != "*", "Cannot create a MediaRange here, use MediaRanges.`multipart/*` instead!") + new Multipart(subType, params) + } + + def custom(value: String, binary: Boolean, compressible: Boolean = true, + fileExtensions: List[String] = Nil): MediaType = { val parts = value.split('/') - if (parts.length != 2) throw new IllegalArgumentException(value + " is not a valid media-type") - custom(parts(0), parts(1), encoding) + require(parts.length == 2, s"`$value` is not a valid media-type. It must consist of two parts separated by '/'.") + if (binary) customBinary(parts(0), parts(1), compressible, fileExtensions) + else customWithOpenCharset(parts(0), parts(1), fileExtensions) } /** - * Tries to parse a ``MediaType`` value from the given String. Returns ``Right(mediaType)`` if successful and - * ``Left(errors)`` otherwise. + * Tries to parse a ``MediaType`` value from the given String. + * Returns ``Right(mediaType)`` if successful and ``Left(errors)`` otherwise. */ def parse(value: String): Either[List[ErrorInfo], MediaType] = ContentType.parse(value).right.map(_.mediaType) + + def unapply(mediaType: MediaType): Option[String] = Some(mediaType.value) + + ///////////////////////////////////////////////////////////////////////// + + private def renderValue(mainType: String, subType: String, params: Map[String, String]): String = { + val r = new StringRendering ~~ mainType ~~ '/' ~~ subType + if (params.nonEmpty) params foreach { case (k, v) ⇒ r ~~ ';' ~~ ' ' ~~ k ~~ '=' ~~# v } + r.get + } + + sealed abstract class Binary(val value: String, val mainType: String, val subType: String, val compressible: Boolean, + val fileExtensions: List[String]) extends MediaType with jm.MediaType.Binary { + def binary = true + def params: Map[String, String] = Map.empty + def withParams(params: Map[String, String]): Binary with MediaType = + customBinary(mainType, subType, compressible, fileExtensions, params) + + /** + * JAVA API + */ + def toContentType: ContentType.Binary = ContentType(this) + } + + sealed abstract class NonBinary extends MediaType with jm.MediaType.NonBinary { + def binary = false + def compressible = true + } + + sealed abstract class WithFixedCharset(val value: String, val mainType: String, val subType: String, + val charset: HttpCharset, val fileExtensions: List[String]) + extends NonBinary with jm.MediaType.WithFixedCharset { + def params: Map[String, String] = Map.empty + def withParams(params: Map[String, String]): WithFixedCharset with MediaType = + customWithFixedCharset(mainType, subType, charset, fileExtensions, params) + + /** + * JAVA API + */ + def toContentType: ContentType.WithFixedCharset = ContentType(this) + } + + sealed abstract class WithOpenCharset extends NonBinary with jm.MediaType.WithOpenCharset { + def withCharset(charset: HttpCharset): ContentType.WithCharset = ContentType(this, charset) + + /** + * JAVA API + */ + def toContentType(charset: jm.HttpCharset): ContentType.WithCharset = withCharset(charset.asScala) + } + + sealed abstract class NonMultipartWithOpenCharset(val value: String, val mainType: String, val subType: String, + val fileExtensions: List[String]) extends WithOpenCharset { + def params: Map[String, String] = Map.empty + def withParams(params: Map[String, String]): WithOpenCharset with MediaType = + customWithOpenCharset(mainType, subType, fileExtensions, params) + } + + final class Multipart(val subType: String, val params: Map[String, String]) + extends WithOpenCharset with jm.MediaType.Multipart { + val value = renderValue(mainType, subType, params) + override def mainType = "multipart" + override def isMultipart = true + override def fileExtensions = Nil + def withParams(params: Map[String, String]): MediaType.Multipart = new MediaType.Multipart(subType, params) + def withBoundary(boundary: String): MediaType.Multipart = + withParams(if (boundary.isEmpty) params - "boundary" else params.updated("boundary", boundary)) + } } object MediaTypes extends ObjectRegistry[(String, String), MediaType] { - import MediaType.Encoding - private[this] var extensionMap = Map.empty[String, MediaType] - private def register(mediaType: MediaType): MediaType = { + def forExtension(ext: String): Option[MediaType] = extensionMap.get(ext.toRootLowerCase) + + private def register[T <: MediaType](mediaType: T): T = { def registerFileExtension(ext: String): Unit = { val lcExt = ext.toRootLowerCase require(!extensionMap.contains(lcExt), s"Extension '$ext' clash: media-types '${extensionMap(lcExt)}' and '$mediaType'") @@ -280,120 +261,95 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] { register(mediaType.mainType.toRootLowerCase -> mediaType.subType.toRootLowerCase, mediaType) } - def forExtension(ext: String): Option[MediaType] = extensionMap.get(ext.toRootLowerCase) - - private def app(subType: String, compressible: Boolean, encoding: Encoding, fileExtensions: String*) = register { - new NonMultipartMediaType("application", subType, compressible, encoding, immutable.Seq(fileExtensions: _*)) { - override def isApplication = true - } - } - private def aud(subType: String, compressible: Boolean, fileExtensions: String*) = register { - new NonMultipartMediaType("audio", subType, compressible, encoding = Encoding.Binary, immutable.Seq(fileExtensions: _*)) { - override def isAudio = true - } - } - private def img(subType: String, compressible: Boolean, encoding: Encoding, fileExtensions: String*) = register { - new NonMultipartMediaType("image", subType, compressible, encoding, immutable.Seq(fileExtensions: _*)) { - override def isImage = true - } - } - private def msg(subType: String, fileExtensions: String*) = register { - new NonMultipartMediaType("message", subType, compressible = true, encoding = Encoding.Binary, - immutable.Seq(fileExtensions: _*)) { - override def isMessage = true - } - } - private def txt(subType: String, fileExtensions: String*) = register { - new NonMultipartMediaType("text", subType, compressible = true, encoding = Encoding.Open, immutable.Seq(fileExtensions: _*)) { - override def isText = true - } - } - private def vid(subType: String, fileExtensions: String*) = register { - new NonMultipartMediaType("video", subType, compressible = false, encoding = Encoding.Binary, immutable.Seq(fileExtensions: _*)) { - override def isVideo = true - } - } - + import MediaType._ + /////////////////////////// PREDEFINED MEDIA-TYPE DEFINITION //////////////////////////// // format: OFF private final val compressible = true // compile-time constant private final val uncompressible = false // compile-time constant - private def binary = Encoding.Binary - private def openEncoding = Encoding.Open + + private def abin(st: String, c: Boolean, fe: String*) = register(applicationBinary(st, c, fe: _*)) + private def awfc(st: String, cs: HttpCharset, fe: String*) = register(applicationWithFixedCharset(st, cs, fe: _*)) + private def awoc(st: String, fe: String*) = register(applicationWithOpenCharset(st, fe: _*)) + private def aud(st: String, c: Boolean, fe: String*) = register(audio(st, c, fe: _*)) + private def img(st: String, c: Boolean, fe: String*) = register(image(st, c, fe: _*)) + private def msg(st: String, fe: String*) = register(message(st, compressible, fe: _*)) + private def txt(st: String, fe: String*) = register(text(st, fe: _*)) + private def vid(st: String, fe: String*) = register(video(st, compressible, fe: _*)) // dummy value currently only used by ContentType.NoContentType - private[http] val NoMediaType = new NonMultipartMediaType("none", "none", false, Encoding.Binary, immutable.Seq.empty) {} + private[http] val NoMediaType = MediaType.customBinary("none", "none", compressible = false) - val `application/atom+xml` = app("atom+xml", compressible, openEncoding, "atom") - val `application/base64` = app("base64", compressible, binary, "mm", "mme") - val `application/excel` = app("excel", uncompressible, binary, "xl", "xla", "xlb", "xlc", "xld", "xlk", "xll", "xlm", "xls", "xlt", "xlv", "xlw") - val `application/font-woff` = app("font-woff", uncompressible, binary, "woff") - val `application/gnutar` = app("gnutar", uncompressible, binary, "tgz") - val `application/java-archive` = app("java-archive", uncompressible, binary, "jar", "war", "ear") - val `application/javascript` = app("javascript", compressible, openEncoding, "js") - val `application/json` = app("json", compressible, Encoding.Fixed(HttpCharsets.`UTF-8`), "json") - val `application/json-patch+json` = app("json-patch+json", compressible, Encoding.Fixed(HttpCharsets.`UTF-8`)) - val `application/lha` = app("lha", uncompressible, binary, "lha") - val `application/lzx` = app("lzx", uncompressible, binary, "lzx") - val `application/mspowerpoint` = app("mspowerpoint", uncompressible, binary, "pot", "pps", "ppt", "ppz") - val `application/msword` = app("msword", uncompressible, binary, "doc", "dot", "w6w", "wiz", "word", "wri") - val `application/octet-stream` = app("octet-stream", uncompressible, binary, "a", "bin", "class", "dump", "exe", "lhx", "lzh", "o", "psd", "saveme", "zoo") - val `application/pdf` = app("pdf", uncompressible, binary, "pdf") - val `application/postscript` = app("postscript", compressible, binary, "ai", "eps", "ps") - val `application/rss+xml` = app("rss+xml", compressible, openEncoding, "rss") - val `application/soap+xml` = app("soap+xml", compressible, openEncoding) - val `application/vnd.api+json` = app("vnd.api+json", compressible, Encoding.Fixed(HttpCharsets.`UTF-8`)) - val `application/vnd.google-earth.kml+xml` = app("vnd.google-earth.kml+xml", compressible, openEncoding, "kml") - val `application/vnd.google-earth.kmz` = app("vnd.google-earth.kmz", uncompressible, binary, "kmz") - val `application/vnd.ms-fontobject` = app("vnd.ms-fontobject", compressible, binary, "eot") - val `application/vnd.oasis.opendocument.chart` = app("vnd.oasis.opendocument.chart", compressible, binary, "odc") - val `application/vnd.oasis.opendocument.database` = app("vnd.oasis.opendocument.database", compressible, binary, "odb") - val `application/vnd.oasis.opendocument.formula` = app("vnd.oasis.opendocument.formula", compressible, binary, "odf") - val `application/vnd.oasis.opendocument.graphics` = app("vnd.oasis.opendocument.graphics", compressible, binary, "odg") - val `application/vnd.oasis.opendocument.image` = app("vnd.oasis.opendocument.image", compressible, binary, "odi") - val `application/vnd.oasis.opendocument.presentation` = app("vnd.oasis.opendocument.presentation", compressible, binary, "odp") - val `application/vnd.oasis.opendocument.spreadsheet` = app("vnd.oasis.opendocument.spreadsheet", compressible, binary, "ods") - val `application/vnd.oasis.opendocument.text` = app("vnd.oasis.opendocument.text", compressible, binary, "odt") - val `application/vnd.oasis.opendocument.text-master` = app("vnd.oasis.opendocument.text-master", compressible, binary, "odm", "otm") - val `application/vnd.oasis.opendocument.text-web` = app("vnd.oasis.opendocument.text-web", compressible, binary, "oth") - val `application/vnd.openxmlformats-officedocument.presentationml.presentation` = app("vnd.openxmlformats-officedocument.presentationml.presentation", compressible, binary, "pptx") - val `application/vnd.openxmlformats-officedocument.presentationml.slide` = app("vnd.openxmlformats-officedocument.presentationml.slide", compressible, binary, "sldx") - val `application/vnd.openxmlformats-officedocument.presentationml.slideshow` = app("vnd.openxmlformats-officedocument.presentationml.slideshow", compressible, binary, "ppsx") - val `application/vnd.openxmlformats-officedocument.presentationml.template` = app("vnd.openxmlformats-officedocument.presentationml.template", compressible, binary, "potx") - val `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` = app("vnd.openxmlformats-officedocument.spreadsheetml.sheet", compressible, binary, "xlsx") - val `application/vnd.openxmlformats-officedocument.spreadsheetml.template` = app("vnd.openxmlformats-officedocument.spreadsheetml.template", compressible, binary, "xltx") - val `application/vnd.openxmlformats-officedocument.wordprocessingml.document` = app("vnd.openxmlformats-officedocument.wordprocessingml.document", compressible, binary, "docx") - val `application/vnd.openxmlformats-officedocument.wordprocessingml.template` = app("vnd.openxmlformats-officedocument.wordprocessingml.template", compressible, binary, "dotx") - val `application/x-7z-compressed` = app("x-7z-compressed", uncompressible, binary, "7z", "s7z") - val `application/x-ace-compressed` = app("x-ace-compressed", uncompressible, binary, "ace") - val `application/x-apple-diskimage` = app("x-apple-diskimage", uncompressible, binary, "dmg") - val `application/x-arc-compressed` = app("x-arc-compressed", uncompressible, binary, "arc") - val `application/x-bzip` = app("x-bzip", uncompressible, binary, "bz") - val `application/x-bzip2` = app("x-bzip2", uncompressible, binary, "boz", "bz2") - val `application/x-chrome-extension` = app("x-chrome-extension", uncompressible, binary, "crx") - val `application/x-compress` = app("x-compress", uncompressible, binary, "z") - val `application/x-compressed` = app("x-compressed", uncompressible, binary, "gz") - val `application/x-debian-package` = app("x-debian-package", compressible, binary, "deb") - val `application/x-dvi` = app("x-dvi", compressible, binary, "dvi") - val `application/x-font-truetype` = app("x-font-truetype", compressible, binary, "ttf") - val `application/x-font-opentype` = app("x-font-opentype", compressible, binary, "otf") - val `application/x-gtar` = app("x-gtar", uncompressible, binary, "gtar") - val `application/x-gzip` = app("x-gzip", uncompressible, binary, "gzip") - val `application/x-latex` = app("x-latex", compressible, binary, "latex", "ltx") - val `application/x-rar-compressed` = app("x-rar-compressed", uncompressible, binary, "rar") - val `application/x-redhat-package-manager` = app("x-redhat-package-manager", uncompressible, binary, "rpm") - val `application/x-shockwave-flash` = app("x-shockwave-flash", uncompressible, binary, "swf") - val `application/x-tar` = app("x-tar", compressible, binary, "tar") - val `application/x-tex` = app("x-tex", compressible, binary, "tex") - val `application/x-texinfo` = app("x-texinfo", compressible, binary, "texi", "texinfo") - val `application/x-vrml` = app("x-vrml", compressible, openEncoding, "vrml") - val `application/x-www-form-urlencoded` = app("x-www-form-urlencoded", compressible, openEncoding) - val `application/x-x509-ca-cert` = app("x-x509-ca-cert", compressible, binary, "der") - val `application/x-xpinstall` = app("x-xpinstall", uncompressible, binary, "xpi") - val `application/xhtml+xml` = app("xhtml+xml", compressible, openEncoding) - val `application/xml-dtd` = app("xml-dtd", compressible, openEncoding) - val `application/xml` = app("xml", compressible, openEncoding) - val `application/zip` = app("zip", uncompressible, binary, "zip") + val `application/atom+xml` = awoc("atom+xml", "atom") + val `application/base64` = awoc("base64", "mm", "mme") + val `application/excel` = abin("excel", uncompressible, "xl", "xla", "xlb", "xlc", "xld", "xlk", "xll", "xlm", "xls", "xlt", "xlv", "xlw") + val `application/font-woff` = abin("font-woff", uncompressible, "woff") + val `application/gnutar` = abin("gnutar", uncompressible, "tgz") + val `application/java-archive` = abin("java-archive", uncompressible, "jar", "war", "ear") + val `application/javascript` = awoc("javascript", "js") + val `application/json` = awfc("json", HttpCharsets.`UTF-8`, "json") + val `application/json-patch+json` = awfc("json-patch+json", HttpCharsets.`UTF-8`) + val `application/lha` = abin("lha", uncompressible, "lha") + val `application/lzx` = abin("lzx", uncompressible, "lzx") + val `application/mspowerpoint` = abin("mspowerpoint", uncompressible, "pot", "pps", "ppt", "ppz") + val `application/msword` = abin("msword", uncompressible, "doc", "dot", "w6w", "wiz", "word", "wri") + val `application/octet-stream` = abin("octet-stream", uncompressible, "a", "bin", "class", "dump", "exe", "lhx", "lzh", "o", "psd", "saveme", "zoo") + val `application/pdf` = abin("pdf", uncompressible, "pdf") + val `application/postscript` = abin("postscript", compressible, "ai", "eps", "ps") + val `application/rss+xml` = awoc("rss+xml", "rss") + val `application/soap+xml` = awoc("soap+xml") + val `application/vnd.api+json` = awfc("vnd.api+json", HttpCharsets.`UTF-8`) + val `application/vnd.google-earth.kml+xml` = awoc("vnd.google-earth.kml+xml", "kml") + val `application/vnd.google-earth.kmz` = abin("vnd.google-earth.kmz", uncompressible, "kmz") + val `application/vnd.ms-fontobject` = abin("vnd.ms-fontobject", compressible, "eot") + val `application/vnd.oasis.opendocument.chart` = abin("vnd.oasis.opendocument.chart", compressible, "odc") + val `application/vnd.oasis.opendocument.database` = abin("vnd.oasis.opendocument.database", compressible, "odb") + val `application/vnd.oasis.opendocument.formula` = abin("vnd.oasis.opendocument.formula", compressible, "odf") + val `application/vnd.oasis.opendocument.graphics` = abin("vnd.oasis.opendocument.graphics", compressible, "odg") + val `application/vnd.oasis.opendocument.image` = abin("vnd.oasis.opendocument.image", compressible, "odi") + val `application/vnd.oasis.opendocument.presentation` = abin("vnd.oasis.opendocument.presentation", compressible, "odp") + val `application/vnd.oasis.opendocument.spreadsheet` = abin("vnd.oasis.opendocument.spreadsheet", compressible, "ods") + val `application/vnd.oasis.opendocument.text` = abin("vnd.oasis.opendocument.text", compressible, "odt") + val `application/vnd.oasis.opendocument.text-master` = abin("vnd.oasis.opendocument.text-master", compressible, "odm", "otm") + val `application/vnd.oasis.opendocument.text-web` = abin("vnd.oasis.opendocument.text-web", compressible, "oth") + val `application/vnd.openxmlformats-officedocument.presentationml.presentation` = abin("vnd.openxmlformats-officedocument.presentationml.presentation", compressible, "pptx") + val `application/vnd.openxmlformats-officedocument.presentationml.slide` = abin("vnd.openxmlformats-officedocument.presentationml.slide", compressible, "sldx") + val `application/vnd.openxmlformats-officedocument.presentationml.slideshow` = abin("vnd.openxmlformats-officedocument.presentationml.slideshow", compressible, "ppsx") + val `application/vnd.openxmlformats-officedocument.presentationml.template` = abin("vnd.openxmlformats-officedocument.presentationml.template", compressible, "potx") + val `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` = abin("vnd.openxmlformats-officedocument.spreadsheetml.sheet", compressible, "xlsx") + val `application/vnd.openxmlformats-officedocument.spreadsheetml.template` = abin("vnd.openxmlformats-officedocument.spreadsheetml.template", compressible, "xltx") + val `application/vnd.openxmlformats-officedocument.wordprocessingml.document` = abin("vnd.openxmlformats-officedocument.wordprocessingml.document", compressible, "docx") + val `application/vnd.openxmlformats-officedocument.wordprocessingml.template` = abin("vnd.openxmlformats-officedocument.wordprocessingml.template", compressible, "dotx") + val `application/x-7z-compressed` = abin("x-7z-compressed", uncompressible, "7z", "s7z") + val `application/x-ace-compressed` = abin("x-ace-compressed", uncompressible, "ace") + val `application/x-apple-diskimage` = abin("x-apple-diskimage", uncompressible, "dmg") + val `application/x-arc-compressed` = abin("x-arc-compressed", uncompressible, "arc") + val `application/x-bzip` = abin("x-bzip", uncompressible, "bz") + val `application/x-bzip2` = abin("x-bzip2", uncompressible, "boz", "bz2") + val `application/x-chrome-extension` = abin("x-chrome-extension", uncompressible, "crx") + val `application/x-compress` = abin("x-compress", uncompressible, "z") + val `application/x-compressed` = abin("x-compressed", uncompressible, "gz") + val `application/x-debian-package` = abin("x-debian-package", compressible, "deb") + val `application/x-dvi` = abin("x-dvi", compressible, "dvi") + val `application/x-font-truetype` = abin("x-font-truetype", compressible, "ttf") + val `application/x-font-opentype` = abin("x-font-opentype", compressible, "otf") + val `application/x-gtar` = abin("x-gtar", uncompressible, "gtar") + val `application/x-gzip` = abin("x-gzip", uncompressible, "gzip") + val `application/x-latex` = awoc("x-latex", "latex", "ltx") + val `application/x-rar-compressed` = abin("x-rar-compressed", uncompressible, "rar") + val `application/x-redhat-package-manager` = abin("x-redhat-package-manager", uncompressible, "rpm") + val `application/x-shockwave-flash` = abin("x-shockwave-flash", uncompressible, "swf") + val `application/x-tar` = abin("x-tar", compressible, "tar") + val `application/x-tex` = abin("x-tex", compressible, "tex") + val `application/x-texinfo` = abin("x-texinfo", compressible, "texi", "texinfo") + val `application/x-vrml` = awoc("x-vrml", "vrml") + val `application/x-www-form-urlencoded` = awoc("x-www-form-urlencoded") + val `application/x-x509-ca-cert` = abin("x-x509-ca-cert", compressible, "der") + val `application/x-xpinstall` = abin("x-xpinstall", uncompressible, "xpi") + val `application/xhtml+xml` = awoc("xhtml+xml") + val `application/xml-dtd` = awoc("xml-dtd") + val `application/xml` = awoc("xml") + val `application/zip` = abin("zip", uncompressible, "zip") val `audio/aiff` = aud("aiff", compressible, "aif", "aifc", "aiff") val `audio/basic` = aud("basic", compressible, "au", "snd") @@ -410,40 +366,34 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] { val `audio/xm` = aud("xm", uncompressible, "xm") val `audio/webm` = aud("webm", uncompressible) - val `image/gif` = img("gif", uncompressible, binary, "gif") - val `image/jpeg` = img("jpeg", uncompressible, binary, "jpe", "jpeg", "jpg") - val `image/pict` = img("pict", compressible, binary, "pic", "pict") - val `image/png` = img("png", uncompressible, binary, "png") - val `image/svg+xml` = img("svg+xml", compressible, openEncoding, "svg") - val `image/tiff` = img("tiff", compressible, binary, "tif", "tiff") - val `image/x-icon` = img("x-icon", compressible, binary, "ico") - val `image/x-ms-bmp` = img("x-ms-bmp", compressible, binary, "bmp") - val `image/x-pcx` = img("x-pcx", compressible, binary, "pcx") - val `image/x-pict` = img("x-pict", compressible, binary, "pct") - val `image/x-quicktime` = img("x-quicktime", uncompressible, binary, "qif", "qti", "qtif") - val `image/x-rgb` = img("x-rgb", compressible, binary, "rgb") - val `image/x-xbitmap` = img("x-xbitmap", compressible, binary, "xbm") - val `image/x-xpixmap` = img("x-xpixmap", compressible, binary, "xpm") - val `image/webp` = img("webp", uncompressible, binary, "webp") + val `image/gif` = img("gif", uncompressible, "gif") + val `image/jpeg` = img("jpeg", uncompressible, "jpe", "jpeg", "jpg") + val `image/pict` = img("pict", compressible, "pic", "pict") + val `image/png` = img("png", uncompressible, "png") + val `image/svg+xml` = img("svg+xml", compressible, "svg") + val `image/tiff` = img("tiff", compressible, "tif", "tiff") + val `image/x-icon` = img("x-icon", compressible, "ico") + val `image/x-ms-bmp` = img("x-ms-bmp", compressible, "bmp") + val `image/x-pcx` = img("x-pcx", compressible, "pcx") + val `image/x-pict` = img("x-pict", compressible, "pct") + val `image/x-quicktime` = img("x-quicktime", uncompressible, "qif", "qti", "qtif") + val `image/x-rgb` = img("x-rgb", compressible, "rgb") + val `image/x-xbitmap` = img("x-xbitmap", compressible, "xbm") + val `image/x-xpixmap` = img("x-xpixmap", compressible, "xpm") + val `image/webp` = img("webp", uncompressible, "webp") val `message/http` = msg("http") val `message/delivery-status` = msg("delivery-status") val `message/rfc822` = msg("rfc822", "eml", "mht", "mhtml", "mime") object multipart { - def apply(subType: String, params: Map[String, String]): MultipartMediaType = { - require(subType != "*", "Cannot create a MediaRange here, use MediaRanges.`multipart/*` instead!") - val r = new StringRendering ~~ "multipart/" ~~ subType - if (params.nonEmpty) params foreach { case (k, v) ⇒ r ~~ ';' ~~ ' ' ~~ k ~~ '=' ~~# v } - new MultipartMediaType(r.get, subType, params) - } - def mixed (params: Map[String, String]) = apply("mixed", params) - def alternative(params: Map[String, String]) = apply("alternative", params) - def related (params: Map[String, String]) = apply("related", params) - def `form-data`(params: Map[String, String]) = apply("form-data", params) - def signed (params: Map[String, String]) = apply("signed", params) - def encrypted (params: Map[String, String]) = apply("encrypted", params) - def byteRanges (params: Map[String, String]) = apply("byteranges", params) + def mixed (params: Map[String, String]) = new MediaType.Multipart("mixed", params) + def alternative(params: Map[String, String]) = new MediaType.Multipart("alternative", params) + def related (params: Map[String, String]) = new MediaType.Multipart("related", params) + def `form-data`(params: Map[String, String]) = new MediaType.Multipart("form-data", params) + def signed (params: Map[String, String]) = new MediaType.Multipart("signed", params) + def encrypted (params: Map[String, String]) = new MediaType.Multipart("encrypted", params) + def byteRanges (params: Map[String, String]) = new MediaType.Multipart("byteranges", params) } val `multipart/mixed` = multipart.mixed(Map.empty) diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/Multipart.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/Multipart.scala index 774aac7619..64537c4955 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/Multipart.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/Multipart.scala @@ -15,14 +15,14 @@ import scala.concurrent.{ Future, ExecutionContext } import scala.collection.immutable import scala.util.{ Failure, Success, Try } import akka.stream.Materializer -import akka.stream.scaladsl.{ Source } +import akka.stream.scaladsl.Source import akka.http.scaladsl.util.FastFuture import akka.http.scaladsl.model.headers._ import akka.http.impl.engine.rendering.BodyPartRenderer import FastFuture._ sealed trait Multipart { - def mediaType: MultipartMediaType + def mediaType: MediaType.Multipart def parts: Source[Multipart.BodyPart, Any] /** @@ -41,7 +41,7 @@ sealed trait Multipart { parts .transform(() ⇒ BodyPartRenderer.streamed(boundary, charset.nioCharset, partHeadersSizeHint = 128, log)) .flatMapConcat(ConstantFun.scalaIdentityFunction) - HttpEntity.Chunked(mediaType withBoundary boundary, chunks) + HttpEntity.Chunked(mediaType withBoundary boundary withCharset charset, chunks) } } @@ -52,7 +52,7 @@ object Multipart { override def toEntity(charset: HttpCharset, boundary: String)(implicit log: LoggingAdapter = NoLogging): HttpEntity.Strict = { val data = BodyPartRenderer.strict(strictParts, boundary, charset.nioCharset, partHeadersSizeHint = 128, log) - HttpEntity(mediaType withBoundary boundary, data) + HttpEntity(mediaType withBoundary boundary withCharset charset, data) } } @@ -91,27 +91,27 @@ object Multipart { * Basic model for multipart content as defined by http://tools.ietf.org/html/rfc2046. */ sealed abstract class General extends Multipart { - def mediaType: MultipartMediaType + def mediaType: MediaType.Multipart def parts: Source[General.BodyPart, Any] def toStrict(timeout: FiniteDuration)(implicit ec: ExecutionContext, fm: Materializer): Future[General.Strict] = strictify(parts)(_.toStrict(timeout)).fast.map(General.Strict(mediaType, _)) } object General { - def apply(mediaType: MultipartMediaType, parts: BodyPart.Strict*): Strict = Strict(mediaType, parts.toVector) + def apply(mediaType: MediaType.Multipart, parts: BodyPart.Strict*): Strict = Strict(mediaType, parts.toVector) - def apply(_mediaType: MultipartMediaType, _parts: Source[BodyPart, Any]): General = + def apply(_mediaType: MediaType.Multipart, _parts: Source[BodyPart, Any]): General = new General { def mediaType = _mediaType def parts = _parts override def toString = s"General($mediaType, $parts)" } - def unapply(value: General): Option[(MultipartMediaType, Source[BodyPart, Any])] = Some(value.mediaType -> value.parts) + def unapply(value: General): Option[(MediaType.Multipart, Source[BodyPart, Any])] = Some(value.mediaType -> value.parts) /** * Strict [[General]]. */ - case class Strict(mediaType: MultipartMediaType, strictParts: immutable.Seq[BodyPart.Strict]) extends General with Multipart.Strict { + case class Strict(mediaType: MediaType.Multipart, strictParts: immutable.Seq[BodyPart.Strict]) extends General with Multipart.Strict { def parts: Source[BodyPart.Strict, Any] = Source(strictParts) override def toStrict(timeout: FiniteDuration)(implicit ec: ExecutionContext, fm: Materializer) = FastFuture.successful(this) diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/HttpEncoding.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/HttpEncoding.scala index dc69e14158..2296e92069 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/HttpEncoding.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/HttpEncoding.scala @@ -22,7 +22,7 @@ object HttpEncodingRange { case class `*`(qValue: Float) extends HttpEncodingRange { require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") final def render[R <: Rendering](r: R): r.type = if (qValue < 1.0f) r ~~ "*;q=" ~~ qValue else r ~~ '*' - def matches(encoding: HttpEncoding) = qValue > 0f + def matches(encoding: HttpEncoding) = true def withQValue(qValue: Float) = if (qValue == 1.0f) `*` else if (qValue != this.qValue) `*`(qValue.toFloat) else this } @@ -30,7 +30,7 @@ object HttpEncodingRange { final case class One(encoding: HttpEncoding, qValue: Float) extends HttpEncodingRange { require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") - def matches(encoding: HttpEncoding) = qValue > 0f && this.encoding.value.equalsIgnoreCase(encoding.value) + def matches(encoding: HttpEncoding) = this.encoding.value.equalsIgnoreCase(encoding.value) def withQValue(qValue: Float) = One(encoding, qValue) def render[R <: Rendering](r: R): r.type = if (qValue < 1.0f) r ~~ encoding ~~ ";q=" ~~ qValue else r ~~ encoding } @@ -55,7 +55,6 @@ object HttpEncodings extends ObjectRegistry[String, HttpEncoding] { val deflate = register("deflate") val gzip = register("gzip") val identity = register("identity") - val `identity;q=MIN` = identity.withQValue(Float.MinPositiveValue) val `x-compress` = register("x-compress") val `x-zip` = register("x-zip") // format: ON diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala index 63b9225e18..a6050073a2 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala @@ -154,7 +154,7 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { |Content-Type: application/pdf |Content-Length: 0 | - |""" should parseTo(HttpRequest(GET, "/data", List(Host("x")), HttpEntity(`application/pdf`, ""))) + |""" should parseTo(HttpRequest(GET, "/data", List(Host("x")), HttpEntity.empty(`application/pdf`))) closeAfterResponseCompletion shouldEqual Seq(false) } diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala index 70c2918287..e3391378ae 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/RequestRendererSpec.scala @@ -12,6 +12,7 @@ import org.scalatest.{ FreeSpec, Matchers, BeforeAndAfterAll } import org.scalatest.matchers.Matcher import akka.actor.ActorSystem import akka.event.NoLogging +import akka.util.ByteString import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers._ import akka.http.impl.util._ @@ -99,7 +100,7 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll HttpRequest(PUT, "/abc/xyz", List( RawHeader("X-Fancy", "naa"), RawHeader("Cache-Control", "public"), - Host("spray.io")), HttpEntity(ContentTypes.NoContentType, "The content please!")) should renderTo { + Host("spray.io")), HttpEntity(ContentTypes.NoContentType, ByteString("The content please!"))) should renderTo { """PUT /abc/xyz HTTP/1.1 |X-Fancy: naa |Cache-Control: public @@ -140,11 +141,11 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll "PUT request with empty chunk stream and custom Content-Type" in new TestSetup() { pending // Disabled until #15981 is fixed - HttpRequest(PUT, "/abc/xyz", entity = Chunked(ContentTypes.`text/plain`, source())) should renderTo { + HttpRequest(PUT, "/abc/xyz", entity = Chunked(ContentTypes.`text/plain(UTF-8)`, source())) should renderTo { """PUT /abc/xyz HTTP/1.1 |Host: test.com:8080 |User-Agent: akka-http/1.0.0 - |Content-Type: text/plain + |Content-Type: text/plain; charset=UTF-8 |Content-Length: 0 | |""" @@ -152,13 +153,13 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll } "POST request with body" in new TestSetup() { - HttpRequest(POST, "/abc/xyz", entity = Chunked(ContentTypes.`text/plain`, + HttpRequest(POST, "/abc/xyz", entity = Chunked(ContentTypes.`text/plain(UTF-8)`, source("XXXX", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))) should renderTo { """POST /abc/xyz HTTP/1.1 |Host: test.com:8080 |User-Agent: akka-http/1.0.0 |Transfer-Encoding: chunked - |Content-Type: text/plain + |Content-Type: text/plain; charset=UTF-8 | |4 |XXXX @@ -177,13 +178,13 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll ChunkStreamPart("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), LastChunk) - HttpRequest(POST, "/abc/xyz", entity = Chunked(ContentTypes.`text/plain`, + HttpRequest(POST, "/abc/xyz", entity = Chunked(ContentTypes.`text/plain(UTF-8)`, Source(chunks))) should renderTo { """POST /abc/xyz HTTP/1.1 |Host: test.com:8080 |User-Agent: akka-http/1.0.0 |Transfer-Encoding: chunked - |Content-Type: text/plain + |Content-Type: text/plain; charset=UTF-8 | |4 |XXXX @@ -203,13 +204,13 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll LastChunk, LastChunk) - HttpRequest(POST, "/abc/xyz", entity = Chunked(ContentTypes.`text/plain`, + HttpRequest(POST, "/abc/xyz", entity = Chunked(ContentTypes.`text/plain(UTF-8)`, Source(chunks))) should renderTo { """POST /abc/xyz HTTP/1.1 |Host: test.com:8080 |User-Agent: akka-http/1.0.0 |Transfer-Encoding: chunked - |Content-Type: text/plain + |Content-Type: text/plain; charset=UTF-8 | |4 |XXXX @@ -223,12 +224,12 @@ class RequestRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll "POST request with custom Transfer-Encoding header" in new TestSetup() { HttpRequest(POST, "/abc/xyz", List(`Transfer-Encoding`(TransferEncodings.Extension("fancy"))), - entity = Chunked(ContentTypes.`text/plain`, source("XXXX", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))) should renderTo { + entity = Chunked(ContentTypes.`text/plain(UTF-8)`, source("XXXX", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))) should renderTo { """POST /abc/xyz HTTP/1.1 |Transfer-Encoding: fancy, chunked |Host: test.com:8080 |User-Agent: akka-http/1.0.0 - |Content-Type: text/plain + |Content-Type: text/plain; charset=UTF-8 | |4 |XXXX diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala index 87a59fc0ae..e2b01621cf 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala @@ -174,7 +174,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll "status 400, a few headers and a body with an explicitly suppressed Content Type header" in new TestSetup() { HttpResponse(400, List(Age(30), Connection("Keep-Alive")), - HttpEntity(contentType = ContentTypes.NoContentType, "Small f*ck up overhere!")) should renderTo { + HttpEntity(ContentTypes.NoContentType, ByteString("Small f*ck up overhere!"))) should renderTo { """HTTP/1.1 400 Bad Request |Age: 30 |Server: akka-http/1.0.0 @@ -202,7 +202,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll "a response with a Default (streamed with explicit content-length body," - { "status 400 and a few headers" in new TestSetup() { HttpResponse(400, List(Age(30), Connection("Keep-Alive")), - entity = Default(contentType = ContentTypes.`text/plain(UTF-8)`, 23, source(ByteString("Small f*ck up overhere!")))) should renderTo { + entity = Default(ContentTypes.`text/plain(UTF-8)`, 23, source(ByteString("Small f*ck up overhere!")))) should renderTo { """HTTP/1.1 400 Bad Request |Age: 30 |Server: akka-http/1.0.0 diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/server/HttpServerSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/server/HttpServerSpec.scala index dc4ce06105..05d67c1f95 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/server/HttpServerSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/server/HttpServerSpec.scala @@ -385,12 +385,12 @@ class HttpServerSpec extends AkkaSpec("akka.loggers = []\n akka.loglevel = OFF") |""") inside(expectRequest()) { case HttpRequest(GET, _, _, _, _) ⇒ - responses.sendNext(HttpResponse(entity = HttpEntity.Strict(ContentTypes.`text/plain`, ByteString("abcd")))) + responses.sendNext(HttpResponse(entity = HttpEntity.Strict(ContentTypes.`text/plain(UTF-8)`, ByteString("abcd")))) expectResponseWithWipedDate( """|HTTP/1.1 200 OK |Server: akka-http/test |Date: XXXX - |Content-Type: text/plain + |Content-Type: text/plain; charset=UTF-8 |Content-Length: 4 | |""") @@ -405,14 +405,14 @@ class HttpServerSpec extends AkkaSpec("akka.loggers = []\n akka.loglevel = OFF") val data = TestPublisher.manualProbe[ByteString]() inside(expectRequest()) { case HttpRequest(GET, _, _, _, _) ⇒ - responses.sendNext(HttpResponse(entity = HttpEntity.Default(ContentTypes.`text/plain`, 4, Source(data)))) + responses.sendNext(HttpResponse(entity = HttpEntity.Default(ContentTypes.`text/plain(UTF-8)`, 4, Source(data)))) val dataSub = data.expectSubscription() dataSub.expectCancellation() expectResponseWithWipedDate( """|HTTP/1.1 200 OK |Server: akka-http/test |Date: XXXX - |Content-Type: text/plain + |Content-Type: text/plain; charset=UTF-8 |Content-Length: 4 | |""") @@ -427,14 +427,14 @@ class HttpServerSpec extends AkkaSpec("akka.loggers = []\n akka.loglevel = OFF") val data = TestPublisher.manualProbe[ByteString]() inside(expectRequest()) { case HttpRequest(GET, _, _, _, _) ⇒ - responses.sendNext(HttpResponse(entity = HttpEntity.CloseDelimited(ContentTypes.`text/plain`, Source(data)))) + responses.sendNext(HttpResponse(entity = HttpEntity.CloseDelimited(ContentTypes.`text/plain(UTF-8)`, Source(data)))) val dataSub = data.expectSubscription() dataSub.expectCancellation() expectResponseWithWipedDate( """|HTTP/1.1 200 OK |Server: akka-http/test |Date: XXXX - |Content-Type: text/plain + |Content-Type: text/plain; charset=UTF-8 | |""") } @@ -450,7 +450,7 @@ class HttpServerSpec extends AkkaSpec("akka.loggers = []\n akka.loglevel = OFF") val data = TestPublisher.manualProbe[ChunkStreamPart]() inside(expectRequest()) { case HttpRequest(GET, _, _, _, _) ⇒ - responses.sendNext(HttpResponse(entity = HttpEntity.Chunked(ContentTypes.`text/plain`, Source(data)))) + responses.sendNext(HttpResponse(entity = HttpEntity.Chunked(ContentTypes.`text/plain(UTF-8)`, Source(data)))) val dataSub = data.expectSubscription() dataSub.expectCancellation() expectResponseWithWipedDate( @@ -458,7 +458,7 @@ class HttpServerSpec extends AkkaSpec("akka.loggers = []\n akka.loglevel = OFF") |Server: akka-http/test |Date: XXXX |Transfer-Encoding: chunked - |Content-Type: text/plain + |Content-Type: text/plain; charset=UTF-8 | |""") } @@ -473,7 +473,7 @@ class HttpServerSpec extends AkkaSpec("akka.loggers = []\n akka.loglevel = OFF") val data = TestPublisher.manualProbe[ByteString]() inside(expectRequest()) { case HttpRequest(GET, _, _, _, _) ⇒ - responses.sendNext(HttpResponse(entity = CloseDelimited(ContentTypes.`text/plain`, Source(data)))) + responses.sendNext(HttpResponse(entity = CloseDelimited(ContentTypes.`text/plain(UTF-8)`, Source(data)))) val dataSub = data.expectSubscription() dataSub.expectCancellation() netOut.expectBytes(1) @@ -884,6 +884,7 @@ class HttpServerSpec extends AkkaSpec("akka.loggers = []\n akka.loglevel = OFF") "the config setting applied before another attribute (default entity)" in new LengthVerificationTest(maxContentLength = 10) { def nameDataSource(name: String): RequestEntity ⇒ RequestEntity = { case x: HttpEntity.Default ⇒ x.copy(data = x.data named name) + case _ ⇒ ??? // prevent a compile-time warning } sendDefaultRequestWithLength(10) expectRequest().mapEntity(nameDataSource("foo")).expectEntity[HttpEntity.Default](10) diff --git a/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala index 91b9bcbc57..ea3eaa3aad 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala @@ -19,7 +19,7 @@ import HttpEncodings._ import HttpMethods._ class HttpHeaderSpec extends FreeSpec with Matchers { - val `application/vnd.spray` = MediaType.custom("application/vnd.spray", MediaType.Encoding.Binary) + val `application/vnd.spray` = MediaType.applicationBinary("vnd.spray", compressible = true) val PROPFIND = HttpMethod.custom("PROPFIND") "The HTTP header model must correctly parse and render the headers" - { @@ -38,9 +38,9 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Accept: */*, text/*; foo=bar, custom/custom; bar=\"b>az\"" =!= Accept(`*/*`, MediaRange.custom("text", Map("foo" -> "bar")), - MediaType.custom("custom", "custom", MediaType.Encoding.Binary, params = Map("bar" -> "b>az"))) + MediaType.customBinary("custom", "custom", compressible = true, params = Map("bar" -> "b>az"))) "Accept: application/*+xml; version=2" =!= - Accept(MediaType.custom("application", "*+xml", MediaType.Encoding.Binary, params = Map("version" -> "2"))) + Accept(MediaType.customBinary("application", "*+xml", compressible = true, params = Map("version" -> "2"))) } "Accept-Charset" in { @@ -197,17 +197,18 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Content-Type: text/plain; charset=utf8" =!= `Content-Type`(ContentType(`text/plain`, `UTF-8`)).renderedTo("text/plain; charset=UTF-8") "Content-Type: text/xml2; version=3; charset=windows-1252" =!= - `Content-Type`(ContentType(MediaType.custom("text", "xml2", encoding = MediaType.Encoding.Open, - params = Map("version" -> "3")), HttpCharsets.getForKey("windows-1252"))) + `Content-Type`(MediaType.customWithOpenCharset("text", "xml2", params = Map("version" -> "3")) + withCharset HttpCharsets.getForKey("windows-1252").get) "Content-Type: text/plain; charset=fancy-pants" =!= - `Content-Type`(ContentType(`text/plain`, HttpCharset.custom("fancy-pants"))) + `Content-Type`(`text/plain` withCharset HttpCharset.custom("fancy-pants")) "Content-Type: multipart/mixed; boundary=ABC123" =!= - `Content-Type`(ContentType(`multipart/mixed` withBoundary "ABC123")) + `Content-Type`(`multipart/mixed` withBoundary "ABC123" withCharset `UTF-8`) + .renderedTo("multipart/mixed; boundary=ABC123; charset=UTF-8") "Content-Type: multipart/mixed; boundary=\"ABC/123\"" =!= - `Content-Type`(ContentType(`multipart/mixed` withBoundary "ABC/123")) + `Content-Type`(`multipart/mixed` withBoundary "ABC/123" withCharset `UTF-8`) + .renderedTo("""multipart/mixed; boundary="ABC/123"; charset=UTF-8""") "Content-Type: application/*" =!= - `Content-Type`(ContentType(MediaType.custom("application", "*", MediaType.Encoding.Binary, - allowArbitrarySubtypes = true))) + `Content-Type`(MediaType.customBinary("application", "*", compressible = false, allowArbitrarySubtypes = true)) } "Content-Range" in { diff --git a/akka-http-core/src/test/scala/akka/http/scaladsl/ClientServerSpec.scala b/akka-http-core/src/test/scala/akka/http/scaladsl/ClientServerSpec.scala index 540d766f33..eca0ed7e50 100644 --- a/akka-http-core/src/test/scala/akka/http/scaladsl/ClientServerSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/scaladsl/ClientServerSpec.scala @@ -164,7 +164,7 @@ class ClientServerSpec extends WordSpec with Matchers with BeforeAndAfterAll { try { def runIdleRequest(uri: Uri): Future[HttpResponse] = { - val itNeverEnds = Chunked.fromData(ContentTypes.`text/plain`, Source.maybe[ByteString]) + val itNeverEnds = Chunked.fromData(ContentTypes.`text/plain(UTF-8)`, Source.maybe[ByteString]) Http().outgoingConnection(hostname, port) .runWith(Source.single(HttpRequest(PUT, uri, entity = itNeverEnds)), Sink.head) ._2 @@ -197,7 +197,7 @@ class ClientServerSpec extends WordSpec with Matchers with BeforeAndAfterAll { try { def runRequest(uri: Uri): Future[HttpResponse] = { - val itNeverSends = Chunked.fromData(ContentTypes.`text/plain`, Source.maybe[ByteString]) + val itNeverSends = Chunked.fromData(ContentTypes.`text/plain(UTF-8)`, Source.maybe[ByteString]) Http().outgoingConnection(hostname, port, settings = clientSettings) .runWith(Source.single(HttpRequest(POST, uri, entity = itNeverSends)), Sink.head) ._2 @@ -232,7 +232,7 @@ class ClientServerSpec extends WordSpec with Matchers with BeforeAndAfterAll { val pool = Http().cachedHostConnectionPool[Int](hostname, port, clientPoolSettings) def runRequest(uri: Uri): Future[(Try[HttpResponse], Int)] = { - val itNeverSends = Chunked.fromData(ContentTypes.`text/plain`, Source.maybe[ByteString]) + val itNeverSends = Chunked.fromData(ContentTypes.`text/plain(UTF-8)`, Source.maybe[ByteString]) Source.single(HttpRequest(POST, uri, entity = itNeverSends) -> 1) .via(pool) .runWith(Sink.head) @@ -265,7 +265,7 @@ class ClientServerSpec extends WordSpec with Matchers with BeforeAndAfterAll { try { def runRequest(uri: Uri): Future[HttpResponse] = { - val itNeverSends = Chunked.fromData(ContentTypes.`text/plain`, Source.maybe[ByteString]) + val itNeverSends = Chunked.fromData(ContentTypes.`text/plain(UTF-8)`, Source.maybe[ByteString]) Http().singleRequest(HttpRequest(POST, uri, entity = itNeverSends), settings = clientPoolSettings) } @@ -368,7 +368,7 @@ class ClientServerSpec extends WordSpec with Matchers with BeforeAndAfterAll { val (serverIn, serverOut) = acceptConnection() val chunks = List(Chunk("abc"), Chunk("defg"), Chunk("hijkl"), LastChunk) - val chunkedContentType: ContentType = MediaTypes.`application/base64` + val chunkedContentType: ContentType = MediaTypes.`application/base64` withCharset HttpCharsets.`UTF-8` val chunkedEntity = HttpEntity.Chunked(chunkedContentType, Source(chunks)) val clientOutSub = clientOut.expectSubscription() diff --git a/akka-http-core/src/test/scala/akka/http/scaladsl/TestServer.scala b/akka-http-core/src/test/scala/akka/http/scaladsl/TestServer.scala index 3ca5e626af..aab2cd04c7 100644 --- a/akka-http-core/src/test/scala/akka/http/scaladsl/TestServer.scala +++ b/akka-http-core/src/test/scala/akka/http/scaladsl/TestServer.scala @@ -51,7 +51,7 @@ object TestServer extends App { ////////////// helpers ////////////// lazy val index = HttpResponse( - entity = HttpEntity(MediaTypes.`text/html`, + entity = HttpEntity(ContentTypes.`text/html(UTF-8)`, """| | |

Say hello to akka-http-core!

diff --git a/akka-http-core/src/test/scala/io/akka/integrationtest/http/HttpModelIntegrationSpec.scala b/akka-http-core/src/test/scala/io/akka/integrationtest/http/HttpModelIntegrationSpec.scala index e413016344..fbd8efaf95 100644 --- a/akka-http-core/src/test/scala/io/akka/integrationtest/http/HttpModelIntegrationSpec.scala +++ b/akka-http-core/src/test/scala/io/akka/integrationtest/http/HttpModelIntegrationSpec.scala @@ -130,7 +130,7 @@ class HttpModelIntegrationSpec extends WordSpec with Matchers with BeforeAndAfte val contentType = convertedHeaders.collectFirst { case ct: `Content-Type` ⇒ ct.contentType } - contentType shouldEqual Some(ContentTypes.`text/plain`) + contentType shouldEqual Some(ContentTypes.`text/plain(UTF-8)`) val contentLength = convertedHeaders.collectFirst { case cl: `Content-Length` ⇒ cl.length @@ -207,7 +207,7 @@ class HttpModelIntegrationSpec extends WordSpec with Matchers with BeforeAndAfte // to be able to directly create a ContentType and ContentLength // headers. ExampleLibrary.contentLength(3) - ExampleLibrary.contentType(ContentTypes.`text/plain`) + ExampleLibrary.contentType(ContentTypes.`text/plain(UTF-8)`) } } From 899b92faf2b66ad3e00150c0eeb710efb69a0bb9 Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 1 Dec 2015 23:07:56 +0100 Subject: [PATCH 3/5] !htp #19034 refactor content negotiation, upgrade to new MediaType / ContentType model --- .../code/docs/http/javadsl/ModelDocTest.java | 2 +- .../server/HighLevelServerExample.java | 2 +- .../server/HttpServerExampleDocTest.java | 4 +- .../http/scaladsl/HttpServerExampleSpec.scala | 6 +- .../code/docs/http/scaladsl/ModelSpec.scala | 3 +- .../CodingDirectivesExamplesSpec.scala | 5 +- .../FileUploadDirectivesExamplesSpec.scala | 10 +- .../sprayjson/SprayJsonSupport.scala | 5 +- .../marshallers/xml/ScalaXmlSupport.scala | 12 +- .../javadsl/server/values/FormFieldsTest.java | 3 +- .../marshallers/xml/ScalaXmlSupportSpec.scala | 21 +- .../marshalling/ContentNegotiationSpec.scala | 104 +++---- .../marshalling/MarshallingSpec.scala | 28 +- .../directives/CodingDirectivesSpec.scala | 9 +- .../FileAndResourceDirectivesSpec.scala | 5 +- .../directives/FileUploadDirectivesSpec.scala | 13 +- .../directives/FormFieldDirectivesSpec.scala | 8 +- .../MarshallingDirectivesSpec.scala | 33 +-- .../directives/MiscDirectivesSpec.scala | 4 - .../directives/RangeDirectivesSpec.scala | 2 +- .../directives/RouteDirectivesSpec.scala | 8 +- .../MultipartUnmarshallersSpec.scala | 64 +++-- .../impl/server/RejectionHandlerWrapper.scala | 2 +- .../http/impl/server/RequestContextImpl.scala | 2 +- .../http/javadsl/server/Marshallers.scala | 14 +- .../javadsl/server/RejectionHandler.scala | 6 +- .../http/javadsl/server/RequestContext.scala | 6 +- .../server/directives/BasicDirectives.scala | 2 +- .../FileAndResourceDirectives.scala | 9 - .../http/scaladsl/common/StrictForm.scala | 10 +- .../marshalling/ContentTypeOverrider.scala | 35 +++ .../http/scaladsl/marshalling/Marshal.scala | 65 ++--- .../scaladsl/marshalling/Marshaller.scala | 73 +++-- .../marshalling/MediaTypeOverrider.scala | 30 -- .../marshalling/MultipartMarshallers.scala | 4 +- .../PredefinedToEntityMarshallers.scala | 60 ++-- .../scaladsl/server/ContentNegotation.scala | 264 ++++++++++++++++++ .../akka/http/scaladsl/server/Rejection.scala | 2 +- .../scaladsl/server/RejectionHandler.scala | 4 +- .../server/directives/CodingDirectives.scala | 44 +-- .../FileAndResourceDirectives.scala | 2 +- .../server/directives/MiscDirectives.scala | 15 +- .../server/directives/RouteDirectives.scala | 2 +- .../MultipartUnmarshallers.scala | 12 +- .../PredefinedFromEntityUnmarshallers.scala | 29 +- .../scaladsl/unmarshalling/Unmarshaller.scala | 12 +- 46 files changed, 652 insertions(+), 403 deletions(-) create mode 100644 akka-http/src/main/scala/akka/http/scaladsl/marshalling/ContentTypeOverrider.scala delete mode 100644 akka-http/src/main/scala/akka/http/scaladsl/marshalling/MediaTypeOverrider.scala create mode 100644 akka-http/src/main/scala/akka/http/scaladsl/server/ContentNegotation.scala diff --git a/akka-docs-dev/rst/java/code/docs/http/javadsl/ModelDocTest.java b/akka-docs-dev/rst/java/code/docs/http/javadsl/ModelDocTest.java index 782fcd0cbe..56dbff1f71 100644 --- a/akka-docs-dev/rst/java/code/docs/http/javadsl/ModelDocTest.java +++ b/akka-docs-dev/rst/java/code/docs/http/javadsl/ModelDocTest.java @@ -35,7 +35,7 @@ public class ModelDocTest { Authorization authorization = Authorization.basic("user", "pass"); HttpRequest complexRequest = HttpRequest.PUT("/user") - .withEntity(HttpEntities.create(MediaTypes.TEXT_PLAIN.toContentType(), "abc")) + .withEntity(HttpEntities.create(ContentTypes.TEXT_PLAIN_UTF8, "abc")) .addHeader(authorization) .withProtocol(HttpProtocols.HTTP_1_0); //#construct-request diff --git a/akka-docs-dev/rst/java/code/docs/http/javadsl/server/HighLevelServerExample.java b/akka-docs-dev/rst/java/code/docs/http/javadsl/server/HighLevelServerExample.java index 3a276f0bab..f59547cb23 100644 --- a/akka-docs-dev/rst/java/code/docs/http/javadsl/server/HighLevelServerExample.java +++ b/akka-docs-dev/rst/java/code/docs/http/javadsl/server/HighLevelServerExample.java @@ -51,7 +51,7 @@ public class HighLevelServerExample extends HttpApp { // matches the empty path pathSingleSlash().route( // return a constant string with a certain content type - complete(ContentTypes.TEXT_HTML, + complete(ContentTypes.TEXT_HTML_UTF8, "Hello world!") ), path("ping").route( diff --git a/akka-docs-dev/rst/java/code/docs/http/javadsl/server/HttpServerExampleDocTest.java b/akka-docs-dev/rst/java/code/docs/http/javadsl/server/HttpServerExampleDocTest.java index 60f314f151..4229b906ac 100644 --- a/akka-docs-dev/rst/java/code/docs/http/javadsl/server/HttpServerExampleDocTest.java +++ b/akka-docs-dev/rst/java/code/docs/http/javadsl/server/HttpServerExampleDocTest.java @@ -156,7 +156,7 @@ public class HttpServerExampleDocTest { .via(failureDetection) .map(request -> { Source bytes = request.entity().getDataBytes(); - HttpEntity.Chunked entity = HttpEntities.create(ContentTypes.TEXT_PLAIN, bytes); + HttpEntityChunked entity = HttpEntities.create(ContentTypes.TEXT_PLAIN_UTF8, bytes); return HttpResponse.create() .withEntity(entity); @@ -199,7 +199,7 @@ public class HttpServerExampleDocTest { if (uri.path().equals("/")) return HttpResponse.create() - .withEntity(ContentTypes.TEXT_HTML, + .withEntity(ContentTypes.TEXT_HTML_UTF8, "Hello world!"); else if (uri.path().equals("/hello")) { String name = Util.getOrElse(uri.query().get("name"), "Mister X"); diff --git a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/HttpServerExampleSpec.scala b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/HttpServerExampleSpec.scala index 19b4a8fe3d..e677ecb5dc 100644 --- a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/HttpServerExampleSpec.scala +++ b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/HttpServerExampleSpec.scala @@ -150,7 +150,7 @@ class HttpServerExampleSpec extends WordSpec with Matchers { .via(reactToConnectionFailure) .map { request => // simple text "echo" response: - HttpResponse(entity = HttpEntity(ContentTypes.`text/plain`, request.entity.dataBytes)) + HttpResponse(entity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, request.entity.dataBytes)) } serverSource @@ -173,7 +173,7 @@ class HttpServerExampleSpec extends WordSpec with Matchers { val requestHandler: HttpRequest => HttpResponse = { case HttpRequest(GET, Uri.Path("/"), _, _, _) => - HttpResponse(entity = HttpEntity(MediaTypes.`text/html`, + HttpResponse(entity = HttpEntity(ContentTypes.`text/html(UTF-8)`, "Hello world!")) case HttpRequest(GET, Uri.Path("/ping"), _, _, _) => @@ -207,7 +207,7 @@ class HttpServerExampleSpec extends WordSpec with Matchers { val requestHandler: HttpRequest => HttpResponse = { case HttpRequest(GET, Uri.Path("/"), _, _, _) => - HttpResponse(entity = HttpEntity(MediaTypes.`text/html`, + HttpResponse(entity = HttpEntity(ContentTypes.`text/html(UTF-8)`, "Hello world!")) case HttpRequest(GET, Uri.Path("/ping"), _, _, _) => diff --git a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/ModelSpec.scala b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/ModelSpec.scala index 1ab2cd3a43..6b6db2a189 100644 --- a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/ModelSpec.scala +++ b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/ModelSpec.scala @@ -32,12 +32,13 @@ class ModelSpec extends AkkaSpec { // customize every detail of HTTP request import HttpProtocols._ import MediaTypes._ + import HttpCharsets._ val userData = ByteString("abc") val authorization = headers.Authorization(BasicHttpCredentials("user", "pass")) HttpRequest( PUT, uri = "/user", - entity = HttpEntity(`text/plain`, userData), + entity = HttpEntity(`text/plain` withCharset `UTF-8`, userData), headers = List(authorization), protocol = `HTTP/1.0`) //#construct-request diff --git a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/CodingDirectivesExamplesSpec.scala b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/CodingDirectivesExamplesSpec.scala index a1366c968e..7276cac7f9 100644 --- a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/CodingDirectivesExamplesSpec.scala +++ b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/CodingDirectivesExamplesSpec.scala @@ -20,7 +20,7 @@ class CodingDirectivesExamplesSpec extends RoutingSpec { Get("/") ~> route ~> check { responseAs[String] shouldEqual "content" } - Get("/") ~> `Accept-Encoding`(`identity;q=MIN`) ~> route ~> check { + Get("/") ~> `Accept-Encoding`(deflate) ~> route ~> check { rejection shouldEqual UnacceptedResponseEncodingRejection(gzip) } } @@ -48,9 +48,6 @@ class CodingDirectivesExamplesSpec extends RoutingSpec { Get("/") ~> route ~> check { response should haveContentEncoding(gzip) } - Get("/") ~> `Accept-Encoding`() ~> route ~> check { - rejection shouldEqual UnacceptedResponseEncodingRejection(gzip) - } Get("/") ~> `Accept-Encoding`(gzip, deflate) ~> route ~> check { response should haveContentEncoding(gzip) } diff --git a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/FileUploadDirectivesExamplesSpec.scala b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/FileUploadDirectivesExamplesSpec.scala index dba56ff2b2..f259adc1eb 100644 --- a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/FileUploadDirectivesExamplesSpec.scala +++ b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/FileUploadDirectivesExamplesSpec.scala @@ -3,15 +3,11 @@ */ package docs.http.scaladsl.server.directives -import java.io.File - -import akka.http.scaladsl.model.{ MediaTypes, HttpEntity, Multipart, StatusCodes } +import akka.http.scaladsl.model._ import akka.stream.io.Framing import akka.util.ByteString import docs.http.scaladsl.server.RoutingSpec - import scala.concurrent.Future -import scala.util.{ Success, Failure } class FileUploadDirectivesExamplesSpec extends RoutingSpec { @@ -32,7 +28,7 @@ class FileUploadDirectivesExamplesSpec extends RoutingSpec { Multipart.FormData( Multipart.FormData.BodyPart.Strict( "csv", - HttpEntity(MediaTypes.`text/plain`, "1,5,7\n11,13,17"), + HttpEntity(ContentTypes.`text/plain(UTF-8)`, "1,5,7\n11,13,17"), Map("filename" -> "data.csv"))) Post("/", multipartForm) ~> route ~> check { @@ -68,7 +64,7 @@ class FileUploadDirectivesExamplesSpec extends RoutingSpec { val multipartForm = Multipart.FormData(Multipart.FormData.BodyPart.Strict( "csv", - HttpEntity(MediaTypes.`text/plain`, "2,3,5\n7,11,13,17,23\n29,31,37\n"), + HttpEntity(ContentTypes.`text/plain(UTF-8)`, "2,3,5\n7,11,13,17,23\n29,31,37\n"), Map("filename" -> "primes.csv"))) Post("/", multipartForm) ~> route ~> check { diff --git a/akka-http-marshallers-scala/akka-http-spray-json/src/main/scala/akka/http/scaladsl/marshallers/sprayjson/SprayJsonSupport.scala b/akka-http-marshallers-scala/akka-http-spray-json/src/main/scala/akka/http/scaladsl/marshallers/sprayjson/SprayJsonSupport.scala index 57ac42d3e8..e06c94de65 100644 --- a/akka-http-marshallers-scala/akka-http-spray-json/src/main/scala/akka/http/scaladsl/marshallers/sprayjson/SprayJsonSupport.scala +++ b/akka-http-marshallers-scala/akka-http-spray-json/src/main/scala/akka/http/scaladsl/marshallers/sprayjson/SprayJsonSupport.scala @@ -5,10 +5,9 @@ package akka.http.scaladsl.marshallers.sprayjson import scala.language.implicitConversions -import akka.stream.Materializer import akka.http.scaladsl.marshalling.{ ToEntityMarshaller, Marshaller } import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshaller } -import akka.http.scaladsl.model.{ ContentTypes, HttpCharsets } +import akka.http.scaladsl.model.{ MediaTypes, HttpCharsets } import akka.http.scaladsl.model.MediaTypes.`application/json` import spray.json._ @@ -33,6 +32,6 @@ trait SprayJsonSupport { implicit def sprayJsonMarshaller[T](implicit writer: RootJsonWriter[T], printer: JsonPrinter = PrettyPrinter): ToEntityMarshaller[T] = sprayJsValueMarshaller compose writer.write implicit def sprayJsValueMarshaller(implicit printer: JsonPrinter = PrettyPrinter): ToEntityMarshaller[JsValue] = - Marshaller.StringMarshaller.wrap(ContentTypes.`application/json`)(printer) + Marshaller.StringMarshaller.wrap(MediaTypes.`application/json`)(printer) } object SprayJsonSupport extends SprayJsonSupport \ No newline at end of file diff --git a/akka-http-marshallers-scala/akka-http-xml/src/main/scala/akka/http/scaladsl/marshallers/xml/ScalaXmlSupport.scala b/akka-http-marshallers-scala/akka-http-xml/src/main/scala/akka/http/scaladsl/marshallers/xml/ScalaXmlSupport.scala index 26a80d589f..bf317c3818 100644 --- a/akka-http-marshallers-scala/akka-http-xml/src/main/scala/akka/http/scaladsl/marshallers/xml/ScalaXmlSupport.scala +++ b/akka-http-marshallers-scala/akka-http-xml/src/main/scala/akka/http/scaladsl/marshallers/xml/ScalaXmlSupport.scala @@ -8,7 +8,6 @@ import java.io.{ ByteArrayInputStream, InputStreamReader } import javax.xml.parsers.{ SAXParserFactory, SAXParser } import scala.collection.immutable import scala.xml.{ XML, NodeSeq } -import akka.stream.Materializer import akka.http.scaladsl.unmarshalling._ import akka.http.scaladsl.marshalling._ import akka.http.scaladsl.model._ @@ -16,10 +15,10 @@ import MediaTypes._ trait ScalaXmlSupport { implicit def defaultNodeSeqMarshaller: ToEntityMarshaller[NodeSeq] = - Marshaller.oneOf(ScalaXmlSupport.nodeSeqContentTypes.map(nodeSeqMarshaller): _*) + Marshaller.oneOf(ScalaXmlSupport.nodeSeqMediaTypes.map(nodeSeqMarshaller): _*) - def nodeSeqMarshaller(contentType: ContentType): ToEntityMarshaller[NodeSeq] = - Marshaller.StringMarshaller.wrap(contentType)(_.toString()) + def nodeSeqMarshaller(mediaType: MediaType.NonBinary): ToEntityMarshaller[NodeSeq] = + Marshaller.StringMarshaller.wrap(mediaType)(_.toString()) implicit def defaultNodeSeqUnmarshaller: FromEntityUnmarshaller[NodeSeq] = nodeSeqUnmarshaller(ScalaXmlSupport.nodeSeqContentTypeRanges: _*) @@ -35,13 +34,12 @@ trait ScalaXmlSupport { /** * Provides a SAXParser for the NodeSeqUnmarshaller to use. Override to provide a custom SAXParser implementation. * Will be called once for for every request to be unmarshalled. The default implementation calls [[ScalaXmlSupport.createSaferSAXParser]]. - * @return */ protected def createSAXParser(): SAXParser = ScalaXmlSupport.createSaferSAXParser() } object ScalaXmlSupport extends ScalaXmlSupport { - val nodeSeqContentTypes: immutable.Seq[ContentType] = List(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`) - val nodeSeqContentTypeRanges: immutable.Seq[ContentTypeRange] = nodeSeqContentTypes.map(ContentTypeRange(_)) + val nodeSeqMediaTypes: immutable.Seq[MediaType.NonBinary] = List(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`) + val nodeSeqContentTypeRanges: immutable.Seq[ContentTypeRange] = nodeSeqMediaTypes.map(ContentTypeRange(_)) /** Creates a safer SAXParser. */ def createSaferSAXParser(): SAXParser = { diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/values/FormFieldsTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/FormFieldsTest.java index 9ecbe73229..886a9889ff 100644 --- a/akka-http-tests/src/test/java/akka/http/javadsl/server/values/FormFieldsTest.java +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/values/FormFieldsTest.java @@ -4,6 +4,7 @@ package akka.http.javadsl.server.values; +import akka.http.javadsl.model.HttpCharsets; import akka.http.javadsl.model.HttpRequest; import akka.http.javadsl.model.MediaTypes; import akka.http.javadsl.server.RequestVal; @@ -50,7 +51,7 @@ public class FormFieldsTest extends JUnitRouteTest { return HttpRequest.POST("/test") - .withEntity(MediaTypes.APPLICATION_X_WWW_FORM_URLENCODED.toContentType(), sb.toString()); + .withEntity(MediaTypes.APPLICATION_X_WWW_FORM_URLENCODED.toContentType(HttpCharsets.UTF_8), sb.toString()); } private HttpRequest singleParameterUrlEncodedRequest(String name, String value) { return urlEncodedRequest(entry(name, value)); diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/marshallers/xml/ScalaXmlSupportSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/marshallers/xml/ScalaXmlSupportSpec.scala index 803706d16e..ef16eaa5c9 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/marshallers/xml/ScalaXmlSupportSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/marshallers/xml/ScalaXmlSupportSpec.scala @@ -7,16 +7,15 @@ package akka.http.scaladsl.marshallers.xml import java.io.File import akka.http.scaladsl.TestUtils -import scala.concurrent.duration._ import org.xml.sax.SAXParseException - -import scala.concurrent.{ Future, Await } import scala.xml.NodeSeq +import scala.concurrent.{ Future, Await } +import scala.concurrent.duration._ import org.scalatest.{ Inside, FreeSpec, Matchers } +import akka.util.ByteString import akka.http.scaladsl.testkit.ScalatestRouteTest import akka.http.scaladsl.unmarshalling.{ Unmarshaller, Unmarshal } import akka.http.scaladsl.model._ -import HttpCharsets._ import MediaTypes._ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest with Inside { @@ -25,13 +24,13 @@ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest "NodeSeqMarshaller should" - { "marshal xml snippets to `text/xml` content in UTF-8" in { marshal(Ha“llo) shouldEqual - HttpEntity(ContentType(`text/xml`, `UTF-8`), "Ha“llo") + HttpEntity(ContentTypes.`text/xml(UTF-8)`, "Ha“llo") } "unmarshal `text/xml` content in UTF-8 to NodeSeqs" in { - Unmarshal(HttpEntity(`text/xml`, "Hällö")).to[NodeSeq].map(_.text) should evaluateTo("Hällö") + Unmarshal(HttpEntity(ContentTypes.`text/xml(UTF-8)`, "Hällö")).to[NodeSeq].map(_.text) should evaluateTo("Hällö") } "reject `application/octet-stream`" in { - Unmarshal(HttpEntity(`application/octet-stream`, "Hällö")).to[NodeSeq].map(_.text) should + Unmarshal(HttpEntity(`application/octet-stream`, ByteString("Hällö"))).to[NodeSeq].map(_.text) should haveFailedWith(Unmarshaller.UnsupportedContentTypeException(nodeSeqContentTypeRanges: _*)) } @@ -43,7 +42,7 @@ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest | | ]>hello&xxe;""".stripMargin - shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(`text/xml`, xml)).to[NodeSeq]) + shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(ContentTypes.`text/xml(UTF-8)`, xml)).to[NodeSeq]) } } "parse XML bodies without loading in a related schema from a parameter" in { @@ -58,7 +57,7 @@ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest | %xpe; | %pe; | ]>hello&xxe;""".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 | ]> | &laugh30;""".stripMargin - shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(`text/xml`, xml)).to[NodeSeq]) + shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(ContentTypes.`text/xml(UTF-8)`, xml)).to[NodeSeq]) } "gracefully fail when an entity expands to be very large" in { val as = "a" * 50000 @@ -83,7 +82,7 @@ class ScalaXmlSupportSpec extends FreeSpec with Matchers with ScalatestRouteTest | | ]> | $entities""".stripMargin - shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(`text/xml`, xml)).to[NodeSeq]) + shouldHaveFailedWithSAXParseException(Unmarshal(HttpEntity(ContentTypes.`text/xml(UTF-8)`, xml)).to[NodeSeq]) } } } diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/ContentNegotiationSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/ContentNegotiationSpec.scala index 1f3fb59d45..649e45df5e 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/ContentNegotiationSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/ContentNegotiationSpec.scala @@ -4,10 +4,10 @@ package akka.http.scaladsl.marshalling -import akka.http.scaladsl.model.MediaType.Encoding - import scala.concurrent.Await import scala.concurrent.duration._ +import akka.http.scaladsl.server.ContentNegotiator.Alternative +import akka.util.ByteString import org.scalatest.{ Matchers, FreeSpec } import akka.http.scaladsl.util.FastFuture._ import akka.http.scaladsl.model._ @@ -19,29 +19,30 @@ class ContentNegotiationSpec extends FreeSpec with Matchers { "Content Negotiation should work properly for requests with header(s)" - { "(without headers)" test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`) + accept(`text/plain` withCharset `UTF-16`) should select(`text/plain` withCharset `UTF-16`) + accept(`text/plain`, `text/html`) should select(`text/plain` withCharset `UTF-8`) + accept(`text/html`, `text/plain`) should select(`text/html` withCharset `UTF-8`) } "Accept: */*" test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`) + accept(`text/plain`) should select(`text/plain` withCharset `UTF-8`) + accept(`text/plain` withCharset `UTF-16`) should select(`text/plain` withCharset `UTF-16`) } "Accept: */*;q=.8" test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`) + accept(`text/plain`) should select(`text/plain` withCharset `UTF-8`) + accept(`text/plain` withCharset `UTF-16`) should select(`text/plain` withCharset `UTF-16`) } "Accept: text/*" test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`) + accept(`text/plain`) should select(`text/plain` withCharset `UTF-8`) + accept(`text/xml` withCharset `UTF-16`) should select(`text/xml` withCharset `UTF-16`) accept(`audio/ogg`) should reject } "Accept: text/*;q=.8" test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`) + accept(`text/plain`) should select(`text/plain` withCharset `UTF-8`) + accept(`text/xml` withCharset `UTF-16`) should select(`text/xml` withCharset `UTF-16`) accept(`audio/ogg`) should reject } @@ -51,39 +52,43 @@ class ContentNegotiationSpec extends FreeSpec with Matchers { accept(`audio/ogg`) should reject } + "Accept: text/*, application/json;q=0.8, text/plain;q=0.5" test { accept ⇒ + accept(`text/plain`, `application/json`) should select(`application/json`) + } + "Accept-Charset: UTF-16" test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-16`) + accept(`text/plain`) should select(`text/plain` withCharset `UTF-16`) accept(`text/plain` withCharset `UTF-8`) should reject } "manually created Accept-Charset: UTF-16" in testHeaders(headers.`Accept-Charset`(Vector(HttpCharsets.`UTF-16`.toRange))) { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-16`) + accept(`text/plain`) should select(`text/plain` withCharset `UTF-16`) accept(`text/plain` withCharset `UTF-8`) should reject } "Accept-Charset: UTF-16, UTF-8" test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`) + accept(`text/plain`) should select(`text/plain` withCharset `UTF-16`) + accept(`text/plain` withCharset `UTF-8`) should select(`text/plain` withCharset `UTF-8`) } "Accept-Charset: UTF-8;q=.2, UTF-16" test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-16`) - accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`) + accept(`text/plain`) should select(`text/plain` withCharset `UTF-16`) + accept(`text/plain` withCharset `UTF-8`) should select(`text/plain` withCharset `UTF-8`) } "Accept-Charset: ISO-8859-1;q=.2" test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `ISO-8859-1`) + accept(`text/plain`) should select(`text/plain` withCharset `ISO-8859-1`) accept(`text/plain` withCharset `UTF-8`) should reject } "Accept-Charset: latin1;q=.1, UTF-8;q=.2" test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`) + accept(`text/plain`) should select(`text/plain` withCharset `UTF-8`) + accept(`text/plain` withCharset `UTF-8`) should select(`text/plain` withCharset `UTF-8`) } "Accept-Charset: *" test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `UTF-8`) - accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`) + accept(`text/plain`) should select(`text/plain` withCharset `UTF-8`) + accept(`text/plain` withCharset `UTF-16`) should select(`text/plain` withCharset `UTF-16`) } "Accept-Charset: *;q=0" test { accept ⇒ @@ -92,47 +97,53 @@ class ContentNegotiationSpec extends FreeSpec with Matchers { } "Accept-Charset: us;q=0.1,*;q=0" test { accept ⇒ - accept(`text/plain`) should select(`text/plain`, `US-ASCII`) + accept(`text/plain`) should select(`text/plain` withCharset `US-ASCII`) accept(`text/plain` withCharset `UTF-8`) should reject } + "Accept-Charset: UTF-8, *;q=0.8, us;q=0.1" test { accept ⇒ + accept(`text/plain` withCharset `US-ASCII`, + `text/plain` withCharset `ISO-8859-1`) should select(`text/plain` withCharset `ISO-8859-1`) + } + "Accept: text/xml, text/html;q=.5" test { accept ⇒ accept(`text/plain`) should reject - accept(`text/xml`) should select(`text/xml`, `UTF-8`) - accept(`text/html`) should select(`text/html`, `UTF-8`) - accept(`text/html`, `text/xml`) should select(`text/xml`, `UTF-8`) - accept(`text/xml`, `text/html`) should select(`text/xml`, `UTF-8`) - accept(`text/plain`, `text/xml`) should select(`text/xml`, `UTF-8`) - accept(`text/plain`, `text/html`) should select(`text/html`, `UTF-8`) + accept(`text/xml`) should select(`text/xml` withCharset `UTF-8`) + accept(`text/html`) should select(`text/html` withCharset `UTF-8`) + accept(`text/html`, `text/xml`) should select(`text/xml` withCharset `UTF-8`) + accept(`text/xml`, `text/html`) should select(`text/xml` withCharset `UTF-8`) + accept(`text/plain`, `text/xml`) should select(`text/xml` withCharset `UTF-8`) + accept(`text/plain`, `text/html`) should select(`text/html` withCharset `UTF-8`) } """Accept: text/html, text/plain;q=0.8, application/*;q=.5, *;q= .2 |Accept-Charset: UTF-16""" test { accept ⇒ - accept(`text/plain`, `text/html`, `audio/ogg`) should select(`text/html`, `UTF-16`) - accept(`text/plain`, `text/html` withCharset `UTF-8`, `audio/ogg`) should select(`text/plain`, `UTF-16`) - accept(`audio/ogg`, `application/javascript`, `text/plain` withCharset `UTF-8`) should select(`application/javascript`, `UTF-16`) - accept(`image/gif`, `application/javascript`) should select(`application/javascript`, `UTF-16`) + accept(`text/plain`, `text/html`, `audio/ogg`) should select(`text/html` withCharset `UTF-16`) + accept(`text/plain`, `text/html` withCharset `UTF-8`, `audio/ogg`) should select(`text/plain` withCharset `UTF-16`) + accept(`audio/ogg`, `application/javascript`, `text/plain` withCharset `UTF-8`) should select(`application/javascript` withCharset `UTF-16`) + accept(`image/gif`, `application/javascript`) should select(`application/javascript` withCharset `UTF-16`) accept(`image/gif`, `audio/ogg`) should select(`image/gif`) } + + "Accept: text/xml, text/plain" test { accept ⇒ + accept(`text/plain` withCharset `UTF-16`) should select(`text/plain` withCharset `UTF-16`) + accept(`text/plain`, `text/xml`) should select(`text/plain` withCharset `UTF-8`) + accept(`text/xml`, `text/plain`) should select(`text/xml` withCharset `UTF-8`) + } } - def testHeaders[U](headers: HttpHeader*)(body: ((ContentType*) ⇒ Option[ContentType]) ⇒ U): U = { + def testHeaders[U](headers: HttpHeader*)(body: ((Alternative*) ⇒ Option[ContentType]) ⇒ U): U = { val request = HttpRequest(headers = headers.toVector) - body { contentTypes ⇒ + body { alternatives ⇒ import scala.concurrent.ExecutionContext.Implicits.global // creates a pseudo marshaller for X, that applies for all the given content types trait X object X extends X implicit val marshallers: ToEntityMarshaller[X] = - Marshaller.oneOf(contentTypes map { - case ct @ ContentType(mt, Some(cs)) ⇒ - Marshaller.withFixedCharset(mt, cs)((s: X) ⇒ HttpEntity(ct, "The X")) - case ContentType(mt, None) ⇒ - mt.encoding match { - case Encoding.Open ⇒ Marshaller.withOpenCharset(mt)((s: X, cs) ⇒ HttpEntity(ContentType(mt, cs), "The X")) - case _ ⇒ Marshaller.withOpenCharset(mt)((s: X, _) ⇒ HttpEntity(ContentType(mt, None), "The X")) - } + Marshaller.oneOf(alternatives map { + case Alternative.ContentType(ct) ⇒ Marshaller.withFixedContentType(ct)((s: X) ⇒ HttpEntity(ct, ByteString("The X"))) + case Alternative.MediaType(mt) ⇒ Marshaller.withOpenCharset(mt)((s: X, cs) ⇒ HttpEntity(mt withCharset cs, "The X")) }: _*) Await.result(Marshal(X).toResponseFor(request) @@ -142,11 +153,10 @@ class ContentNegotiationSpec extends FreeSpec with Matchers { } def reject = equal(None) - def select(mediaType: MediaType, charset: HttpCharset) = equal(Some(ContentType(mediaType, charset))) - def select(mediaType: MediaType) = equal(Some(ContentType(mediaType, None))) + def select(contentType: ContentType) = equal(Some(contentType)) implicit class AddStringToIn(example: String) { - def test(body: ((ContentType*) ⇒ Option[ContentType]) ⇒ Unit): Unit = example in { + def test(body: ((Alternative*) ⇒ Option[ContentType]) ⇒ Unit): Unit = example in { val headers = if (example != "(without headers)") { example.stripMarginWithNewline("\n").split('\n').toList map { rawHeader ⇒ diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/MarshallingSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/MarshallingSpec.scala index 41cde09922..b833b3834e 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/MarshallingSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/MarshallingSpec.scala @@ -4,14 +4,14 @@ package akka.http.scaladsl.marshalling -import akka.http.scaladsl.testkit.MarshallingTestUtils -import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._ - import scala.collection.immutable.ListMap import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers } +import akka.util.ByteString import akka.actor.ActorSystem import akka.stream.ActorMaterializer import akka.stream.scaladsl.Source +import akka.http.scaladsl.testkit.MarshallingTestUtils +import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._ import akka.http.impl.util._ import akka.http.scaladsl.model._ import headers._ @@ -32,7 +32,7 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with } "FormDataMarshaller should marshal FormData instances to application/x-www-form-urlencoded content" in { marshal(FormData(Map("name" -> "Bob", "pass" -> "hällo", "admin" -> ""))) shouldEqual - HttpEntity(ContentType(`application/x-www-form-urlencoded`, `UTF-8`), "name=Bob&pass=h%C3%A4llo&admin=") + HttpEntity(`application/x-www-form-urlencoded` withCharset `UTF-8`, "name=Bob&pass=h%C3%A4llo&admin=") } } @@ -52,7 +52,7 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with "multipartMarshaller should correctly marshal multipart content with" - { "one empty part" in { marshal(Multipart.General(`multipart/mixed`, Multipart.General.BodyPart.Strict(""))) shouldEqual HttpEntity( - contentType = ContentType(`multipart/mixed` withBoundary randomBoundary), + contentType = `multipart/mixed` withBoundary randomBoundary withCharset `UTF-8`, string = s"""--$randomBoundary |Content-Type: text/plain; charset=UTF-8 | @@ -61,10 +61,10 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with } "one non-empty part" in { marshal(Multipart.General(`multipart/alternative`, Multipart.General.BodyPart.Strict( - entity = HttpEntity(ContentType(`text/plain`, `UTF-8`), "test@there.com"), + entity = HttpEntity(ContentTypes.`text/plain(UTF-8)`, "test@there.com"), headers = `Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")) :: Nil))) shouldEqual HttpEntity( - contentType = ContentType(`multipart/alternative` withBoundary randomBoundary), + contentType = `multipart/alternative` withBoundary randomBoundary withCharset `UTF-8`, string = s"""--$randomBoundary |Content-Type: text/plain; charset=UTF-8 |Content-Disposition: form-data; name=email @@ -74,12 +74,12 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with } "two different parts" in { marshal(Multipart.General(`multipart/related`, - Multipart.General.BodyPart.Strict(HttpEntity(ContentType(`text/plain`, Some(`US-ASCII`)), "first part, with a trailing linebreak\r\n")), + Multipart.General.BodyPart.Strict(HttpEntity(`text/plain` withCharset `US-ASCII`, "first part, with a trailing linebreak\r\n")), Multipart.General.BodyPart.Strict( - HttpEntity(ContentType(`application/octet-stream`), "filecontent"), + HttpEntity(`application/octet-stream`, ByteString("filecontent")), RawHeader("Content-Transfer-Encoding", "binary") :: Nil))) shouldEqual HttpEntity( - contentType = ContentType(`multipart/related` withBoundary randomBoundary), + contentType = `multipart/related` withBoundary randomBoundary withCharset `UTF-8`, string = s"""--$randomBoundary |Content-Type: text/plain; charset=US-ASCII | @@ -100,7 +100,7 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with "surname" -> HttpEntity("Mike"), "age" -> marshal(42)))) shouldEqual HttpEntity( - contentType = ContentType(`multipart/form-data` withBoundary randomBoundary), + contentType = `multipart/form-data` withBoundary randomBoundary withCharset `UTF-8`, string = s"""--$randomBoundary |Content-Type: text/plain; charset=UTF-8 |Content-Disposition: form-data; name=surname @@ -116,14 +116,14 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with "two fields having a custom `Content-Disposition`" in { marshal(Multipart.FormData(Source(List( - Multipart.FormData.BodyPart("attachment[0]", HttpEntity(`text/csv`, "name,age\r\n\"John Doe\",20\r\n"), + Multipart.FormData.BodyPart("attachment[0]", HttpEntity(`text/csv` withCharset `UTF-8`, "name,age\r\n\"John Doe\",20\r\n"), Map("filename" -> "attachment.csv")), Multipart.FormData.BodyPart("attachment[1]", HttpEntity("naice!".getBytes), Map("filename" -> "attachment2.csv"), List(RawHeader("Content-Transfer-Encoding", "binary"))))))) shouldEqual HttpEntity( - contentType = ContentType(`multipart/form-data` withBoundary randomBoundary), + contentType = `multipart/form-data` withBoundary randomBoundary withCharset `UTF-8`, string = s"""--$randomBoundary - |Content-Type: text/csv + |Content-Type: text/csv; charset=UTF-8 |Content-Disposition: form-data; filename=attachment.csv; name="attachment[0]" | |name,age diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/CodingDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/CodingDirectivesSpec.scala index 8049e196d2..d237a31ff0 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/CodingDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/CodingDirectivesSpec.scala @@ -201,7 +201,7 @@ class CodingDirectivesSpec extends RoutingSpec with Inside { () ⇒ text.grouped(8).map { chars ⇒ Chunk(chars.mkString): ChunkStreamPart } - val chunkedTextEntity = HttpEntity.Chunked(MediaTypes.`text/plain`, Source(textChunks)) + val chunkedTextEntity = HttpEntity.Chunked(ContentTypes.`text/plain(UTF-8)`, Source(textChunks)) Post() ~> `Accept-Encoding`(gzip) ~> { encodeResponseWith(Gzip) { @@ -348,6 +348,13 @@ class CodingDirectivesSpec extends RoutingSpec with Inside { response should haveContentEncoding(gzip) strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped) } + + Get("/") ~> `Accept-Encoding`(HttpEncodingRange.`*`, deflate withQValue 0.2) ~> { + encodeResponseWith(Deflate, Gzip) { yeah } + } ~> check { + response should haveContentEncoding(gzip) + strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped) + } } "reject the request if it has an Accept-Encoding header with an encoding that doesn't match" in { Get("/") ~> `Accept-Encoding`(deflate) ~> { diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala index d7d5916b1c..68b6b4c591 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala @@ -9,6 +9,7 @@ import java.io.File import scala.concurrent.duration._ import scala.concurrent.{ ExecutionContext, Future } import scala.util.Properties +import akka.util.ByteString import org.scalatest.matchers.Matcher import org.scalatest.{ Inside, Inspectors } import akka.http.scaladsl.model.MediaTypes._ @@ -37,7 +38,7 @@ class FileAndResourceDirectivesSpec extends RoutingSpec with Inspectors with Ins writeAllText("This is PDF", file) Get() ~> getFromFile(file.getPath) ~> check { mediaType shouldEqual `application/pdf` - definedCharset shouldEqual None + charsetOption shouldEqual None responseAs[String] shouldEqual "This is PDF" headers should contain(`Last-Modified`(DateTime(file.lastModified))) } @@ -71,7 +72,7 @@ class FileAndResourceDirectivesSpec extends RoutingSpec with Inspectors with Ins try { writeAllText("ABCDEFGHIJKLMNOPQRSTUVWXYZ", file) val rangeHeader = Range(ByteRange(1, 10), ByteRange.suffix(10)) - Get() ~> addHeader(rangeHeader) ~> getFromFile(file, ContentTypes.`text/plain`) ~> check { + Get() ~> addHeader(rangeHeader) ~> getFromFile(file, ContentTypes.`text/plain(UTF-8)`) ~> check { status shouldEqual StatusCodes.PartialContent header[`Content-Range`] shouldEqual None mediaType.withParams(Map.empty) shouldEqual `multipart/byteranges` diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileUploadDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileUploadDirectivesSpec.scala index 23f6f5d290..396bb3f177 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileUploadDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileUploadDirectivesSpec.scala @@ -6,7 +6,6 @@ package akka.http.scaladsl.server.directives import java.io.{ FileInputStream, File } import java.nio.charset.StandardCharsets - import akka.http.scaladsl.model._ import akka.http.scaladsl.server.{ MissingFormFieldRejection, RoutingSpec } import akka.util.ByteString @@ -22,7 +21,7 @@ class FileUploadDirectivesSpec extends RoutingSpec { val simpleMultipartUpload = Multipart.FormData(Multipart.FormData.BodyPart.Strict( "fieldName", - HttpEntity(MediaTypes.`text/xml`, xml), + HttpEntity(ContentTypes.`text/xml(UTF-8)`, xml), Map("filename" -> "age.xml"))) @volatile var file: Option[File] = None @@ -36,7 +35,7 @@ class FileUploadDirectivesSpec extends RoutingSpec { } } ~> check { file.isDefined === true - responseAs[String] === FileInfo("fieldName", "age.xml", ContentTypes.`text/xml`).toString + responseAs[String] === FileInfo("fieldName", "age.xml", ContentTypes.`text/xml(UTF-8)`).toString read(file.get) === xml } } finally { @@ -72,7 +71,7 @@ class FileUploadDirectivesSpec extends RoutingSpec { val multipartForm = Multipart.FormData(Multipart.FormData.BodyPart.Strict( "field1", - HttpEntity(MediaTypes.`text/plain`, str1), + HttpEntity(ContentTypes.`text/plain(UTF-8)`, str1), Map("filename" -> "data1.txt"))) Post("/", multipartForm) ~> route ~> check { @@ -93,11 +92,11 @@ class FileUploadDirectivesSpec extends RoutingSpec { Multipart.FormData( Multipart.FormData.BodyPart.Strict( "field1", - HttpEntity(MediaTypes.`text/plain`, str1), + HttpEntity(ContentTypes.`text/plain(UTF-8)`, str1), Map("filename" -> "data1.txt")), Multipart.FormData.BodyPart.Strict( "field1", - HttpEntity(MediaTypes.`text/plain`, str2), + HttpEntity(ContentTypes.`text/plain(UTF-8)`, str2), Map("filename" -> "data2.txt"))) Post("/", multipartForm) ~> route ~> check { @@ -130,7 +129,7 @@ class FileUploadDirectivesSpec extends RoutingSpec { val multipartForm = Multipart.FormData(Multipart.FormData.BodyPart.Strict( "field1", - HttpEntity(MediaTypes.`text/plain`, str1), + HttpEntity(ContentTypes.`text/plain(UTF-8)`, str1), Map("filename" -> "data1.txt"))) Post("/", multipartForm) ~> route ~> check { diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FormFieldDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FormFieldDirectivesSpec.scala index e0a073b68b..cbf19f1b03 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FormFieldDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FormFieldDirectivesSpec.scala @@ -24,18 +24,18 @@ class FormFieldDirectivesSpec extends RoutingSpec { val multipartForm = Multipart.FormData { Map( "firstName" -> HttpEntity("Mike"), - "age" -> HttpEntity(`text/xml`, "42"), + "age" -> HttpEntity(ContentTypes.`text/xml(UTF-8)`, "42"), "VIPBoolean" -> HttpEntity("true")) } val multipartFormWithTextHtml = Multipart.FormData { Map( "firstName" -> HttpEntity("Mike"), - "age" -> HttpEntity(`text/xml`, "42"), - "VIP" -> HttpEntity(`text/html`, "yes"), + "age" -> HttpEntity(ContentTypes.`text/xml(UTF-8)`, "42"), + "VIP" -> HttpEntity(ContentTypes.`text/html(UTF-8)`, "yes"), "super" -> HttpEntity("no")) } val multipartFormWithFile = Multipart.FormData( - Multipart.FormData.BodyPart.Strict("file", HttpEntity(MediaTypes.`text/xml`, "42"), + Multipart.FormData.BodyPart.Strict("file", HttpEntity(ContentTypes.`text/xml(UTF-8)`, "42"), Map("filename" -> "age.xml"))) "The 'formFields' extraction directive" should { diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MarshallingDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MarshallingDirectivesSpec.scala index f27650a7fc..776195f46f 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MarshallingDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MarshallingDirectivesSpec.scala @@ -12,10 +12,10 @@ import akka.http.scaladsl.unmarshalling._ import akka.http.scaladsl.marshalling._ import akka.http.scaladsl.model._ import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ +import spray.json.DefaultJsonProtocol._ import MediaTypes._ import HttpCharsets._ import headers._ -import spray.json.DefaultJsonProtocol._ class MarshallingDirectivesSpec extends RoutingSpec with Inside { import ScalaXmlSupport._ @@ -27,9 +27,10 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside { case x ⇒ { val i = x.text.toInt; require(i >= 0); i } } + val `text/xxml` = MediaType.customWithFixedCharset("text", "xxml", `UTF-8`) implicit val IntMarshaller: ToEntityMarshaller[Int] = - Marshaller.oneOf(ContentType(`application/xhtml+xml`), ContentType(`text/xml`, `UTF-8`)) { contentType ⇒ - nodeSeqMarshaller(contentType).wrap(contentType) { (i: Int) ⇒ { i } } + Marshaller.oneOf[MediaType.NonBinary, Int, MessageEntity](`application/xhtml+xml`, `text/xxml`) { mediaType ⇒ + nodeSeqMarshaller(mediaType).wrap(mediaType) { (i: Int) ⇒ { i } } } "The 'entityAs' directive" should { @@ -44,7 +45,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside { } ~> check { rejection shouldEqual RequestEntityExpectedRejection } } "return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope" in { - Put("/", HttpEntity(`text/css`, "

cool

")) ~> { + Put("/", HttpEntity(`text/css` withCharset `UTF-8`, "

cool

")) ~> { entity(as[NodeSeq]) { echoComplete } } ~> check { rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`)) @@ -56,7 +57,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside { } } "cancel UnsupportedRequestContentTypeRejections if a subsequent `entity` directive succeeds" in { - Put("/", HttpEntity(`text/plain`, "yeah")) ~> { + Put("/", HttpEntity(ContentTypes.`text/plain(UTF-8)`, "yeah")) ~> { entity(as[NodeSeq]) { _ ⇒ completeOk } ~ entity(as[String]) { _ ⇒ validate(false, "Problem") { completeOk } } } ~> check { rejection shouldEqual ValidationRejection("Problem") } @@ -71,7 +72,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside { } } "return a MalformedRequestContentRejection if unmarshalling failed due to a not further classified error" in { - Put("/", HttpEntity(`text/xml`, " { + Put("/", HttpEntity(ContentTypes.`text/xml(UTF-8)`, " { entity(as[NodeSeq]) { _ ⇒ completeOk } } ~> check { rejection shouldEqual MalformedRequestContentRejection( @@ -89,7 +90,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside { } ~> check { responseAs[String] shouldEqual "None" } } "return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope (for Option[T]s)" in { - Put("/", HttpEntity(`text/css`, "

cool

")) ~> { + Put("/", HttpEntity(`text/css` withCharset `UTF-8`, "

cool

")) ~> { entity(as[Option[NodeSeq]]) { echoComplete } } ~> check { rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`)) @@ -105,13 +106,13 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside { val route = entity(as[Person]) { echoComplete } - Put("/", HttpEntity(`text/xml`, "Peter Xml")) ~> route ~> check { + Put("/", HttpEntity(ContentTypes.`text/xml(UTF-8)`, "Peter Xml")) ~> route ~> check { responseAs[String] shouldEqual "Person(Peter Xml)" } Put("/", HttpEntity(`application/json`, """{ "name": "Paul Json" }""")) ~> route ~> check { responseAs[String] shouldEqual "Person(Paul Json)" } - Put("/", HttpEntity(`text/plain`, """name = Sir Text }""")) ~> route ~> check { + Put("/", HttpEntity(ContentTypes.`text/plain(UTF-8)`, """name = Sir Text }""")) ~> route ~> check { rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`application/json`, `text/xml`)) } } @@ -125,7 +126,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside { } "return a UnacceptedResponseContentTypeRejection rejection if no acceptable marshaller is in scope" in { Get() ~> Accept(`text/css`) ~> completeWith(instanceOf[Int]) { prod ⇒ prod(42) } ~> check { - rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, ContentType(`text/xml`, `UTF-8`))) + rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, `text/xxml`)) } } "convert the response content to an accepted charset" in { @@ -139,19 +140,19 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside { def times2(x: Int) = x * 2 "support proper round-trip content unmarshalling/marshalling to and from a function" in ( - Put("/", HttpEntity(`text/html`, "42")) ~> Accept(`text/xml`) ~> handleWith(times2) - ~> check { responseEntity shouldEqual HttpEntity(ContentType(`text/xml`, `UTF-8`), "84") }) + Put("/", HttpEntity(ContentTypes.`text/html(UTF-8)`, "42")) ~> Accept(`text/xxml`) ~> handleWith(times2) + ~> check { responseEntity shouldEqual HttpEntity(`text/xxml`, "84") }) "result in UnsupportedRequestContentTypeRejection rejection if there is no unmarshaller supporting the requests charset" in ( - Put("/", HttpEntity(`text/xml`, "42")) ~> Accept(`text/xml`) ~> handleWith(times2) + Put("/", HttpEntity(ContentTypes.`text/xml(UTF-8)`, "42")) ~> Accept(`text/xml`) ~> handleWith(times2) ~> check { rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(ContentTypeRange(`text/xml`, iso88592), `text/html`)) }) "result in an UnacceptedResponseContentTypeRejection rejection if there is no marshaller supporting the requests Accept-Charset header" in ( - Put("/", HttpEntity(`text/html`, "42")) ~> addHeaders(Accept(`text/xml`), `Accept-Charset`(`UTF-16`)) ~> + Put("/", HttpEntity(ContentTypes.`text/html(UTF-8)`, "42")) ~> addHeaders(Accept(`text/xxml`), `Accept-Charset`(`UTF-16`)) ~> handleWith(times2) ~> check { - rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, ContentType(`text/xml`, `UTF-8`))) + rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, `text/xxml`)) }) } @@ -163,7 +164,7 @@ class MarshallingDirectivesSpec extends RoutingSpec with Inside { "render JSON with UTF-8 encoding if no `Accept-Charset` request header is present" in { Get() ~> complete(foo) ~> check { - responseEntity shouldEqual HttpEntity(ContentType(`application/json`, `UTF-8`), foo.toJson.prettyPrint) + responseEntity shouldEqual HttpEntity(`application/json`, foo.toJson.prettyPrint) } } "reject JSON rendering if an `Accept-Charset` request header requests a non-UTF-8 encoding" in { diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MiscDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MiscDirectivesSpec.scala index deacba2401..74f55dd8c6 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MiscDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MiscDirectivesSpec.scala @@ -34,10 +34,6 @@ class MiscDirectivesSpec extends RoutingSpec { "the selectPreferredLanguage directive" should { "Accept-Language: de, en" test { selectFrom ⇒ selectFrom("de", "en") shouldEqual "de" - selectFrom("en", "de") shouldEqual "de" - } - "Accept-Language: en, de" test { selectFrom ⇒ - selectFrom("de", "en") shouldEqual "en" selectFrom("en", "de") shouldEqual "en" } "Accept-Language: en, de;q=.5" test { selectFrom ⇒ diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/RangeDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/RangeDirectivesSpec.scala index fb4aca5a37..11e5d59ac7 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/RangeDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/RangeDirectivesSpec.scala @@ -121,7 +121,7 @@ class RangeDirectivesSpec extends RoutingSpec with Inspectors with Inside { def entityData() = StreamUtils.oneTimeSource(Source.single(ByteString(content))) Get() ~> addHeader(Range(ByteRange(5, 10), ByteRange(0, 1), ByteRange(1, 2))) ~> { - wrs { complete(HttpEntity.Default(MediaTypes.`text/plain`, content.length, entityData())) } + wrs { complete(HttpEntity.Default(ContentTypes.`text/plain(UTF-8)`, content.length, entityData())) } } ~> check { header[`Content-Range`] should be(None) val parts = Await.result(responseAs[Multipart.ByteRanges].parts.grouped(1000).runWith(Sink.head), 1.second) diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/RouteDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/RouteDirectivesSpec.scala index 7de787fb30..c46c0cf3c9 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/RouteDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/RouteDirectivesSpec.scala @@ -6,9 +6,9 @@ package akka.http.scaladsl.server.directives import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport -import akka.stream.scaladsl.Sink import org.scalatest.FreeSpec import scala.concurrent.{ Future, Promise } +import akka.testkit.EventFilter import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._ import akka.http.scaladsl.marshalling._ import akka.http.scaladsl.server._ @@ -16,9 +16,6 @@ import akka.http.scaladsl.model._ import akka.http.impl.util._ import headers._ import StatusCodes._ -import MediaTypes._ -import scala.xml.NodeSeq -import akka.testkit.EventFilter class RouteDirectivesSpec extends FreeSpec with GenericRoutingSpec { @@ -122,7 +119,8 @@ class RouteDirectivesSpec extends FreeSpec with GenericRoutingSpec { } ~> check { response shouldEqual HttpResponse( status = 302, - entity = HttpEntity(`text/html`, "The requested resource temporarily resides under this URI."), + entity = HttpEntity(ContentTypes.`text/html(UTF-8)`, + "The requested resource temporarily resides under this URI."), headers = Location("/foo") :: Nil) } } diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/unmarshalling/MultipartUnmarshallersSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/unmarshalling/MultipartUnmarshallersSpec.scala index ae3213c7c4..288d9bc7b7 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/unmarshalling/MultipartUnmarshallersSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/unmarshalling/MultipartUnmarshallersSpec.scala @@ -18,6 +18,7 @@ import akka.http.scaladsl.util.FastFuture._ import akka.http.impl.util._ import akka.http.scaladsl.model.headers._ import MediaTypes._ +import HttpCharsets._ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAfterAll with ScalatestUtils { implicit val system = ActorSystem(getClass.getSimpleName) @@ -28,13 +29,13 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf "multipartGeneralUnmarshaller should correctly unmarshal 'multipart/*' content with" - { "an empty part" in { - Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC", + Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`, """--XYZABC |--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts( Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`))) } "two empty parts" in { - Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC", + Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`, """--XYZABC |--XYZABC |--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts( @@ -42,15 +43,15 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`))) } "a part without entity and missing header separation CRLF" in { - Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC", + Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`, """--XYZABC |Content-type: text/xml |Age: 12 |--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts( - Multipart.General.BodyPart.Strict(HttpEntity.empty(MediaTypes.`text/xml`), List(Age(12)))) + Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/xml(UTF-8)`), List(Age(12)))) } "an implicitly typed part (without headers) (Strict)" in { - Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC", + Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`, """--XYZABC | |Perfectly fine part content. @@ -63,12 +64,12 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf |Perfectly fine part content. |--XYZABC--""".stripMarginWithNewline("\r\n") val byteStrings = content.map(c ⇒ ByteString(c.toString)) // one-char ByteStrings - Unmarshal(HttpEntity.Default(`multipart/mixed` withBoundary "XYZABC", content.length, Source(byteStrings))) + Unmarshal(HttpEntity.Default(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`, content.length, Source(byteStrings))) .to[Multipart.General] should haveParts( Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "Perfectly fine part content."))) } "one non-empty form-data part" in { - Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-", + Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-" withCharset `UTF-8`, """--- |Content-type: text/plain; charset=UTF8 |content-disposition: form-data; name="email" @@ -80,7 +81,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf List(`Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email"))))) } "two different parts" in { - Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345", + Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345" withCharset `UTF-8`, """--12345 | |first part, with a trailing newline @@ -92,10 +93,11 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf |filecontent |--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts( Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, with a trailing newline\r\n")), - Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "filecontent"), List(RawHeader("Content-Transfer-Encoding", "binary")))) + Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, ByteString("filecontent")), + List(RawHeader("Content-Transfer-Encoding", "binary")))) } "illegal headers" in ( - Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC", + Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC" withCharset `UTF-8`, """--XYZABC |Date: unknown |content-disposition: form-data; name=email @@ -107,7 +109,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf List(RawHeader("date", "unknown"), `Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")))))) "a full example (Strict)" in { - Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345", + Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345" withCharset `UTF-8`, """preamble and |more preamble |--12345 @@ -121,7 +123,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf |epilogue and |more epilogue""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts( Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")), - Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "second part, explicitly typed"))) + Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, ByteString("second part, explicitly typed")))) } "a full example (Default)" in { val content = """preamble and @@ -137,13 +139,13 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf |epilogue and |more epilogue""".stripMarginWithNewline("\r\n") val byteStrings = content.map(c ⇒ ByteString(c.toString)) // one-char ByteStrings - Unmarshal(HttpEntity.Default(`multipart/mixed` withBoundary "12345", content.length, Source(byteStrings))) + Unmarshal(HttpEntity.Default(`multipart/mixed` withBoundary "12345" withCharset `UTF-8`, content.length, Source(byteStrings))) .to[Multipart.General] should haveParts( Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")), - Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "second part, explicitly typed"))) + Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, ByteString("second part, explicitly typed")))) } "a boundary with spaces" in { - Unmarshal(HttpEntity(`multipart/mixed` withBoundary "simple boundary", + Unmarshal(HttpEntity(`multipart/mixed` withBoundary "simple boundary" withCharset `UTF-8`, """--simple boundary |--simple boundary--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts( Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`))) @@ -152,17 +154,17 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf "multipartGeneralUnmarshaller should reject illegal multipart content with" - { "an empty entity" in { - Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC", ByteString.empty)) + Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`, ByteString.empty)) .to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity" } "an entity without initial boundary" in { - Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC", + Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC" withCharset `UTF-8`, """this is |just preamble text""".stripMarginWithNewline("\r\n"))) .to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity" } "a stray boundary" in { - Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "ABC", + Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "ABC" withCharset `UTF-8`, """--ABC |Content-type: text/plain; charset=UTF8 |--ABCContent-type: application/json @@ -171,7 +173,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf .to[Multipart.General].failed, 1.second).getMessage shouldEqual "Illegal multipart boundary in message content" } "duplicate Content-Type header" in { - Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-", + Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-" withCharset `UTF-8`, """--- |Content-type: text/plain; charset=UTF8 |Content-type: application/json @@ -183,7 +185,7 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf "multipart part must not contain more than one Content-Type header" } "a missing header-separating CRLF (in Strict entity)" in { - Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-", + Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-" withCharset `UTF-8`, """--- |not good here |-----""".stripMarginWithNewline("\r\n"))) @@ -197,27 +199,27 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf |not ok |-----""".stripMarginWithNewline("\r\n") val byteStrings = content.map(c ⇒ ByteString(c.toString)) // one-char ByteStrings - val contentType = `multipart/form-data` withBoundary "-" + val contentType = `multipart/form-data` withBoundary "-" withCharset `UTF-8` Await.result(Unmarshal(HttpEntity.Default(contentType, content.length, Source(byteStrings))) .to[Multipart.General] .flatMap(_ toStrict 1.second).failed, 1.second).getMessage shouldEqual "Illegal character ' ' in header name" } "a boundary with a trailing space" in { Await.result( - Unmarshal(HttpEntity(`multipart/mixed` withBoundary "simple boundary ", ByteString.empty)) + Unmarshal(HttpEntity(`multipart/mixed` withBoundary "simple boundary " withCharset `UTF-8`, ByteString.empty)) .to[Multipart.General].failed, 1.second).getMessage shouldEqual "requirement failed: 'boundary' parameter of multipart Content-Type must not end with a space char" } "a boundary with an illegal character" in { Await.result( - Unmarshal(HttpEntity(`multipart/mixed` withBoundary "simple&boundary", ByteString.empty)) + Unmarshal(HttpEntity(`multipart/mixed` withBoundary "simple&boundary" withCharset `UTF-8`, ByteString.empty)) .to[Multipart.General].failed, 1.second).getMessage shouldEqual "requirement failed: 'boundary' parameter of multipart Content-Type contains illegal character '&'" } } "multipartByteRangesUnmarshaller should correctly unmarshal multipart/byteranges content with two different parts" in { - Unmarshal(HttpEntity(`multipart/byteranges` withBoundary "12345", + Unmarshal(HttpEntity(`multipart/byteranges` withBoundary "12345" withCharset `UTF-8`, """--12345 |Content-Range: bytes 0-2/26 |Content-Type: text/plain @@ -229,24 +231,24 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf | |XYZ |--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.ByteRanges] should haveParts( - Multipart.ByteRanges.BodyPart.Strict(ContentRange(0, 2, 26), HttpEntity(ContentTypes.`text/plain`, "ABC")), - Multipart.ByteRanges.BodyPart.Strict(ContentRange(23, 25, 26), HttpEntity(ContentTypes.`text/plain`, "XYZ"))) + Multipart.ByteRanges.BodyPart.Strict(ContentRange(0, 2, 26), HttpEntity(ContentTypes.`text/plain(UTF-8)`, "ABC")), + Multipart.ByteRanges.BodyPart.Strict(ContentRange(23, 25, 26), HttpEntity(ContentTypes.`text/plain(UTF-8)`, "XYZ"))) } "multipartFormDataUnmarshaller should correctly unmarshal 'multipart/form-data' content" - { "with one element" in { - Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC", + Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC" withCharset `UTF-8`, """--XYZABC |content-disposition: form-data; name=email | |test@there.com |--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.FormData] should haveParts( - Multipart.FormData.BodyPart.Strict("email", HttpEntity(ContentTypes.`application/octet-stream`, "test@there.com"))) + Multipart.FormData.BodyPart.Strict("email", HttpEntity(`application/octet-stream`, ByteString("test@there.com")))) } "with a file" in { Unmarshal { HttpEntity.Default( - contentType = `multipart/form-data` withBoundary "XYZABC", + contentType = `multipart/form-data` withBoundary "XYZABC" withCharset `UTF-8`, contentLength = 1, // not verified during unmarshalling data = Source { List( @@ -270,8 +272,8 @@ class MultipartUnmarshallersSpec extends FreeSpec with Matchers with BeforeAndAf }) }) }.to[Multipart.FormData].flatMap(_.toStrict(1.second)) should haveParts( - Multipart.FormData.BodyPart.Strict("email", HttpEntity(ContentTypes.`application/octet-stream`, "test@there.com")), - Multipart.FormData.BodyPart.Strict("userfile", HttpEntity(MediaTypes.`application/pdf`, "filecontent"), Map("filename" -> "test.dat"), + Multipart.FormData.BodyPart.Strict("email", HttpEntity(`application/octet-stream`, ByteString("test@there.com"))), + Multipart.FormData.BodyPart.Strict("userfile", HttpEntity(`application/pdf`, ByteString("filecontent")), Map("filename" -> "test.dat"), List( RawHeader("Content-Transfer-Encoding", "binary"), RawHeader("Content-Additional-1", "anything"), diff --git a/akka-http/src/main/scala/akka/http/impl/server/RejectionHandlerWrapper.scala b/akka-http/src/main/scala/akka/http/impl/server/RejectionHandlerWrapper.scala index 3e76865158..ecf631a6cf 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RejectionHandlerWrapper.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RejectionHandlerWrapper.scala @@ -52,7 +52,7 @@ private[http] class RejectionHandlerWrapper(javaHandler: server.RejectionHandler case RequestEntityExpectedRejection ⇒ handleRequestEntityExpectedRejection(ctx) case UnacceptedResponseContentTypeRejection(supported) ⇒ - handleUnacceptedResponseContentTypeRejection(ctx, supported.toList.toSeq.asJava) + handleUnacceptedResponseContentTypeRejection(ctx, supported.toList.map(_.format).toSeq.asJava) case UnacceptedResponseEncodingRejection(supported) ⇒ handleUnacceptedResponseEncodingRejection(ctx, supported.toList.toSeq.asJava) case AuthenticationFailedRejection(cause, challenge) ⇒ diff --git a/akka-http/src/main/scala/akka/http/impl/server/RequestContextImpl.scala b/akka-http/src/main/scala/akka/http/impl/server/RequestContextImpl.scala index 2e2dad778c..c40cbf0e43 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/RequestContextImpl.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/RequestContextImpl.scala @@ -29,7 +29,7 @@ private[http] final case class RequestContextImpl(underlying: ScalaRequestContex case r: RouteResultImpl ⇒ r.underlying }(executionContext()) def complete(text: String): RouteResult = underlying.complete(text) - def complete(contentType: ContentType, text: String): RouteResult = + def complete(contentType: ContentType.NonBinary, text: String): RouteResult = underlying.complete(HttpEntity(contentType.asScala, text)) def completeWithStatus(statusCode: Int): RouteResult = diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/Marshallers.scala b/akka-http/src/main/scala/akka/http/javadsl/server/Marshallers.scala index c9f04f4f43..e978f80aaf 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/Marshallers.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/Marshallers.scala @@ -28,7 +28,14 @@ object Marshallers { * Creates a marshaller by specifying a media type and conversion function from ``T`` to String. * The charset for encoding the response will be negotiated with the client. */ - def toEntityString[T](mediaType: MediaType, convert: function.Function[T, String]): Marshaller[T] = + def toEntityString[T](mediaType: MediaType.WithOpenCharset, convert: function.Function[T, String]): Marshaller[T] = + MarshallerImpl(_ ⇒ ScalaMarshaller.stringMarshaller(mediaType.asScala).compose[T](convert(_))) + + /** + * Creates a marshaller by specifying a media type and conversion function from ``T`` to String. + * The charset for encoding the response will be negotiated with the client. + */ + def toEntityString[T](mediaType: MediaType.WithFixedCharset, convert: function.Function[T, String]): Marshaller[T] = MarshallerImpl(_ ⇒ ScalaMarshaller.stringMarshaller(mediaType.asScala).compose[T](convert(_))) /** @@ -48,7 +55,7 @@ object Marshallers { */ def toEntity[T](contentType: ContentType, convert: function.Function[T, ResponseEntity]): Marshaller[T] = MarshallerImpl { _ ⇒ - ScalaMarshaller.withFixedCharset(contentType.mediaType().asScala, contentType.charset().asScala)(t ⇒ + ScalaMarshaller.withFixedContentType(contentType.asScala)(t ⇒ HttpResponse.create().withStatus(200).withEntity(convert(t)).asScala) } @@ -57,7 +64,6 @@ object Marshallers { */ def toResponse[T](contentType: ContentType, convert: function.Function[T, HttpResponse]): Marshaller[T] = MarshallerImpl { _ ⇒ - ScalaMarshaller.withFixedCharset(contentType.mediaType().asScala, contentType.charset().asScala)(t ⇒ - convert(t).asScala) + ScalaMarshaller.withFixedContentType(contentType.asScala)(t ⇒ convert(t).asScala) } } diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/RejectionHandler.scala b/akka-http/src/main/scala/akka/http/javadsl/server/RejectionHandler.scala index 975efaa877..85e2738451 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/RejectionHandler.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/RejectionHandler.scala @@ -7,7 +7,7 @@ package akka.http.javadsl.server import java.{ lang ⇒ jl } import akka.http.impl.server.PassRejectionRouteResult -import akka.http.javadsl.model.{ ContentType, ContentTypeRange, HttpMethod } +import akka.http.javadsl.model.{ ContentTypeRange, HttpMethod } import akka.http.javadsl.model.headers.{ HttpChallenge, ByteRange, HttpEncoding } import akka.http.scaladsl.server.Rejection @@ -122,9 +122,9 @@ abstract class RejectionHandler { /** * Callback called to handle rejection created by marshallers. * Signals that the request was rejected because the service is not capable of producing a response entity whose - * content type is accepted by the client + * content type is accepted by the client. */ - def handleUnacceptedResponseContentTypeRejection(ctx: RequestContext, supported: jl.Iterable[ContentType]): RouteResult = passRejection() + def handleUnacceptedResponseContentTypeRejection(ctx: RequestContext, supported: jl.Iterable[String]): RouteResult = passRejection() /** * Callback called to handle rejection created by encoding filters. diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/RequestContext.scala b/akka-http/src/main/scala/akka/http/javadsl/server/RequestContext.scala index d62d220fed..e2b41703e8 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/RequestContext.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/RequestContext.scala @@ -4,11 +4,9 @@ package akka.http.javadsl.server +import scala.concurrent.{ ExecutionContext, Future } import akka.http.javadsl.model._ import akka.stream.Materializer -import akka.util.ByteString - -import scala.concurrent.{ ExecutionContext, Future } /** * The RequestContext represents the state of the request while it is routed through @@ -50,7 +48,7 @@ trait RequestContext { /** * Completes the request with the given string as an entity of the given type. */ - def complete(contentType: ContentType, text: String): RouteResult + def complete(contentType: ContentType.NonBinary, text: String): RouteResult /** * Completes the request with the given status code and no entity. diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/BasicDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/BasicDirectives.scala index e4ea1868c5..1c1692ca3c 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/directives/BasicDirectives.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/BasicDirectives.scala @@ -33,7 +33,7 @@ abstract class BasicDirectives extends BasicDirectivesBase { /** * A route that completes the request with a static text */ - def complete(contentType: ContentType, text: String): Route = + def complete(contentType: ContentType.NonBinary, text: String): Route = new OpaqueRoute() { def handle(ctx: RequestContext): RouteResult = ctx.complete(contentType, text) diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/directives/FileAndResourceDirectives.scala b/akka-http/src/main/scala/akka/http/javadsl/server/directives/FileAndResourceDirectives.scala index 1dd35c99b7..a4cd59be95 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/directives/FileAndResourceDirectives.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/directives/FileAndResourceDirectives.scala @@ -36,11 +36,6 @@ trait FileAndResourceRoute extends Route { */ def withContentType(contentType: ContentType): Route - /** - * Returns a variant of this route that responds with the given constant [[MediaType]]. - */ - def withContentType(mediaType: MediaType): Route - /** * Returns a variant of this route that uses the specified [[ContentTypeResolver]] to determine * which [[ContentType]] to respond with by file name. @@ -55,8 +50,6 @@ object FileAndResourceRoute { private[http] def apply(f: ContentTypeResolver ⇒ Route): FileAndResourceRoute = new FileAndResourceRouteWithDefaultResolver(f) with FileAndResourceRoute { def withContentType(contentType: ContentType): Route = resolveContentTypeWith(StaticContentTypeResolver(contentType)) - def withContentType(mediaType: MediaType): Route = withContentType(mediaType.toContentType) - def resolveContentTypeWith(resolver: ContentTypeResolver): Route = f(resolver) } @@ -66,8 +59,6 @@ object FileAndResourceRoute { private[http] def forFixedName(fileName: String)(f: ContentType ⇒ Route): FileAndResourceRoute = new FileAndResourceRouteWithDefaultResolver(resolver ⇒ f(resolver.resolve(fileName))) with FileAndResourceRoute { def withContentType(contentType: ContentType): Route = resolveContentTypeWith(StaticContentTypeResolver(contentType)) - def withContentType(mediaType: MediaType): Route = withContentType(mediaType.toContentType) - def resolveContentTypeWith(resolver: ContentTypeResolver): Route = f(resolver.resolve(fileName)) } } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/common/StrictForm.scala b/akka-http/src/main/scala/akka/http/scaladsl/common/StrictForm.scala index b0ca5f726d..7dadb70cd6 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/common/StrictForm.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/common/StrictForm.scala @@ -56,7 +56,9 @@ object StrictForm { def unmarshallerFromFSU[T](fsu: FromStringUnmarshaller[T]): FromStrictFormFieldUnmarshaller[T] = Unmarshaller.withMaterializer(implicit ec ⇒ implicit mat ⇒ { case FromString(value) ⇒ fsu(value) - case FromPart(value) ⇒ fsu(value.entity.data.decodeString(value.entity.contentType.charset.nioCharset.name)) + case FromPart(value) ⇒ + val charsetName = value.entity.contentType.asInstanceOf[ContentType.NonBinary].charset.nioCharset.name + fsu(value.entity.data.decodeString(charsetName)) }) @implicitNotFound("In order to unmarshal a `StrictForm.Field` to type `${T}` you need to supply a " + @@ -76,8 +78,10 @@ object StrictForm { implicit def fromFSU[T](implicit fsu: FromStringUnmarshaller[T]) = new FieldUnmarshaller[T] { def unmarshalString(value: String)(implicit ec: ExecutionContext, mat: Materializer) = fsu(value) - def unmarshalPart(value: Multipart.FormData.BodyPart.Strict)(implicit ec: ExecutionContext, mat: Materializer) = - fsu(value.entity.data.decodeString(value.entity.contentType.charset.nioCharset.name)) + def unmarshalPart(value: Multipart.FormData.BodyPart.Strict)(implicit ec: ExecutionContext, mat: Materializer) = { + val charsetName = value.entity.contentType.asInstanceOf[ContentType.NonBinary].charset.nioCharset.name + fsu(value.entity.data.decodeString(charsetName)) + } } implicit def fromFEU[T](implicit feu: FromEntityUnmarshaller[T]) = new FieldUnmarshaller[T] { diff --git a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/ContentTypeOverrider.scala b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/ContentTypeOverrider.scala new file mode 100644 index 0000000000..cff0c3df9f --- /dev/null +++ b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/ContentTypeOverrider.scala @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +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)) + } +} diff --git a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/Marshal.scala b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/Marshal.scala index e8fb1f5f24..ea358902e3 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/Marshal.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/Marshal.scala @@ -5,16 +5,15 @@ package akka.http.scaladsl.marshalling import scala.concurrent.{ ExecutionContext, Future } -import akka.http.scaladsl.model.HttpCharsets._ +import akka.http.scaladsl.server.ContentNegotiator import akka.http.scaladsl.model._ import akka.http.scaladsl.util.FastFuture._ object Marshal { def apply[T](value: T): Marshal[T] = new Marshal(value) - case class UnacceptableResponseContentTypeException(supported: Set[ContentType]) extends RuntimeException - - private class MarshallingWeight(val weight: Float, val marshal: () ⇒ HttpResponse) + case class UnacceptableResponseContentTypeException(supported: Set[ContentNegotiator.Alternative]) + extends RuntimeException } class Marshal[A](val value: A) { @@ -25,9 +24,9 @@ class Marshal[A](val value: A) { def to[B](implicit m: Marshaller[A, B], ec: ExecutionContext): Future[B] = m(value).fast.map { _.head match { - case Marshalling.WithFixedCharset(_, _, marshal) ⇒ marshal() - case Marshalling.WithOpenCharset(_, marshal) ⇒ marshal(HttpCharsets.`UTF-8`) - case Marshalling.Opaque(marshal) ⇒ marshal() + case Marshalling.WithFixedContentType(_, marshal) ⇒ marshal() + case Marshalling.WithOpenCharset(_, marshal) ⇒ marshal(HttpCharsets.`UTF-8`) + case Marshalling.Opaque(marshal) ⇒ marshal() } } @@ -36,40 +35,32 @@ class Marshal[A](val value: A) { */ def toResponseFor(request: HttpRequest)(implicit m: ToResponseMarshaller[A], ec: ExecutionContext): Future[HttpResponse] = { import akka.http.scaladsl.marshalling.Marshal._ - val mediaRanges = request.acceptedMediaRanges // cache for performance - val charsetRanges = request.acceptedCharsetRanges // cache for performance - def qValueMT(mediaType: MediaType) = request.qValueForMediaType(mediaType, mediaRanges) - def qValueCS(charset: HttpCharset) = request.qValueForCharset(charset, charsetRanges) + val ctn = ContentNegotiator(request.headers) m(value).fast.map { marshallings ⇒ - val defaultMarshallingWeight = new MarshallingWeight(0f, { () ⇒ - val supportedContentTypes = marshallings collect { - case Marshalling.WithFixedCharset(mt, cs, _) ⇒ ContentType(mt, cs) - case Marshalling.WithOpenCharset(mt, _) ⇒ ContentType(mt) - } - throw UnacceptableResponseContentTypeException(supportedContentTypes.toSet) - }) - def choose(acc: MarshallingWeight, mt: MediaType, cs: HttpCharset, marshal: () ⇒ HttpResponse) = { - val weight = math.min(qValueMT(mt), qValueCS(cs)) - if (weight > acc.weight) new MarshallingWeight(weight, marshal) else acc - } - val best = marshallings.foldLeft(defaultMarshallingWeight) { - case (acc, Marshalling.WithFixedCharset(mt, cs, marshal)) ⇒ - choose(acc, mt, cs, marshal) - case (acc, Marshalling.WithOpenCharset(mt, marshal)) ⇒ - def withCharset(cs: HttpCharset) = choose(acc, mt, cs, () ⇒ marshal(cs)) - // logic for choosing the charset adapted from http://tools.ietf.org/html/rfc7231#section-5.3.3 - if (qValueCS(`UTF-8`) == 1f) withCharset(`UTF-8`) // prefer UTF-8 if fully accepted - else charsetRanges match { - // pick the charset which the highest q-value (head of charsetRanges) if it isn't explicitly rejected - case (HttpCharsetRange.One(cs, qValue)) +: _ if qValue > 0f ⇒ withCharset(cs) - case _ ⇒ acc + val supportedAlternatives: List[ContentNegotiator.Alternative] = + marshallings.collect { + case Marshalling.WithFixedContentType(ct, _) ⇒ ContentNegotiator.Alternative(ct) + case Marshalling.WithOpenCharset(mt, _) ⇒ ContentNegotiator.Alternative(mt) + }(collection.breakOut) + val bestMarshal = { + if (supportedAlternatives.nonEmpty) { + ctn.pickContentType(supportedAlternatives).flatMap { + case best @ (_: ContentType.Binary | _: ContentType.WithFixedCharset) ⇒ + marshallings collectFirst { case Marshalling.WithFixedContentType(`best`, marshal) ⇒ marshal } + case best @ ContentType.WithCharset(bestMT, bestCS) ⇒ + marshallings collectFirst { + case Marshalling.WithFixedContentType(`best`, marshal) ⇒ marshal + case Marshalling.WithOpenCharset(`bestMT`, marshal) ⇒ () ⇒ marshal(bestCS) + } } - - case (acc, Marshalling.Opaque(marshal)) ⇒ - if (acc.weight == 0f) new MarshallingWeight(Float.MinPositiveValue, marshal) else acc + } else None + } orElse { + marshallings collectFirst { case Marshalling.Opaque(marshal) ⇒ marshal } + } getOrElse { + throw UnacceptableResponseContentTypeException(supportedAlternatives.toSet) } - best.marshal() + bestMarshal() } } } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/Marshaller.scala b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/Marshaller.scala index 008a35200d..f1312867f3 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/Marshaller.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/Marshaller.scala @@ -19,39 +19,50 @@ sealed abstract class Marshaller[-A, +B] { /** * Reuses this Marshaller's logic to produce a new Marshaller from another type `C` which overrides - * the produced [[ContentType]] with another one. - * Depending on whether the given [[ContentType]] has a defined charset or not and whether the underlying - * marshaller marshals with a fixed charset it can happen, that the wrapping becomes illegal. - * For example, a marshaller producing content encoded with UTF-16 cannot be wrapped with a [[ContentType]] - * that has a defined charset of UTF-8, since akka-http will never recode entities. + * the [[MediaType]] of the marshalling result with the given one. + * Note that not all wrappings are legal. f the underlying [[MediaType]] has constraints with regard to the + * charsets it allows the new [[MediaType]] must be compatible, since akka-http will never recode entities. * If the wrapping is illegal the [[Future]] produced by the resulting marshaller will contain a [[RuntimeException]]. */ - def wrap[C, D >: B](contentType: ContentType)(f: C ⇒ A)(implicit mto: MediaTypeOverrider[D]): Marshaller[C, D] = - wrapWithEC[C, D](contentType)(_ ⇒ f) + def wrap[C, D >: B](newMediaType: MediaType)(f: C ⇒ A)(implicit mto: ContentTypeOverrider[D]): Marshaller[C, D] = + wrapWithEC[C, D](newMediaType)(_ ⇒ f) /** * Reuses this Marshaller's logic to produce a new Marshaller from another type `C` which overrides - * the produced [[ContentType]] with another one. - * Depending on whether the given [[ContentType]] has a defined charset or not and whether the underlying - * marshaller marshals with a fixed charset it can happen, that the wrapping becomes illegal. - * For example, a marshaller producing content encoded with UTF-16 cannot be wrapped with a [[ContentType]] - * that has a defined charset of UTF-8, since akka-http will never recode entities. + * the [[MediaType]] of the marshalling result with the given one. + * Note that not all wrappings are legal. f the underlying [[MediaType]] has constraints with regard to the + * charsets it allows the new [[MediaType]] must be compatible, since akka-http will never recode entities. * If the wrapping is illegal the [[Future]] produced by the resulting marshaller will contain a [[RuntimeException]]. */ - def wrapWithEC[C, D >: B](contentType: ContentType)(f: ExecutionContext ⇒ C ⇒ A)(implicit mto: MediaTypeOverrider[D]): Marshaller[C, D] = + def wrapWithEC[C, D >: B](newMediaType: MediaType)(f: ExecutionContext ⇒ C ⇒ A)(implicit cto: ContentTypeOverrider[D]): Marshaller[C, D] = Marshaller { implicit ec ⇒ value ⇒ import Marshalling._ this(f(ec)(value)).fast map { _ map { - case WithFixedCharset(_, cs, marshal) if contentType.hasOpenCharset || contentType.charset == cs ⇒ - WithFixedCharset(contentType.mediaType, cs, () ⇒ mto(marshal(), contentType.mediaType)) - case WithOpenCharset(_, marshal) if contentType.hasOpenCharset ⇒ - WithOpenCharset(contentType.mediaType, cs ⇒ mto(marshal(cs), contentType.mediaType)) - case WithOpenCharset(_, marshal) ⇒ - WithFixedCharset(contentType.mediaType, contentType.charset, () ⇒ mto(marshal(contentType.charset), contentType.mediaType)) - case Opaque(marshal) if contentType.definedCharset.isEmpty ⇒ Opaque(() ⇒ mto(marshal(), contentType.mediaType)) - case x ⇒ sys.error(s"Illegal marshaller wrapping. Marshalling `$x` cannot be wrapped with ContentType `$contentType`") + (_, newMediaType) match { + case (WithFixedContentType(_, marshal), newMT: MediaType.Binary) ⇒ + WithFixedContentType(newMT, () ⇒ cto(marshal(), newMT)) + case (WithFixedContentType(oldCT: ContentType.Binary, marshal), newMT: MediaType.WithFixedCharset) ⇒ + WithFixedContentType(newMT, () ⇒ cto(marshal(), newMT)) + case (WithFixedContentType(oldCT: ContentType.NonBinary, marshal), newMT: MediaType.WithFixedCharset) if oldCT.charset == newMT.charset ⇒ + WithFixedContentType(newMT, () ⇒ cto(marshal(), newMT)) + case (WithFixedContentType(oldCT: ContentType.NonBinary, marshal), newMT: MediaType.WithOpenCharset) ⇒ + val newCT = newMT withCharset oldCT.charset + WithFixedContentType(newCT, () ⇒ cto(marshal(), newCT)) + + case (WithOpenCharset(oldMT, marshal), newMT: MediaType.WithOpenCharset) ⇒ + WithOpenCharset(newMT, cs ⇒ cto(marshal(cs), newMT withCharset cs)) + case (WithOpenCharset(oldMT, marshal), newMT: MediaType.WithFixedCharset) ⇒ + WithFixedContentType(newMT, () ⇒ cto(marshal(newMT.charset), newMT)) + + case (Opaque(marshal), newMT: MediaType.Binary) ⇒ + WithFixedContentType(newMT, () ⇒ cto(marshal(), newMT)) + case (Opaque(marshal), newMT: MediaType.WithFixedCharset) ⇒ + WithFixedContentType(newMT, () ⇒ cto(marshal(), newMT)) + + case x ⇒ sys.error(s"Illegal marshaller wrapping. Marshalling `$x` cannot be wrapped with MediaType `$newMediaType`") + } } } } @@ -103,13 +114,13 @@ object Marshaller /** * Helper for creating a synchronous [[Marshaller]] to content with a fixed charset from the given function. */ - def withFixedCharset[A, B](mediaType: MediaType, charset: HttpCharset)(marshal: A ⇒ B): Marshaller[A, B] = - strict { value ⇒ Marshalling.WithFixedCharset(mediaType, charset, () ⇒ marshal(value)) } + def withFixedContentType[A, B](contentType: ContentType)(marshal: A ⇒ B): Marshaller[A, B] = + strict { value ⇒ Marshalling.WithFixedContentType(contentType, () ⇒ marshal(value)) } /** * Helper for creating a synchronous [[Marshaller]] to content with a negotiable charset from the given function. */ - def withOpenCharset[A, B](mediaType: MediaType)(marshal: (A, HttpCharset) ⇒ B): Marshaller[A, B] = + def withOpenCharset[A, B](mediaType: MediaType.WithOpenCharset)(marshal: (A, HttpCharset) ⇒ B): Marshaller[A, B] = strict { value ⇒ Marshalling.WithOpenCharset(mediaType, charset ⇒ marshal(value, charset)) } /** @@ -136,19 +147,19 @@ sealed trait Marshalling[+A] { } object Marshalling { + /** - * A Marshalling to a specific MediaType and charset. + * A Marshalling to a specific [[ContentType]]. */ - final case class WithFixedCharset[A](mediaType: MediaType, - charset: HttpCharset, - marshal: () ⇒ A) extends Marshalling[A] { - def map[B](f: A ⇒ B): WithFixedCharset[B] = copy(marshal = () ⇒ f(marshal())) + final case class WithFixedContentType[A](contentType: ContentType, + marshal: () ⇒ A) extends Marshalling[A] { + def map[B](f: A ⇒ B): WithFixedContentType[B] = copy(marshal = () ⇒ f(marshal())) } /** - * A Marshalling to a specific MediaType and a potentially flexible charset. + * A Marshalling to a specific [[MediaType]] with a flexible charset. */ - final case class WithOpenCharset[A](mediaType: MediaType, + final case class WithOpenCharset[A](mediaType: MediaType.WithOpenCharset, marshal: HttpCharset ⇒ A) extends Marshalling[A] { def map[B](f: A ⇒ B): WithOpenCharset[B] = copy(marshal = cs ⇒ f(marshal(cs))) } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/MediaTypeOverrider.scala b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/MediaTypeOverrider.scala deleted file mode 100644 index 96b6833d1a..0000000000 --- a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/MediaTypeOverrider.scala +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (C) 2009-2014 Typesafe Inc. - */ - -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)) - } -} diff --git a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/MultipartMarshallers.scala b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/MultipartMarshallers.scala index b38c4afff9..5d50eae789 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/MultipartMarshallers.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/MultipartMarshallers.scala @@ -13,8 +13,8 @@ trait MultipartMarshallers { implicit def multipartMarshaller[T <: Multipart](implicit log: LoggingAdapter = NoLogging): ToEntityMarshaller[T] = Marshaller strict { value ⇒ val boundary = randomBoundary() - val contentType = ContentType(value.mediaType withBoundary boundary) - Marshalling.WithOpenCharset(contentType.mediaType, { charset ⇒ + val mediaType = value.mediaType withBoundary boundary + Marshalling.WithOpenCharset(mediaType, { charset ⇒ value.toEntity(charset, boundary)(log) }) } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/PredefinedToEntityMarshallers.scala b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/PredefinedToEntityMarshallers.scala index 04e1f7aafe..f8c80906ae 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/PredefinedToEntityMarshallers.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/PredefinedToEntityMarshallers.scala @@ -13,51 +13,39 @@ import akka.util.ByteString trait PredefinedToEntityMarshallers extends MultipartMarshallers { implicit val ByteArrayMarshaller: ToEntityMarshaller[Array[Byte]] = byteArrayMarshaller(`application/octet-stream`) - def byteArrayMarshaller(mediaType: MediaType, charset: HttpCharset): ToEntityMarshaller[Array[Byte]] = { - val ct = ContentType(mediaType, charset) - Marshaller.withFixedCharset(ct.mediaType, ct.definedCharset.get) { bytes ⇒ HttpEntity(ct, bytes) } - } - def byteArrayMarshaller(mediaType: MediaType): ToEntityMarshaller[Array[Byte]] = { - val ct = ContentType(mediaType) - // since we don't want to recode we simply ignore the charset determined by content negotiation here - Marshaller.withOpenCharset(ct.mediaType) { (bytes, _) ⇒ HttpEntity(ct, bytes) } - } + def byteArrayMarshaller(contentType: ContentType): ToEntityMarshaller[Array[Byte]] = + Marshaller.withFixedContentType(contentType) { bytes ⇒ HttpEntity(contentType, bytes) } implicit val ByteStringMarshaller: ToEntityMarshaller[ByteString] = byteStringMarshaller(`application/octet-stream`) - def byteStringMarshaller(mediaType: MediaType, charset: HttpCharset): ToEntityMarshaller[ByteString] = { - val ct = ContentType(mediaType, charset) - Marshaller.withFixedCharset(ct.mediaType, ct.definedCharset.get) { bytes ⇒ HttpEntity(ct, bytes) } - } - def byteStringMarshaller(mediaType: MediaType): ToEntityMarshaller[ByteString] = { - val ct = ContentType(mediaType) - // since we don't want to recode we simply ignore the charset determined by content negotiation here - Marshaller.withOpenCharset(ct.mediaType) { (bytes, _) ⇒ HttpEntity(ct, bytes) } - } + def byteStringMarshaller(contentType: ContentType): ToEntityMarshaller[ByteString] = + Marshaller.withFixedContentType(contentType) { bytes ⇒ HttpEntity(contentType, bytes) } implicit val CharArrayMarshaller: ToEntityMarshaller[Array[Char]] = charArrayMarshaller(`text/plain`) - def charArrayMarshaller(mediaType: MediaType): ToEntityMarshaller[Array[Char]] = - Marshaller.withOpenCharset(mediaType) { (value, charset) ⇒ - if (value.length > 0) { - val charBuffer = CharBuffer.wrap(value) - val byteBuffer = charset.nioCharset.encode(charBuffer) - val array = new Array[Byte](byteBuffer.remaining()) - byteBuffer.get(array) - HttpEntity(ContentType(mediaType, charset), array) - } else HttpEntity.Empty - } + def charArrayMarshaller(mediaType: MediaType.WithOpenCharset): ToEntityMarshaller[Array[Char]] = + Marshaller.withOpenCharset(mediaType) { (value, charset) ⇒ marshalCharArray(value, mediaType withCharset charset) } + def charArrayMarshaller(mediaType: MediaType.WithFixedCharset): ToEntityMarshaller[Array[Char]] = + Marshaller.withFixedContentType(mediaType) { value ⇒ marshalCharArray(value, mediaType) } + + private def marshalCharArray(value: Array[Char], contentType: ContentType.NonBinary): HttpEntity.Strict = + if (value.length > 0) { + val charBuffer = CharBuffer.wrap(value) + val byteBuffer = contentType.charset.nioCharset.encode(charBuffer) + val array = new Array[Byte](byteBuffer.remaining()) + byteBuffer.get(array) + HttpEntity(contentType, array) + } else HttpEntity.Empty implicit val StringMarshaller: ToEntityMarshaller[String] = stringMarshaller(`text/plain`) - def stringMarshaller(mediaType: MediaType): ToEntityMarshaller[String] = - Marshaller.withOpenCharset(mediaType) { (s, cs) ⇒ HttpEntity(ContentType(mediaType, cs), s) } + def stringMarshaller(mediaType: MediaType.WithOpenCharset): ToEntityMarshaller[String] = + Marshaller.withOpenCharset(mediaType) { (s, cs) ⇒ HttpEntity(mediaType withCharset cs, s) } + def stringMarshaller(mediaType: MediaType.WithFixedCharset): ToEntityMarshaller[String] = + Marshaller.withFixedContentType(mediaType) { s ⇒ HttpEntity(mediaType, s) } implicit val FormDataMarshaller: ToEntityMarshaller[FormData] = - Marshaller.withOpenCharset(`application/x-www-form-urlencoded`) { (formData, charset) ⇒ - formData.toEntity(charset) - } + Marshaller.withOpenCharset(`application/x-www-form-urlencoded`) { _ toEntity _ } - implicit val MessageEntityMarshaller: ToEntityMarshaller[MessageEntity] = Marshaller strict { value ⇒ - Marshalling.WithFixedCharset(value.contentType.mediaType, value.contentType.charset, () ⇒ value) - } + implicit val MessageEntityMarshaller: ToEntityMarshaller[MessageEntity] = + Marshaller strict { value ⇒ Marshalling.WithFixedContentType(value.contentType, () ⇒ value) } } object PredefinedToEntityMarshallers extends PredefinedToEntityMarshallers diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/ContentNegotation.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/ContentNegotation.scala new file mode 100644 index 0000000000..589b515ce7 --- /dev/null +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/ContentNegotation.scala @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +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) +} diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/Rejection.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/Rejection.scala index d62834879f..b49a79f1a8 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/Rejection.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/Rejection.scala @@ -112,7 +112,7 @@ case object RequestEntityExpectedRejection extends Rejection * Signals that the request was rejected because the service is not capable of producing a response entity whose * content type is accepted by the client */ -case class UnacceptedResponseContentTypeRejection(supported: immutable.Set[ContentType]) extends Rejection +case class UnacceptedResponseContentTypeRejection(supported: immutable.Set[ContentNegotiator.Alternative]) extends Rejection /** * Rejection created by encoding filters. diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/RejectionHandler.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/RejectionHandler.scala index 72ced368e9..864694878e 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/RejectionHandler.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/RejectionHandler.scala @@ -188,8 +188,8 @@ object RejectionHandler { } .handleAll[UnacceptedResponseContentTypeRejection] { rejections ⇒ val supported = rejections.flatMap(_.supported) - complete((NotAcceptable, "Resource representation is only available with these Content-Types:\n" + - supported.map(_.value).mkString("\n"))) + val msg = supported.map(_.format).mkString("Resource representation is only available with these types:\n", "\n", "") + complete((NotAcceptable, msg)) } .handleAll[UnacceptedResponseEncodingRejection] { rejections ⇒ val supported = rejections.flatMap(_.supported) diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala index 0f9531e4e2..f25e7d463f 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala @@ -5,10 +5,9 @@ package akka.http.scaladsl.server package directives -import scala.annotation.tailrec import scala.collection.immutable import scala.util.control.NonFatal -import akka.http.scaladsl.model.headers.{ HttpEncodings, `Accept-Encoding`, HttpEncoding, HttpEncodingRange } +import akka.http.scaladsl.model.headers.{ HttpEncodings, HttpEncoding } import akka.http.scaladsl.model._ import akka.http.scaladsl.coding._ import akka.http.impl.util._ @@ -26,8 +25,10 @@ trait CodingDirectives { * if the given response encoding is not accepted by the client. */ def responseEncodingAccepted(encoding: HttpEncoding): Directive0 = - extract(_.request.isEncodingAccepted(encoding)) - .flatMap(if (_) pass else reject(UnacceptedResponseEncodingRejection(Set(encoding)))) + extractRequest.flatMap { request ⇒ + if (EncodingNegotiator(request.headers).isAccepted(encoding)) pass + else reject(UnacceptedResponseEncodingRejection(Set(encoding))) + } /** * Encodes the response with the encoding that is requested by the client via the `Accept- @@ -117,37 +118,18 @@ object CodingDirectives extends CodingDirectives { def theseOrDefault[T >: Coder](these: Seq[T]): Seq[T] = if (these.isEmpty) DefaultCoders else these import BasicDirectives._ - import HeaderDirectives._ import RouteDirectives._ private def _encodeResponse(encoders: immutable.Seq[Encoder]): Directive0 = - optionalHeaderValueByType(classOf[`Accept-Encoding`]) flatMap { accept ⇒ - val acceptedEncoder = accept match { - case None ⇒ - // use first defined encoder when Accept-Encoding is missing - encoders.headOption - case Some(`Accept-Encoding`(encodings)) ⇒ - // provide fallback to identity - val withIdentity = - if (encodings.exists { - case HttpEncodingRange.One(HttpEncodings.identity, _) ⇒ true - case _ ⇒ false - }) encodings - else encodings :+ HttpEncodings.`identity;q=MIN` - // sort client-accepted encodings by q-Value (and orig. order) and find first matching encoder - @tailrec def find(encodings: List[HttpEncodingRange]): Option[Encoder] = encodings match { - case encoding :: rest ⇒ - encoders.find(e ⇒ encoding.matches(e.encoding)) match { - case None ⇒ find(rest) - case x ⇒ x - } - case _ ⇒ None - } - find(withIdentity.sortBy(e ⇒ (-e.qValue, withIdentity.indexOf(e))).toList) - } - acceptedEncoder match { + BasicDirectives.extractRequest.flatMap { request ⇒ + val negotiator = EncodingNegotiator(request.headers) + val encodings: List[HttpEncoding] = encoders.map(_.encoding)(collection.breakOut) + val bestEncoder = negotiator.pickEncoding(encodings).flatMap(be ⇒ encoders.find(_.encoding == be)) + bestEncoder match { case Some(encoder) ⇒ mapResponse(encoder.encode(_)) - case _ ⇒ reject(UnacceptedResponseEncodingRejection(encoders.map(_.encoding).toSet)) + case _ ⇒ + if (encoders.contains(NoCoding) && !negotiator.hasMatchingFor(HttpEncodings.identity)) pass + else reject(UnacceptedResponseEncodingRejection(encodings.toSet)) } } } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectives.scala index e90e212d79..00e800593b 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectives.scala @@ -265,7 +265,7 @@ object ContentTypeResolver { case x ⇒ fileName.substring(x + 1) } val mediaType = MediaTypes.forExtension(ext) getOrElse MediaTypes.`application/octet-stream` - ContentType(mediaType) withDefaultCharset charset + ContentType(mediaType, () ⇒ charset) } } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala index dec77516e3..2b2689607e 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala @@ -51,19 +51,10 @@ trait MiscDirectives { * has equal preference for (even if this preference is zero!) * the order of the arguments is used as a tie breaker (First one wins). */ - def selectPreferredLanguage(first: Language, more: Language*): Directive1[Language] = { - val available = first :: List(more: _*) // we use List rather than Seq since element count is likely very small - BasicDirectives.extractRequest.map { req ⇒ - val sortedWithQValues = available.zip(available.map(req.qValueForLanguage(_))).sortBy(-_._2) - val firstBest = sortedWithQValues.head - val moreBest = sortedWithQValues.tail.takeWhile(_._2 == firstBest._2) - if (moreBest.nonEmpty) { - // we have several languages that have the same qvalue, so we pick the one the `Accept-Header` lists first - val allBest = firstBest :: moreBest - req.acceptedLanguageRanges.flatMap(range ⇒ allBest.find(t ⇒ range.matches(t._1))).head._1 - } else firstBest._1 // we have a single best match, so pick that + def selectPreferredLanguage(first: Language, more: Language*): Directive1[Language] = + BasicDirectives.extractRequest.map { request ⇒ + LanguageNegotiator(request.headers).pickLanguage(first :: List(more: _*)) getOrElse first } - } } object MiscDirectives extends MiscDirectives { diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/RouteDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/RouteDirectives.scala index 17dddac09c..53f889f20b 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/RouteDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/RouteDirectives.scala @@ -34,7 +34,7 @@ trait RouteDirectives { headers = headers.Location(uri) :: Nil, entity = redirectionType.htmlTemplate match { case "" ⇒ HttpEntity.Empty - case template ⇒ HttpEntity(MediaTypes.`text/html`, template format uri) + case template ⇒ HttpEntity(ContentTypes.`text/html(UTF-8)`, template format uri) }) } //# diff --git a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/MultipartUnmarshallers.scala b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/MultipartUnmarshallers.scala index e22e658987..94473bcf60 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/MultipartUnmarshallers.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/MultipartUnmarshallers.scala @@ -25,7 +25,7 @@ trait MultipartUnmarshallers { def multipartGeneralUnmarshaller(defaultCharset: HttpCharset)(implicit log: LoggingAdapter = NoLogging): FromEntityUnmarshaller[Multipart.General] = multipartUnmarshaller[Multipart.General, Multipart.General.BodyPart, Multipart.General.BodyPart.Strict]( mediaRange = `multipart/*`, - defaultContentType = ContentTypes.`text/plain` withCharset defaultCharset, + defaultContentType = MediaTypes.`text/plain` withCharset defaultCharset, createBodyPart = Multipart.General.BodyPart(_, _), createStreamed = Multipart.General(_, _), createStrictBodyPart = Multipart.General.BodyPart.Strict, @@ -45,7 +45,7 @@ trait MultipartUnmarshallers { def multipartByteRangesUnmarshaller(defaultCharset: HttpCharset)(implicit log: LoggingAdapter = NoLogging): FromEntityUnmarshaller[Multipart.ByteRanges] = multipartUnmarshaller[Multipart.ByteRanges, Multipart.ByteRanges.BodyPart, Multipart.ByteRanges.BodyPart.Strict]( mediaRange = `multipart/byteranges`, - defaultContentType = ContentTypes.`text/plain` withCharset defaultCharset, + defaultContentType = MediaTypes.`text/plain` withCharset defaultCharset, createBodyPart = (entity, headers) ⇒ Multipart.General.BodyPart(entity, headers).toByteRangesBodyPart.get, createStreamed = (_, parts) ⇒ Multipart.ByteRanges(parts), createStrictBodyPart = (entity, headers) ⇒ Multipart.General.BodyPart.Strict(entity, headers).toByteRangesBodyPart.get, @@ -54,9 +54,9 @@ trait MultipartUnmarshallers { def multipartUnmarshaller[T <: Multipart, BP <: Multipart.BodyPart, BPS <: Multipart.BodyPart.Strict](mediaRange: MediaRange, defaultContentType: ContentType, createBodyPart: (BodyPartEntity, List[HttpHeader]) ⇒ BP, - createStreamed: (MultipartMediaType, Source[BP, Any]) ⇒ T, + createStreamed: (MediaType.Multipart, Source[BP, Any]) ⇒ T, createStrictBodyPart: (HttpEntity.Strict, List[HttpHeader]) ⇒ BPS, - createStrict: (MultipartMediaType, immutable.Seq[BPS]) ⇒ T)(implicit log: LoggingAdapter = NoLogging): FromEntityUnmarshaller[T] = + createStrict: (MediaType.Multipart, immutable.Seq[BPS]) ⇒ T)(implicit log: LoggingAdapter = NoLogging): FromEntityUnmarshaller[T] = Unmarshaller { implicit ec ⇒ entity ⇒ if (entity.contentType.mediaType.isMultipart && mediaRange.matches(entity.contentType.mediaType)) { @@ -68,7 +68,7 @@ trait MultipartUnmarshallers { val parser = new BodyPartParser(defaultContentType, boundary, log) FastFuture.successful { entity match { - case HttpEntity.Strict(ContentType(mediaType: MultipartMediaType, _), data) ⇒ + case HttpEntity.Strict(ContentType(mediaType: MediaType.Multipart, _), data) ⇒ val builder = new VectorBuilder[BPS]() val iter = new IteratorInterpreter[ByteString, BodyPartParser.Output]( Iterator.single(data), List(parser)).iterator @@ -93,7 +93,7 @@ trait MultipartUnmarshallers { case (BodyPartStart(headers, createEntity), entityParts) ⇒ createBodyPart(createEntity(entityParts), headers) case (ParseError(errorInfo), _) ⇒ throw ParsingException(errorInfo) } - createStreamed(entity.contentType.mediaType.asInstanceOf[MultipartMediaType], bodyParts) + createStreamed(entity.contentType.mediaType.asInstanceOf[MediaType.Multipart], bodyParts) } } } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/PredefinedFromEntityUnmarshallers.scala b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/PredefinedFromEntityUnmarshallers.scala index dba637eb7e..ffc64d564b 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/PredefinedFromEntityUnmarshallers.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/PredefinedFromEntityUnmarshallers.scala @@ -21,29 +21,32 @@ trait PredefinedFromEntityUnmarshallers extends MultipartUnmarshallers { implicit def charArrayUnmarshaller: FromEntityUnmarshaller[Array[Char]] = byteStringUnmarshaller mapWithInput { (entity, bytes) ⇒ - val charBuffer = entity.contentType.charset.nioCharset.decode(bytes.asByteBuffer) - val array = new Array[Char](charBuffer.length()) - charBuffer.get(array) - array + if (entity.isKnownEmpty) Array.emptyCharArray + else { + val charBuffer = Unmarshaller.bestUnmarshallingCharsetFor(entity).nioCharset.decode(bytes.asByteBuffer) + val array = new Array[Char](charBuffer.length()) + charBuffer.get(array) + array + } } implicit def stringUnmarshaller: FromEntityUnmarshaller[String] = byteStringUnmarshaller mapWithInput { (entity, bytes) ⇒ - // FIXME: add `ByteString::decodeString(java.nio.Charset): String` overload!!! - bytes.decodeString(entity.contentType.charset.nioCharset.name) // ouch!!! + if (entity.isKnownEmpty) "" + else bytes.decodeString(Unmarshaller.bestUnmarshallingCharsetFor(entity).nioCharset.name) } implicit def defaultUrlEncodedFormDataUnmarshaller: FromEntityUnmarshaller[FormData] = urlEncodedFormDataUnmarshaller(MediaTypes.`application/x-www-form-urlencoded`) def urlEncodedFormDataUnmarshaller(ranges: ContentTypeRange*): FromEntityUnmarshaller[FormData] = stringUnmarshaller.forContentTypes(ranges: _*).mapWithInput { (entity, string) ⇒ - try { - val nioCharset = entity.contentType.definedCharset.getOrElse(HttpCharsets.`UTF-8`).nioCharset - val query = Uri.Query(string, nioCharset) - FormData(query) - } catch { - case IllegalUriException(info) ⇒ - throw new IllegalArgumentException(info.formatPretty.replace("Query,", "form content,")) + if (entity.isKnownEmpty) FormData.Empty + else { + try FormData(Uri.Query(string, Unmarshaller.bestUnmarshallingCharsetFor(entity).nioCharset)) + catch { + case IllegalUriException(info) ⇒ + throw new IllegalArgumentException(info.formatPretty.replace("Query,", "form content,")) + } } } } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/Unmarshaller.scala b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/Unmarshaller.scala index b0383f027b..6aaaf9a673 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/Unmarshaller.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/Unmarshaller.scala @@ -90,7 +90,7 @@ object Unmarshaller implicit class EnhancedFromEntityUnmarshaller[A](val underlying: FromEntityUnmarshaller[A]) extends AnyVal { def mapWithCharset[B](f: (A, HttpCharset) ⇒ B): FromEntityUnmarshaller[B] = - underlying.mapWithInput { (entity, data) ⇒ f(data, entity.contentType.charset) } + underlying.mapWithInput { (entity, data) ⇒ f(data, Unmarshaller.bestUnmarshallingCharsetFor(entity)) } /** * Modifies the underlying [[Unmarshaller]] to only accept content-types matching one of the given ranges. @@ -114,6 +114,16 @@ object Unmarshaller case UnsupportedContentTypeException(supported) ⇒ underlying(entity withContentType supported.head.specimen) } + /** + * Returns the best charset for unmarshalling the given entity to a character-based representation. + * Falls back to UTF-8 if no better alternative can be determined. + */ + def bestUnmarshallingCharsetFor(entity: HttpEntity): HttpCharset = + entity.contentType match { + case x: ContentType.NonBinary ⇒ x.charset + case _ ⇒ HttpCharsets.`UTF-8` + } + /** * Signals that unmarshalling failed because the entity was unexpectedly empty. */ From 79008950446f294d8d42760929b59690d6752b29 Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 4 Dec 2015 10:24:48 +0100 Subject: [PATCH 4/5] !htc, htp remove `specimen` method of ranges, restrict `Unmarshaller#forContentType` --- .../http/javadsl/model/ContentTypeRange.java | 5 ---- .../http/scaladsl/model/ContentType.scala | 5 ---- .../http/scaladsl/model/HttpCharset.scala | 7 ----- .../akka/http/scaladsl/model/MediaRange.scala | 15 ----------- .../scaladsl/unmarshalling/Unmarshaller.scala | 26 +++++++++++-------- 5 files changed, 15 insertions(+), 43 deletions(-) diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/ContentTypeRange.java b/akka-http-core/src/main/java/akka/http/javadsl/model/ContentTypeRange.java index ba86afa484..cf40b35cb0 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/ContentTypeRange.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/ContentTypeRange.java @@ -17,9 +17,4 @@ public abstract class ContentTypeRange { * Returns true if this range includes the given content type. */ public abstract boolean matches(ContentType contentType); - - /** - * Returns a ContentType instance which fits this range. - */ - public abstract ContentType specimen(); } diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/ContentType.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/ContentType.scala index 22b79288bd..e2bf86910c 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/ContentType.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/ContentType.scala @@ -21,11 +21,6 @@ final case class ContentTypeRange(mediaRange: MediaRange, charsetRange: HttpChar case HttpCharsetRange.`*` ⇒ r ~~ mediaRange case x ⇒ r ~~ mediaRange ~~ ContentType.`; charset=` ~~ x } - - /** - * Returns a [[ContentType]] instance which fits this range. - */ - def specimen: ContentType = ContentType(mediaRange.specimen, () ⇒ charsetRange.specimen) } object ContentTypeRange { diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpCharset.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpCharset.scala index 8ebf0d7724..92bc1eda1f 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpCharset.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/HttpCharset.scala @@ -20,11 +20,6 @@ sealed abstract class HttpCharsetRange extends jm.HttpCharsetRange with ValueRen def qValue: Float def matches(charset: HttpCharset): Boolean - /** - * Returns a [[HttpCharset]] instance which fits this range. - */ - def specimen: HttpCharset - /** Java API */ def matches(charset: jm.HttpCharset): Boolean = { import akka.http.impl.util.JavaMapping.Implicits._ @@ -37,7 +32,6 @@ object HttpCharsetRange { require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") final def render[R <: Rendering](r: R): r.type = if (qValue < 1.0f) r ~~ "*;q=" ~~ qValue else r ~~ '*' def matches(charset: HttpCharset) = true - def specimen: HttpCharset = HttpCharsets.`UTF-8` def withQValue(qValue: Float) = if (qValue == 1.0f) `*` else if (qValue != this.qValue) `*`(qValue.toFloat) else this } @@ -46,7 +40,6 @@ object HttpCharsetRange { final case class One(charset: HttpCharset, qValue: Float) extends HttpCharsetRange { require(0.0f <= qValue && qValue <= 1.0f, "qValue must be >= 0 and <= 1.0") def matches(charset: HttpCharset) = this.charset.value.equalsIgnoreCase(charset.value) - def specimen: HttpCharset = charset def withQValue(qValue: Float) = One(charset, qValue) def render[R <: Rendering](r: R): r.type = if (qValue < 1.0f) r ~~ charset ~~ ";q=" ~~ qValue else r ~~ charset } diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaRange.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaRange.scala index 4a526073da..a3dade03e6 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaRange.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaRange.scala @@ -35,11 +35,6 @@ sealed abstract class MediaRange extends jm.MediaRange with Renderable with With */ def withCharsetRange(charsetRange: HttpCharsetRange): ContentTypeRange = ContentTypeRange(this, charsetRange) - /** - * Returns a [[MediaType]] instance which fits this range. - */ - def specimen: MediaType - /** Java API */ def getParams: util.Map[String, String] = { import collection.JavaConverters._ @@ -78,7 +73,6 @@ object MediaRange { override def isMultipart = mainType == "multipart" override def isText = mainType == "text" override def isVideo = mainType == "video" - def specimen = MediaType.customBinary(mainType, "custom", compressible = true) } def custom(mainType: String, params: Map[String, String] = Map.empty, qValue: Float = 1.0f): MediaRange = { @@ -102,7 +96,6 @@ object MediaRange { def withParams(params: Map[String, String]) = copy(mediaType = mediaType.withParams(params)) def withQValue(qValue: Float) = copy(qValue = qValue) def render[R <: Rendering](r: R): r.type = if (qValue < 1.0f) r ~~ mediaType ~~ ";q=" ~~ qValue else r ~~ mediaType - def specimen = mediaType } implicit def apply(mediaType: MediaType): MediaRange = apply(mediaType, 1.0f) @@ -122,42 +115,34 @@ object MediaRanges extends ObjectRegistry[String, MediaRange] { val `*/*` = new PredefinedMediaRange("*/*") { def matches(mediaType: MediaType) = true - def specimen = MediaTypes.`text/plain` } val `*/*;q=MIN` = `*/*`.withQValue(Float.MinPositiveValue) val `application/*` = new PredefinedMediaRange("application/*") { def matches(mediaType: MediaType) = mediaType.isApplication override def isApplication = true - def specimen = MediaTypes.`application/json` } val `audio/*` = new PredefinedMediaRange("audio/*") { def matches(mediaType: MediaType) = mediaType.isAudio override def isAudio = true - def specimen = MediaTypes.`audio/ogg` } val `image/*` = new PredefinedMediaRange("image/*") { def matches(mediaType: MediaType) = mediaType.isImage override def isImage = true - def specimen = MediaTypes.`image/png` } val `message/*` = new PredefinedMediaRange("message/*") { def matches(mediaType: MediaType) = mediaType.isMessage override def isMessage = true - def specimen = MediaTypes.`message/rfc822` } val `multipart/*` = new PredefinedMediaRange("multipart/*") { def matches(mediaType: MediaType) = mediaType.isMultipart override def isMultipart = true - def specimen = MediaTypes.`multipart/form-data` } val `text/*` = new PredefinedMediaRange("text/*") { def matches(mediaType: MediaType) = mediaType.isText override def isText = true - def specimen = MediaTypes.`text/plain` } val `video/*` = new PredefinedMediaRange("video/*") { def matches(mediaType: MediaType) = mediaType.isVideo override def isVideo = true - def specimen = MediaTypes.`video/mp4` } } \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/Unmarshaller.scala b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/Unmarshaller.scala index 6aaaf9a673..1e9ca40175 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/Unmarshaller.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/Unmarshaller.scala @@ -43,7 +43,7 @@ object Unmarshaller /** * Creates an `Unmarshaller` from the given function. */ - def apply[A, B](f: ExecutionContext ⇒ A ⇒ Future[B]): Unmarshaller[A, B] = + def apply[A, B](f: ExecutionContext ⇒ A ⇒ Future[B]): Unmarshaller[A, B] = withMaterializer(ec => _ => f(ec)) def withMaterializer[A, B](f: ExecutionContext ⇒ Materializer => A ⇒ Future[B]): Unmarshaller[A, B] = @@ -93,25 +93,29 @@ object Unmarshaller underlying.mapWithInput { (entity, data) ⇒ f(data, Unmarshaller.bestUnmarshallingCharsetFor(entity)) } /** - * Modifies the underlying [[Unmarshaller]] to only accept content-types matching one of the given ranges. - * If the underlying [[Unmarshaller]] already contains a content-type filter (also wrapped at some level), - * this filter is *replaced* by this method, not stacked! + * Modifies the underlying [[Unmarshaller]] to only accept Content-Types matching one of the given ranges. + * Note that you can only restrict to a subset of the Content-Types accepted by the underlying unmarshaller, + * i.e. the given ranges must be completely supported also by the underlying Unmarshaller! + * If a violation of this rule is detected at runtime, i.e. if an entity is encountered whose Content-Type + * is matched by one of the given ranges but rejected by the underlying unmarshaller + * an IllegalStateException will be thrown! */ def forContentTypes(ranges: ContentTypeRange*): FromEntityUnmarshaller[A] = Unmarshaller.withMaterializer { implicit ec ⇒ implicit mat ⇒ entity ⇒ if (entity.contentType == ContentTypes.NoContentType || ranges.exists(_ matches entity.contentType)) { - underlying(entity).fast recoverWith retryWithPatchedContentType(underlying, entity) + underlying(entity).fast.recover[A](barkAtUnsupportedContentTypeException(ranges, entity.contentType)) } else FastFuture.failed(UnsupportedContentTypeException(ranges: _*)) } - } - // must be moved out of the the [[EnhancedFromEntityUnmarshaller]] value class due to bug in scala 2.10: - // https://issues.scala-lang.org/browse/SI-8018 - private def retryWithPatchedContentType[T](underlying: FromEntityUnmarshaller[T], entity: HttpEntity)( - implicit ec: ExecutionContext, mat: Materializer): PartialFunction[Throwable, Future[T]] = { - case UnsupportedContentTypeException(supported) ⇒ underlying(entity withContentType supported.head.specimen) + // TODO: move back into the [[EnhancedFromEntityUnmarshaller]] value class after the upgrade to Scala 2.11, + // Scala 2.10 suffers from this bug: https://issues.scala-lang.org/browse/SI-8018 + private def barkAtUnsupportedContentTypeException(ranges: Seq[ContentTypeRange], + newContentType: ContentType): PartialFunction[Throwable, Nothing] = { + case UnsupportedContentTypeException(supported) ⇒ throw new IllegalStateException( + s"Illegal use of `unmarshaller.forContentTypes($ranges)`: $newContentType is not supported by underlying marshaller!") + } } /** From ba397463c308786458437e2dedb13d7b6f006fce Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 4 Dec 2015 12:34:04 +0100 Subject: [PATCH 5/5] !htc, htp #16991 add support for precompressed media-types and `.gz` file extension suffixes --- .../akka/http/javadsl/model/MediaType.java | 2 +- .../akka/http/javadsl/model/MediaTypes.java | 4 +- .../impl/model/parser/CommonActions.scala | 2 +- .../akka/http/scaladsl/model/MediaType.scala | 254 ++++++++++-------- .../impl/model/parser/HttpHeaderSpec.scala | 8 +- .../FileAndResourceDirectivesSpec.scala | 25 +- .../akka/http/scaladsl/coding/Encoder.scala | 2 +- .../server/directives/CodingDirectives.scala | 10 + .../FileAndResourceDirectives.scala | 43 +-- 9 files changed, 203 insertions(+), 147 deletions(-) diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/MediaType.java b/akka-http-core/src/main/java/akka/http/javadsl/model/MediaType.java index 8f330fce21..b53721cc38 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/MediaType.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/MediaType.java @@ -22,7 +22,7 @@ public interface MediaType { /** * True when this media-type is generally compressible. */ - boolean compressible(); + boolean isCompressible(); /** * True when this media-type is not character-based. diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/MediaTypes.java b/akka-http-core/src/main/java/akka/http/javadsl/model/MediaTypes.java index c7daac4c5a..18b68730bb 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/MediaTypes.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/MediaTypes.java @@ -188,7 +188,9 @@ public abstract class MediaTypes { * Creates a custom media type. */ public static MediaType custom(String value, boolean binary, boolean compressible) { - return akka.http.scaladsl.model.MediaType.custom(value, binary, compressible, List.empty()); + akka.http.scaladsl.model.MediaType.Compressibility comp = compressible ? + akka.http.scaladsl.model.MediaType.Compressible$.MODULE$ : akka.http.scaladsl.model.MediaType.NotCompressible$.MODULE$; + return akka.http.scaladsl.model.MediaType.custom(value, binary, comp , List.empty()); } /** diff --git a/akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonActions.scala b/akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonActions.scala index 5a0e50842c..46ddddb41a 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonActions.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/model/parser/CommonActions.scala @@ -32,7 +32,7 @@ private[parser] trait CommonActions { if (charsetDefined) MediaType.customWithOpenCharset(mainLower, subType, params = params, allowArbitrarySubtypes = true) else - MediaType.customBinary(mainLower, subType, compressible = true, params = params, + MediaType.customBinary(mainLower, subType, MediaType.Compressible, params = params, allowArbitrarySubtypes = true) } } diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaType.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaType.scala index 36e32c5968..558bacc3a6 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaType.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/MediaType.scala @@ -33,9 +33,11 @@ import akka.http.impl.util.JavaMapping.Implicits._ * Like binary MediaTypes `WithFixedCharset` types can be implicitly converted to a [[ContentType]]. */ sealed abstract class MediaType extends jm.MediaType with LazyValueBytesRenderable with WithQValue[MediaRange] { + import MediaType.Compressibility def fileExtensions: List[String] def params: Map[String, String] + def comp: Compressibility override def isApplication: Boolean = false override def isAudio: Boolean = false @@ -46,7 +48,7 @@ sealed abstract class MediaType extends jm.MediaType with LazyValueBytesRenderab override def isVideo: Boolean = false def withParams(params: Map[String, String]): MediaType - + def withComp(comp: Compressibility): MediaType def withQValue(qValue: Float): MediaRange = MediaRange(this, qValue.toFloat) override def equals(that: Any): Boolean = @@ -62,12 +64,13 @@ sealed abstract class MediaType extends jm.MediaType with LazyValueBytesRenderab */ def toRange = jm.MediaRanges.create(this) def toRange(qValue: Float) = jm.MediaRanges.create(this, qValue) + def isCompressible: Boolean = comp.compressible } object MediaType { - def applicationBinary(subType: String, compressible: Boolean, fileExtensions: String*): Binary = - new Binary("application/" + subType, "application", subType, compressible, fileExtensions.toList) { + def applicationBinary(subType: String, comp: Compressibility, fileExtensions: String*): Binary = + new Binary("application/" + subType, "application", subType, comp, fileExtensions.toList) { override def isApplication = true } @@ -82,18 +85,18 @@ object MediaType { override def isApplication = true } - def audio(subType: String, compressible: Boolean, fileExtensions: String*): Binary = - new Binary("audio/" + subType, "audio", subType, compressible, fileExtensions.toList) { + def audio(subType: String, comp: Compressibility, fileExtensions: String*): Binary = + new Binary("audio/" + subType, "audio", subType, comp, fileExtensions.toList) { override def isAudio = true } - def image(subType: String, compressible: Boolean, fileExtensions: String*): Binary = - new Binary("image/" + subType, "image", subType, compressible, fileExtensions.toList) { + def image(subType: String, comp: Compressibility, fileExtensions: String*): Binary = + new Binary("image/" + subType, "image", subType, comp, fileExtensions.toList) { override def isImage = true } - def message(subType: String, compressible: Boolean, fileExtensions: String*): Binary = - new Binary("message/" + subType, "message", subType, compressible, fileExtensions.toList) { + def message(subType: String, comp: Compressibility, fileExtensions: String*): Binary = + new Binary("message/" + subType, "message", subType, comp, fileExtensions.toList) { override def isMessage = true } @@ -102,17 +105,17 @@ object MediaType { override def isText = true } - def video(subType: String, compressible: Boolean, fileExtensions: String*): Binary = - new Binary("video/" + subType, "video", subType, compressible, fileExtensions.toList) { + def video(subType: String, comp: Compressibility, fileExtensions: String*): Binary = + new Binary("video/" + subType, "video", subType, comp, fileExtensions.toList) { override def isVideo = true } - def customBinary(mainType: String, subType: String, compressible: Boolean, fileExtensions: List[String] = Nil, + def customBinary(mainType: String, subType: String, comp: Compressibility, fileExtensions: List[String] = Nil, params: Map[String, String] = Map.empty, allowArbitrarySubtypes: Boolean = false): Binary = { require(mainType != "multipart", "Cannot create a MediaType.Multipart here, use `customMultipart` instead!") require(allowArbitrarySubtypes || subType != "*", "Cannot create a MediaRange here, use `MediaRange.custom` instead!") val _params = params - new Binary(renderValue(mainType, subType, params), mainType, subType, compressible, fileExtensions) { + new Binary(renderValue(mainType, subType, params), mainType, subType, comp, fileExtensions) { override def params = _params override def isApplication = mainType == "application" override def isAudio = mainType == "audio" @@ -162,11 +165,11 @@ object MediaType { new Multipart(subType, params) } - def custom(value: String, binary: Boolean, compressible: Boolean = true, + def custom(value: String, binary: Boolean, comp: Compressibility = Compressible, fileExtensions: List[String] = Nil): MediaType = { val parts = value.split('/') require(parts.length == 2, s"`$value` is not a valid media-type. It must consist of two parts separated by '/'.") - if (binary) customBinary(parts(0), parts(1), compressible, fileExtensions) + if (binary) customBinary(parts(0), parts(1), comp, fileExtensions) else customWithOpenCharset(parts(0), parts(1), fileExtensions) } @@ -187,12 +190,14 @@ object MediaType { r.get } - sealed abstract class Binary(val value: String, val mainType: String, val subType: String, val compressible: Boolean, + sealed abstract class Binary(val value: String, val mainType: String, val subType: String, val comp: Compressibility, val fileExtensions: List[String]) extends MediaType with jm.MediaType.Binary { def binary = true def params: Map[String, String] = Map.empty def withParams(params: Map[String, String]): Binary with MediaType = - customBinary(mainType, subType, compressible, fileExtensions, params) + customBinary(mainType, subType, comp, fileExtensions, params) + def withComp(comp: Compressibility): Binary with MediaType = + customBinary(mainType, subType, comp, fileExtensions, params) /** * JAVA API @@ -202,7 +207,9 @@ object MediaType { sealed abstract class NonBinary extends MediaType with jm.MediaType.NonBinary { def binary = false - def compressible = true + def comp = Compressible + def withComp(comp: Compressibility): Binary with MediaType = + customBinary(mainType, subType, comp, fileExtensions, params) } sealed abstract class WithFixedCharset(val value: String, val mainType: String, val subType: String, @@ -244,20 +251,30 @@ object MediaType { def withBoundary(boundary: String): MediaType.Multipart = withParams(if (boundary.isEmpty) params - "boundary" else params.updated("boundary", boundary)) } + + sealed abstract class Compressibility(val compressible: Boolean) + case object Compressible extends Compressibility(compressible = true) + case object NotCompressible extends Compressibility(compressible = false) + case object Gzipped extends Compressibility(compressible = false) } object MediaTypes extends ObjectRegistry[(String, String), MediaType] { private[this] var extensionMap = Map.empty[String, MediaType] - def forExtension(ext: String): Option[MediaType] = extensionMap.get(ext.toRootLowerCase) + def forExtensionOption(ext: String): Option[MediaType] = extensionMap.get(ext.toLowerCase) + def forExtension(ext: String): MediaType = extensionMap.getOrElse(ext.toLowerCase, `application/octet-stream`) - private def register[T <: MediaType](mediaType: T): T = { - def registerFileExtension(ext: String): Unit = { - val lcExt = ext.toRootLowerCase + private def registerFileExtensions[T <: MediaType](mediaType: T): T = { + mediaType.fileExtensions.foreach { ext ⇒ + val lcExt = ext.toLowerCase require(!extensionMap.contains(lcExt), s"Extension '$ext' clash: media-types '${extensionMap(lcExt)}' and '$mediaType'") extensionMap = extensionMap.updated(lcExt, mediaType) } - mediaType.fileExtensions.foreach(registerFileExtension) + mediaType + } + + private def register[T <: MediaType](mediaType: T): T = { + registerFileExtensions(mediaType) register(mediaType.mainType.toRootLowerCase -> mediaType.subType.toRootLowerCase, mediaType) } @@ -265,122 +282,121 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] { /////////////////////////// PREDEFINED MEDIA-TYPE DEFINITION //////////////////////////// // format: OFF - private final val compressible = true // compile-time constant - private final val uncompressible = false // compile-time constant - private def abin(st: String, c: Boolean, fe: String*) = register(applicationBinary(st, c, fe: _*)) - private def awfc(st: String, cs: HttpCharset, fe: String*) = register(applicationWithFixedCharset(st, cs, fe: _*)) - private def awoc(st: String, fe: String*) = register(applicationWithOpenCharset(st, fe: _*)) - private def aud(st: String, c: Boolean, fe: String*) = register(audio(st, c, fe: _*)) - private def img(st: String, c: Boolean, fe: String*) = register(image(st, c, fe: _*)) - private def msg(st: String, fe: String*) = register(message(st, compressible, fe: _*)) - private def txt(st: String, fe: String*) = register(text(st, fe: _*)) - private def vid(st: String, fe: String*) = register(video(st, compressible, fe: _*)) + private def abin(st: String, c: Compressibility, fe: String*) = register(applicationBinary(st, c, fe: _*)) + private def awfc(st: String, cs: HttpCharset, fe: String*) = register(applicationWithFixedCharset(st, cs, fe: _*)) + private def awoc(st: String, fe: String*) = register(applicationWithOpenCharset(st, fe: _*)) + private def aud(st: String, c: Compressibility, fe: String*) = register(audio(st, c, fe: _*)) + private def img(st: String, c: Compressibility, fe: String*) = register(image(st, c, fe: _*)) + private def msg(st: String, fe: String*) = register(message(st, Compressible, fe: _*)) + private def txt(st: String, fe: String*) = register(text(st, fe: _*)) + private def vid(st: String, fe: String*) = register(video(st, NotCompressible, fe: _*)) // dummy value currently only used by ContentType.NoContentType - private[http] val NoMediaType = MediaType.customBinary("none", "none", compressible = false) + private[http] val NoMediaType = MediaType.customBinary("none", "none", comp = NotCompressible) val `application/atom+xml` = awoc("atom+xml", "atom") val `application/base64` = awoc("base64", "mm", "mme") - val `application/excel` = abin("excel", uncompressible, "xl", "xla", "xlb", "xlc", "xld", "xlk", "xll", "xlm", "xls", "xlt", "xlv", "xlw") - val `application/font-woff` = abin("font-woff", uncompressible, "woff") - val `application/gnutar` = abin("gnutar", uncompressible, "tgz") - val `application/java-archive` = abin("java-archive", uncompressible, "jar", "war", "ear") + val `application/excel` = abin("excel", NotCompressible, "xl", "xla", "xlb", "xlc", "xld", "xlk", "xll", "xlm", "xls", "xlt", "xlv", "xlw") + val `application/font-woff` = abin("font-woff", NotCompressible, "woff") + val `application/gnutar` = abin("gnutar", NotCompressible, "tgz") + val `application/java-archive` = abin("java-archive", NotCompressible, "jar", "war", "ear") val `application/javascript` = awoc("javascript", "js") val `application/json` = awfc("json", HttpCharsets.`UTF-8`, "json") val `application/json-patch+json` = awfc("json-patch+json", HttpCharsets.`UTF-8`) - val `application/lha` = abin("lha", uncompressible, "lha") - val `application/lzx` = abin("lzx", uncompressible, "lzx") - val `application/mspowerpoint` = abin("mspowerpoint", uncompressible, "pot", "pps", "ppt", "ppz") - val `application/msword` = abin("msword", uncompressible, "doc", "dot", "w6w", "wiz", "word", "wri") - val `application/octet-stream` = abin("octet-stream", uncompressible, "a", "bin", "class", "dump", "exe", "lhx", "lzh", "o", "psd", "saveme", "zoo") - val `application/pdf` = abin("pdf", uncompressible, "pdf") - val `application/postscript` = abin("postscript", compressible, "ai", "eps", "ps") + val `application/lha` = abin("lha", NotCompressible, "lha") + val `application/lzx` = abin("lzx", NotCompressible, "lzx") + val `application/mspowerpoint` = abin("mspowerpoint", NotCompressible, "pot", "pps", "ppt", "ppz") + val `application/msword` = abin("msword", NotCompressible, "doc", "dot", "w6w", "wiz", "word", "wri") + val `application/octet-stream` = abin("octet-stream", NotCompressible, "a", "bin", "class", "dump", "exe", "lhx", "lzh", "o", "psd", "saveme", "zoo") + val `application/pdf` = abin("pdf", NotCompressible, "pdf") + val `application/postscript` = abin("postscript", Compressible, "ai", "eps", "ps") val `application/rss+xml` = awoc("rss+xml", "rss") val `application/soap+xml` = awoc("soap+xml") val `application/vnd.api+json` = awfc("vnd.api+json", HttpCharsets.`UTF-8`) val `application/vnd.google-earth.kml+xml` = awoc("vnd.google-earth.kml+xml", "kml") - val `application/vnd.google-earth.kmz` = abin("vnd.google-earth.kmz", uncompressible, "kmz") - val `application/vnd.ms-fontobject` = abin("vnd.ms-fontobject", compressible, "eot") - val `application/vnd.oasis.opendocument.chart` = abin("vnd.oasis.opendocument.chart", compressible, "odc") - val `application/vnd.oasis.opendocument.database` = abin("vnd.oasis.opendocument.database", compressible, "odb") - val `application/vnd.oasis.opendocument.formula` = abin("vnd.oasis.opendocument.formula", compressible, "odf") - val `application/vnd.oasis.opendocument.graphics` = abin("vnd.oasis.opendocument.graphics", compressible, "odg") - val `application/vnd.oasis.opendocument.image` = abin("vnd.oasis.opendocument.image", compressible, "odi") - val `application/vnd.oasis.opendocument.presentation` = abin("vnd.oasis.opendocument.presentation", compressible, "odp") - val `application/vnd.oasis.opendocument.spreadsheet` = abin("vnd.oasis.opendocument.spreadsheet", compressible, "ods") - val `application/vnd.oasis.opendocument.text` = abin("vnd.oasis.opendocument.text", compressible, "odt") - val `application/vnd.oasis.opendocument.text-master` = abin("vnd.oasis.opendocument.text-master", compressible, "odm", "otm") - val `application/vnd.oasis.opendocument.text-web` = abin("vnd.oasis.opendocument.text-web", compressible, "oth") - val `application/vnd.openxmlformats-officedocument.presentationml.presentation` = abin("vnd.openxmlformats-officedocument.presentationml.presentation", compressible, "pptx") - val `application/vnd.openxmlformats-officedocument.presentationml.slide` = abin("vnd.openxmlformats-officedocument.presentationml.slide", compressible, "sldx") - val `application/vnd.openxmlformats-officedocument.presentationml.slideshow` = abin("vnd.openxmlformats-officedocument.presentationml.slideshow", compressible, "ppsx") - val `application/vnd.openxmlformats-officedocument.presentationml.template` = abin("vnd.openxmlformats-officedocument.presentationml.template", compressible, "potx") - val `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` = abin("vnd.openxmlformats-officedocument.spreadsheetml.sheet", compressible, "xlsx") - val `application/vnd.openxmlformats-officedocument.spreadsheetml.template` = abin("vnd.openxmlformats-officedocument.spreadsheetml.template", compressible, "xltx") - val `application/vnd.openxmlformats-officedocument.wordprocessingml.document` = abin("vnd.openxmlformats-officedocument.wordprocessingml.document", compressible, "docx") - val `application/vnd.openxmlformats-officedocument.wordprocessingml.template` = abin("vnd.openxmlformats-officedocument.wordprocessingml.template", compressible, "dotx") - val `application/x-7z-compressed` = abin("x-7z-compressed", uncompressible, "7z", "s7z") - val `application/x-ace-compressed` = abin("x-ace-compressed", uncompressible, "ace") - val `application/x-apple-diskimage` = abin("x-apple-diskimage", uncompressible, "dmg") - val `application/x-arc-compressed` = abin("x-arc-compressed", uncompressible, "arc") - val `application/x-bzip` = abin("x-bzip", uncompressible, "bz") - val `application/x-bzip2` = abin("x-bzip2", uncompressible, "boz", "bz2") - val `application/x-chrome-extension` = abin("x-chrome-extension", uncompressible, "crx") - val `application/x-compress` = abin("x-compress", uncompressible, "z") - val `application/x-compressed` = abin("x-compressed", uncompressible, "gz") - val `application/x-debian-package` = abin("x-debian-package", compressible, "deb") - val `application/x-dvi` = abin("x-dvi", compressible, "dvi") - val `application/x-font-truetype` = abin("x-font-truetype", compressible, "ttf") - val `application/x-font-opentype` = abin("x-font-opentype", compressible, "otf") - val `application/x-gtar` = abin("x-gtar", uncompressible, "gtar") - val `application/x-gzip` = abin("x-gzip", uncompressible, "gzip") + val `application/vnd.google-earth.kmz` = abin("vnd.google-earth.kmz", NotCompressible, "kmz") + val `application/vnd.ms-fontobject` = abin("vnd.ms-fontobject", Compressible, "eot") + val `application/vnd.oasis.opendocument.chart` = abin("vnd.oasis.opendocument.chart", Compressible, "odc") + val `application/vnd.oasis.opendocument.database` = abin("vnd.oasis.opendocument.database", Compressible, "odb") + val `application/vnd.oasis.opendocument.formula` = abin("vnd.oasis.opendocument.formula", Compressible, "odf") + val `application/vnd.oasis.opendocument.graphics` = abin("vnd.oasis.opendocument.graphics", Compressible, "odg") + val `application/vnd.oasis.opendocument.image` = abin("vnd.oasis.opendocument.image", Compressible, "odi") + val `application/vnd.oasis.opendocument.presentation` = abin("vnd.oasis.opendocument.presentation", Compressible, "odp") + val `application/vnd.oasis.opendocument.spreadsheet` = abin("vnd.oasis.opendocument.spreadsheet", Compressible, "ods") + val `application/vnd.oasis.opendocument.text` = abin("vnd.oasis.opendocument.text", Compressible, "odt") + val `application/vnd.oasis.opendocument.text-master` = abin("vnd.oasis.opendocument.text-master", Compressible, "odm", "otm") + val `application/vnd.oasis.opendocument.text-web` = abin("vnd.oasis.opendocument.text-web", Compressible, "oth") + val `application/vnd.openxmlformats-officedocument.presentationml.presentation` = abin("vnd.openxmlformats-officedocument.presentationml.presentation", Compressible, "pptx") + val `application/vnd.openxmlformats-officedocument.presentationml.slide` = abin("vnd.openxmlformats-officedocument.presentationml.slide", Compressible, "sldx") + val `application/vnd.openxmlformats-officedocument.presentationml.slideshow` = abin("vnd.openxmlformats-officedocument.presentationml.slideshow", Compressible, "ppsx") + val `application/vnd.openxmlformats-officedocument.presentationml.template` = abin("vnd.openxmlformats-officedocument.presentationml.template", Compressible, "potx") + val `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` = abin("vnd.openxmlformats-officedocument.spreadsheetml.sheet", Compressible, "xlsx") + val `application/vnd.openxmlformats-officedocument.spreadsheetml.template` = abin("vnd.openxmlformats-officedocument.spreadsheetml.template", Compressible, "xltx") + val `application/vnd.openxmlformats-officedocument.wordprocessingml.document` = abin("vnd.openxmlformats-officedocument.wordprocessingml.document", Compressible, "docx") + val `application/vnd.openxmlformats-officedocument.wordprocessingml.template` = abin("vnd.openxmlformats-officedocument.wordprocessingml.template", Compressible, "dotx") + val `application/x-7z-compressed` = abin("x-7z-compressed", NotCompressible, "7z", "s7z") + val `application/x-ace-compressed` = abin("x-ace-compressed", NotCompressible, "ace") + val `application/x-apple-diskimage` = abin("x-apple-diskimage", NotCompressible, "dmg") + val `application/x-arc-compressed` = abin("x-arc-compressed", NotCompressible, "arc") + val `application/x-bzip` = abin("x-bzip", NotCompressible, "bz") + val `application/x-bzip2` = abin("x-bzip2", NotCompressible, "boz", "bz2") + val `application/x-chrome-extension` = abin("x-chrome-extension", NotCompressible, "crx") + val `application/x-compress` = abin("x-compress", NotCompressible, "z") + val `application/x-compressed` = abin("x-compressed", NotCompressible, "gz") + val `application/x-debian-package` = abin("x-debian-package", Compressible, "deb") + val `application/x-dvi` = abin("x-dvi", Compressible, "dvi") + val `application/x-font-truetype` = abin("x-font-truetype", Compressible, "ttf") + val `application/x-font-opentype` = abin("x-font-opentype", Compressible, "otf") + val `application/x-gtar` = abin("x-gtar", NotCompressible, "gtar") + val `application/x-gzip` = abin("x-gzip", NotCompressible, "gzip") val `application/x-latex` = awoc("x-latex", "latex", "ltx") - val `application/x-rar-compressed` = abin("x-rar-compressed", uncompressible, "rar") - val `application/x-redhat-package-manager` = abin("x-redhat-package-manager", uncompressible, "rpm") - val `application/x-shockwave-flash` = abin("x-shockwave-flash", uncompressible, "swf") - val `application/x-tar` = abin("x-tar", compressible, "tar") - val `application/x-tex` = abin("x-tex", compressible, "tex") - val `application/x-texinfo` = abin("x-texinfo", compressible, "texi", "texinfo") + val `application/x-rar-compressed` = abin("x-rar-compressed", NotCompressible, "rar") + val `application/x-redhat-package-manager` = abin("x-redhat-package-manager", NotCompressible, "rpm") + val `application/x-shockwave-flash` = abin("x-shockwave-flash", NotCompressible, "swf") + val `application/x-tar` = abin("x-tar", Compressible, "tar") + val `application/x-tex` = abin("x-tex", Compressible, "tex") + val `application/x-texinfo` = abin("x-texinfo", Compressible, "texi", "texinfo") val `application/x-vrml` = awoc("x-vrml", "vrml") val `application/x-www-form-urlencoded` = awoc("x-www-form-urlencoded") - val `application/x-x509-ca-cert` = abin("x-x509-ca-cert", compressible, "der") - val `application/x-xpinstall` = abin("x-xpinstall", uncompressible, "xpi") + val `application/x-x509-ca-cert` = abin("x-x509-ca-cert", Compressible, "der") + val `application/x-xpinstall` = abin("x-xpinstall", NotCompressible, "xpi") val `application/xhtml+xml` = awoc("xhtml+xml") val `application/xml-dtd` = awoc("xml-dtd") val `application/xml` = awoc("xml") - val `application/zip` = abin("zip", uncompressible, "zip") + val `application/zip` = abin("zip", NotCompressible, "zip") - val `audio/aiff` = aud("aiff", compressible, "aif", "aifc", "aiff") - val `audio/basic` = aud("basic", compressible, "au", "snd") - val `audio/midi` = aud("midi", compressible, "mid", "midi", "kar") - val `audio/mod` = aud("mod", uncompressible, "mod") - val `audio/mpeg` = aud("mpeg", uncompressible, "m2a", "mp2", "mp3", "mpa", "mpga") - val `audio/ogg` = aud("ogg", uncompressible, "oga", "ogg") - val `audio/voc` = aud("voc", uncompressible, "voc") - val `audio/vorbis` = aud("vorbis", uncompressible, "vorbis") - val `audio/voxware` = aud("voxware", uncompressible, "vox") - val `audio/wav` = aud("wav", compressible, "wav") - val `audio/x-realaudio` = aud("x-pn-realaudio", uncompressible, "ra", "ram", "rmm", "rmp") - val `audio/x-psid` = aud("x-psid", compressible, "sid") - val `audio/xm` = aud("xm", uncompressible, "xm") - val `audio/webm` = aud("webm", uncompressible) + val `audio/aiff` = aud("aiff", Compressible, "aif", "aifc", "aiff") + val `audio/basic` = aud("basic", Compressible, "au", "snd") + val `audio/midi` = aud("midi", Compressible, "mid", "midi", "kar") + val `audio/mod` = aud("mod", NotCompressible, "mod") + val `audio/mpeg` = aud("mpeg", NotCompressible, "m2a", "mp2", "mp3", "mpa", "mpga") + val `audio/ogg` = aud("ogg", NotCompressible, "oga", "ogg") + val `audio/voc` = aud("voc", NotCompressible, "voc") + val `audio/vorbis` = aud("vorbis", NotCompressible, "vorbis") + val `audio/voxware` = aud("voxware", NotCompressible, "vox") + val `audio/wav` = aud("wav", Compressible, "wav") + val `audio/x-realaudio` = aud("x-pn-realaudio", NotCompressible, "ra", "ram", "rmm", "rmp") + val `audio/x-psid` = aud("x-psid", Compressible, "sid") + val `audio/xm` = aud("xm", NotCompressible, "xm") + val `audio/webm` = aud("webm", NotCompressible) - val `image/gif` = img("gif", uncompressible, "gif") - val `image/jpeg` = img("jpeg", uncompressible, "jpe", "jpeg", "jpg") - val `image/pict` = img("pict", compressible, "pic", "pict") - val `image/png` = img("png", uncompressible, "png") - val `image/svg+xml` = img("svg+xml", compressible, "svg") - val `image/tiff` = img("tiff", compressible, "tif", "tiff") - val `image/x-icon` = img("x-icon", compressible, "ico") - val `image/x-ms-bmp` = img("x-ms-bmp", compressible, "bmp") - val `image/x-pcx` = img("x-pcx", compressible, "pcx") - val `image/x-pict` = img("x-pict", compressible, "pct") - val `image/x-quicktime` = img("x-quicktime", uncompressible, "qif", "qti", "qtif") - val `image/x-rgb` = img("x-rgb", compressible, "rgb") - val `image/x-xbitmap` = img("x-xbitmap", compressible, "xbm") - val `image/x-xpixmap` = img("x-xpixmap", compressible, "xpm") - val `image/webp` = img("webp", uncompressible, "webp") + val `image/gif` = img("gif", NotCompressible, "gif") + val `image/jpeg` = img("jpeg", NotCompressible, "jpe", "jpeg", "jpg") + val `image/pict` = img("pict", Compressible, "pic", "pict") + val `image/png` = img("png", NotCompressible, "png") + val `image/svg+xml` = img("svg+xml", Compressible, "svg") + val `image/svgz` = registerFileExtensions(image("svg+xml", Gzipped, "svgz")) + val `image/tiff` = img("tiff", Compressible, "tif", "tiff") + val `image/x-icon` = img("x-icon", Compressible, "ico") + val `image/x-ms-bmp` = img("x-ms-bmp", Compressible, "bmp") + val `image/x-pcx` = img("x-pcx", Compressible, "pcx") + val `image/x-pict` = img("x-pict", Compressible, "pct") + val `image/x-quicktime` = img("x-quicktime", NotCompressible, "qif", "qti", "qtif") + val `image/x-rgb` = img("x-rgb", Compressible, "rgb") + val `image/x-xbitmap` = img("x-xbitmap", Compressible, "xbm") + val `image/x-xpixmap` = img("x-xpixmap", Compressible, "xpm") + val `image/webp` = img("webp", NotCompressible, "webp") val `message/http` = msg("http") val `message/delivery-status` = msg("delivery-status") @@ -406,7 +422,7 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] { val `text/asp` = txt("asp", "asp") val `text/cache-manifest` = txt("cache-manifest", "manifest") - val `text/calendar` = txt("calendar", "ics", "icz") + val `text/calendar` = txt("calendar", "ics") val `text/css` = txt("css", "css") val `text/csv` = txt("csv", "csv") val `text/html` = txt("html", "htm", "html", "htmls", "htx") diff --git a/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala index ea3eaa3aad..5bb0ef48e8 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala @@ -19,7 +19,7 @@ import HttpEncodings._ import HttpMethods._ class HttpHeaderSpec extends FreeSpec with Matchers { - val `application/vnd.spray` = MediaType.applicationBinary("vnd.spray", compressible = true) + val `application/vnd.spray` = MediaType.applicationBinary("vnd.spray", MediaType.Compressible) val PROPFIND = HttpMethod.custom("PROPFIND") "The HTTP header model must correctly parse and render the headers" - { @@ -38,9 +38,9 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Accept: */*, text/*; foo=bar, custom/custom; bar=\"b>az\"" =!= Accept(`*/*`, MediaRange.custom("text", Map("foo" -> "bar")), - MediaType.customBinary("custom", "custom", compressible = true, params = Map("bar" -> "b>az"))) + MediaType.customBinary("custom", "custom", MediaType.Compressible, params = Map("bar" -> "b>az"))) "Accept: application/*+xml; version=2" =!= - Accept(MediaType.customBinary("application", "*+xml", compressible = true, params = Map("version" -> "2"))) + Accept(MediaType.customBinary("application", "*+xml", MediaType.Compressible, params = Map("version" -> "2"))) } "Accept-Charset" in { @@ -208,7 +208,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers { `Content-Type`(`multipart/mixed` withBoundary "ABC/123" withCharset `UTF-8`) .renderedTo("""multipart/mixed; boundary="ABC/123"; charset=UTF-8""") "Content-Type: application/*" =!= - `Content-Type`(MediaType.customBinary("application", "*", compressible = false, allowArbitrarySubtypes = true)) + `Content-Type`(MediaType.customBinary("application", "*", MediaType.Compressible, allowArbitrarySubtypes = true)) } "Content-Range" in { diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala index 68b6b4c591..ac31b1cb17 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectivesSpec.scala @@ -9,7 +9,6 @@ import java.io.File import scala.concurrent.duration._ import scala.concurrent.{ ExecutionContext, Future } import scala.util.Properties -import akka.util.ByteString import org.scalatest.matchers.Matcher import org.scalatest.{ Inside, Inspectors } import akka.http.scaladsl.model.MediaTypes._ @@ -92,6 +91,30 @@ class FileAndResourceDirectivesSpec extends RoutingSpec with Inspectors with Ins } } finally file.delete } + + "support precompressed files with registered MediaType" in { + val file = File.createTempFile("akkaHttpTest", ".svgz") + try { + writeAllText("123", file) + Get() ~> getFromFile(file) ~> check { + mediaType shouldEqual `image/svg+xml` + header[`Content-Encoding`] shouldEqual Some(`Content-Encoding`(HttpEncodings.gzip)) + responseAs[String] shouldEqual "123" + } + } finally file.delete + } + + "support files with registered MediaType and .gz suffix" in { + val file = File.createTempFile("akkaHttpTest", ".js.gz") + try { + writeAllText("456", file) + Get() ~> getFromFile(file) ~> check { + mediaType shouldEqual `application/javascript` + header[`Content-Encoding`] shouldEqual Some(`Content-Encoding`(HttpEncodings.gzip)) + responseAs[String] shouldEqual "456" + } + } finally file.delete + } } "getFromResource" should { diff --git a/akka-http/src/main/scala/akka/http/scaladsl/coding/Encoder.scala b/akka-http/src/main/scala/akka/http/scaladsl/coding/Encoder.scala index bf29f27b5d..39bfc4d8a9 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/coding/Encoder.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/coding/Encoder.scala @@ -46,7 +46,7 @@ object Encoder { case res @ HttpResponse(status, _, _, _) ⇒ isCompressible(res) && status.isSuccess } private[coding] def isCompressible(msg: HttpMessage): Boolean = - msg.entity.contentType.mediaType.compressible + msg.entity.contentType.mediaType.isCompressible private[coding] val isContentEncodingHeader: HttpHeader ⇒ Boolean = _.isInstanceOf[`Content-Encoding`] } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala index f25e7d463f..9ff0fb8ddf 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/CodingDirectives.scala @@ -108,6 +108,16 @@ trait CodingDirectives { */ def decodeRequest: Directive0 = decodeRequestWith(DefaultCoders: _*) + + /** + * Inspects the response entity and adds a `Content-Encoding: gzip` response header if + * the entities media-type is precompressed with gzip and no `Content-Encoding` header is present yet. + */ + def withPrecompressedMediaTypeSupport: Directive0 = + mapResponse { response ⇒ + if (response.entity.contentType.mediaType.comp != MediaType.Gzipped) response + else response.withDefaultHeaders(headers.`Content-Encoding`(HttpEncodings.gzip)) + } } object CodingDirectives extends CodingDirectives { diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectives.scala index 00e800593b..47640228bb 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/FileAndResourceDirectives.scala @@ -26,7 +26,6 @@ trait FileAndResourceDirectives { import RouteDirectives._ import BasicDirectives._ import RouteConcatenation._ - import RangeDirectives._ /** * Completes GET requests with the content of the given file. @@ -51,12 +50,10 @@ trait FileAndResourceDirectives { if (file.isFile && file.canRead) conditionalFor(file.length, file.lastModified) { if (file.length > 0) { - withRangeSupport { - extractSettings { settings ⇒ - complete { - HttpEntity.Default(contentType, file.length, - Source.file(file).withAttributes(ActorAttributes.dispatcher(settings.fileIODispatcher))) - } + withRangeSupportAndPrecompressedMediaTypeSupportAndExtractSettings { settings ⇒ + complete { + HttpEntity.Default(contentType, file.length, + Source.file(file).withAttributes(ActorAttributes.dispatcher(settings.fileIODispatcher))) } } } else complete(HttpEntity.Empty) @@ -90,13 +87,11 @@ trait FileAndResourceDirectives { case Some(ResourceFile(url, length, lastModified)) ⇒ conditionalFor(length, lastModified) { if (length > 0) { - withRangeSupport { - extractSettings { settings ⇒ - complete { - HttpEntity.Default(contentType, length, - Source.inputStream(() ⇒ url.openStream()) - .withAttributes(ActorAttributes.dispatcher(settings.fileIODispatcher))) - } + withRangeSupportAndPrecompressedMediaTypeSupportAndExtractSettings { settings ⇒ + complete { + HttpEntity.Default(contentType, length, + Source.inputStream(() ⇒ url.openStream()) + .withAttributes(ActorAttributes.dispatcher(settings.fileIODispatcher))) } } } else complete(HttpEntity.Empty) @@ -186,6 +181,11 @@ trait FileAndResourceDirectives { } object FileAndResourceDirectives extends FileAndResourceDirectives { + private val withRangeSupportAndPrecompressedMediaTypeSupportAndExtractSettings = + RangeDirectives.withRangeSupport & + CodingDirectives.withPrecompressedMediaTypeSupport & + BasicDirectives.extractSettings + private def withTrailingSlash(path: String): String = if (path endsWith "/") path else path + '/' private def fileSystemPath(base: String, path: Uri.Path, log: LoggingAdapter, separator: Char = File.separatorChar): String = { import java.lang.StringBuilder @@ -260,11 +260,16 @@ object ContentTypeResolver { def withDefaultCharset(charset: HttpCharset): ContentTypeResolver = new ContentTypeResolver { def apply(fileName: String) = { - val ext = fileName.lastIndexOf('.') match { - case -1 ⇒ "" - case x ⇒ fileName.substring(x + 1) - } - val mediaType = MediaTypes.forExtension(ext) getOrElse MediaTypes.`application/octet-stream` + val lastDotIx = fileName.lastIndexOf('.') + val mediaType = if (lastDotIx >= 0) { + fileName.substring(lastDotIx + 1) match { + case "gz" ⇒ fileName.lastIndexOf('.', lastDotIx - 1) match { + case -1 ⇒ MediaTypes.`application/octet-stream` + case x ⇒ MediaTypes.forExtension(fileName.substring(x + 1, lastDotIx)).withComp(MediaType.Gzipped) + } + case ext ⇒ MediaTypes.forExtension(ext) + } + } else MediaTypes.`application/octet-stream` ContentType(mediaType, () ⇒ charset) } }