!htc #16803 introduce proper model for type of media-type encoding specs

This commit is contained in:
Mathias 2015-03-30 17:31:38 +02:00
parent 37aa2cb886
commit 5074aebfeb
10 changed files with 109 additions and 69 deletions

View file

@ -185,10 +185,10 @@ public abstract class MediaTypes {
String mainType,
String subType,
boolean compressible,
boolean binary,
akka.http.model.MediaType.Encoding encoding,
Iterable<String> fileExtensions,
Map<String, String> params) {
return akka.http.model.MediaType.custom(mainType, subType, compressible, binary, Util.<String, String>convertIterable(fileExtensions), Util.convertMapToScala(params), false);
return akka.http.model.MediaType.custom(mainType, subType, encoding, compressible, Util.<String, String>convertIterable(fileExtensions), Util.convertMapToScala(params), false);
}
/**

View file

@ -37,19 +37,23 @@ object ContentTypeRange {
}
}
final case class ContentType(mediaType: MediaType, definedCharset: Option[HttpCharset]) extends japi.ContentType with ValueRenderable {
abstract case class ContentType private (mediaType: MediaType, definedCharset: Option[HttpCharset]) extends japi.ContentType with ValueRenderable {
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 getOrElse HttpCharsets.`UTF-8`
def charset: HttpCharset = definedCharset orElse mediaType.encoding.charset getOrElse HttpCharsets.`UTF-8`
def hasOpenCharset: Boolean = definedCharset.isEmpty && mediaType.encoding == MediaType.Encoding.Open
def withMediaType(mediaType: MediaType) =
if (mediaType != this.mediaType) copy(mediaType = mediaType) else this
if (mediaType != this.mediaType) ContentType(mediaType, definedCharset) else this
def withCharset(charset: HttpCharset) =
if (definedCharset.isEmpty || charset != definedCharset.get) copy(definedCharset = Some(charset)) else this
if (definedCharset.isEmpty || charset != definedCharset.get) ContentType(mediaType, charset) else this
def withoutDefinedCharset =
if (definedCharset.isDefined) copy(definedCharset = None) else this
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
/** Java API */
def getDefinedCharset: JOption[japi.HttpCharset] = definedCharset.asJava
@ -58,13 +62,27 @@ final case class ContentType(mediaType: MediaType, definedCharset: Option[HttpCh
object ContentType {
private[http] case object `; charset=` extends SingletonValueRenderable
def apply(mediaType: MediaType, charset: HttpCharset): ContentType = apply(mediaType, Some(charset))
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) {}
}
}
object ContentTypes {
// RFC4627 defines JSON to always be UTF encoded, we always render JSON to UTF-8
val `application/json` = ContentType(MediaTypes.`application/json`, HttpCharsets.`UTF-8`)
val `application/json` = ContentType(MediaTypes.`application/json`)
val `text/plain` = ContentType(MediaTypes.`text/plain`)
val `text/plain(UTF-8)` = ContentType(MediaTypes.`text/plain`, HttpCharsets.`UTF-8`)
val `application/octet-stream` = ContentType(MediaTypes.`application/octet-stream`)

View file

@ -78,7 +78,7 @@ object MediaRange {
override def isMultipart = mainType == "multipart"
override def isText = mainType == "text"
override def isVideo = mainType == "video"
def specimen = MediaType.custom(mainType, "custom")
def specimen = MediaType.custom(mainType, "custom", MediaType.Encoding.Binary)
}
def custom(mainType: String, params: Map[String, String] = Map.empty, qValue: Float = 1.0f): MediaRange = {
@ -165,7 +165,7 @@ object MediaRanges extends ObjectRegistry[String, MediaRange] {
sealed abstract case class MediaType private[http] (value: String)(val mainType: String,
val subType: String,
val compressible: Boolean,
val binary: Boolean,
val encoding: MediaType.Encoding,
val fileExtensions: immutable.Seq[String],
val params: Map[String, String])
extends japi.MediaType with LazyValueBytesRenderable with WithQValue[MediaRange] {
@ -191,7 +191,7 @@ sealed abstract case class MediaType private[http] (value: String)(val mainType:
}
class MultipartMediaType private[http] (_value: String, _subType: String, _params: Map[String, String])
extends MediaType(_value)("multipart", _subType, compressible = true, binary = true, Nil, _params) {
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)
@ -200,28 +200,47 @@ class MultipartMediaType private[http] (_value: String, _subType: String, _param
}
sealed abstract class NonMultipartMediaType private[http] (_value: String, _mainType: String, _subType: String,
_compressible: Boolean, _binary: Boolean,
_compressible: Boolean, _encoding: MediaType.Encoding,
_fileExtensions: immutable.Seq[String],
_params: Map[String, String])
extends MediaType(_value)(_mainType, _subType, _compressible, _binary, _fileExtensions, _params) {
private[http] def this(mainType: String, subType: String, compressible: Boolean, binary: Boolean, fileExtensions: immutable.Seq[String]) =
this(mainType + '/' + subType, mainType, subType, compressible, binary, fileExtensions, Map.empty)
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, compressible, binary, fileExtensions, params)
MediaType.custom(mainType, subType, encoding, compressible, fileExtensions, params)
}
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)
/**
* Indicates that a media-type is textual and mandates a clearly defined character encoding.
*/
final case class Fixed(cs: HttpCharset) extends Encoding(Some(cs))
}
/**
* Create a custom media type.
*/
def custom(mainType: String, subType: String, compressible: Boolean = false, binary: Boolean = false,
fileExtensions: immutable.Seq[String] = Nil, params: Map[String, String] = Map.empty,
allowArbitrarySubtypes: Boolean = false): MediaType = {
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!")
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, binary, fileExtensions, params) {
new NonMultipartMediaType(r.get, mainType, subType, compressible, encoding, fileExtensions, params) {
override def isApplication = mainType == "application"
override def isAudio = mainType == "audio"
override def isImage = mainType == "image"
@ -231,14 +250,16 @@ object MediaType {
}
}
def custom(value: String): MediaType = {
def custom(value: String, encoding: MediaType.Encoding): MediaType = {
val parts = value.split('/')
if (parts.length != 2) throw new IllegalArgumentException(value + " is not a valid media-type")
custom(parts(0), parts(1))
custom(parts(0), parts(1), encoding)
}
}
object MediaTypes extends ObjectRegistry[(String, String), MediaType] {
import MediaType.Encoding
private[this] var extensionMap = Map.empty[String, MediaType]
private def register(mediaType: MediaType): MediaType = {
@ -253,33 +274,34 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] {
def forExtension(ext: String): Option[MediaType] = extensionMap.get(ext.toRootLowerCase)
private def app(subType: String, compressible: Boolean, binary: Boolean, fileExtensions: String*) = register {
new NonMultipartMediaType("application", subType, compressible, binary, immutable.Seq(fileExtensions: _*)) {
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, binary = true, immutable.Seq(fileExtensions: _*)) {
new NonMultipartMediaType("audio", subType, compressible, encoding = Encoding.Binary, immutable.Seq(fileExtensions: _*)) {
override def isAudio = true
}
}
private def img(subType: String, compressible: Boolean, binary: Boolean, fileExtensions: String*) = register {
new NonMultipartMediaType("image", subType, compressible, binary, immutable.Seq(fileExtensions: _*)) {
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, binary = false, immutable.Seq(fileExtensions: _*)) {
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, binary = false, immutable.Seq(fileExtensions: _*)) {
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, binary = true, immutable.Seq(fileExtensions: _*)) {
new NonMultipartMediaType("video", subType, compressible = false, encoding = Encoding.Binary, immutable.Seq(fileExtensions: _*)) {
override def isVideo = true
}
}
@ -288,21 +310,21 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] {
// format: OFF
private final val compressible = true // compile-time constant
private final val uncompressible = false // compile-time constant
private final val binary = true // compile-time constant
private final val notBinary = false // compile-time constant
private def binary = Encoding.Binary
private def openEncoding = Encoding.Open
// dummy value currently only used by ContentType.NoContentType
private[http] val NoMediaType = new NonMultipartMediaType("none", "none", false, false, immutable.Seq.empty) {}
private[http] val NoMediaType = new NonMultipartMediaType("none", "none", false, Encoding.Binary, immutable.Seq.empty) {}
val `application/atom+xml` = app("atom+xml", compressible, notBinary, "atom")
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, notBinary, "js")
val `application/json` = app("json", compressible, binary, "json") // we treat JSON as binary, since its encoding is not variable but defined by RFC4627
val `application/json-patch+json` = app("json-patch+json", compressible, binary) // we treat JSON as binary, since its encoding is not variable but defined by RFC4627
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")
@ -310,10 +332,10 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] {
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, notBinary, "rss")
val `application/soap+xml` = app("soap+xml", compressible, notBinary)
val `application/vnd.api+json` = app("vnd.api+json", compressible, binary) // we treat JSON as binary, since its encoding is not variable but defined by RFC4627
val `application/vnd.google-earth.kml+xml` = app("vnd.google-earth.kml+xml", compressible, notBinary, "kml")
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")
@ -356,13 +378,13 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] {
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, notBinary, "vrml")
val `application/x-www-form-urlencoded` = app("x-www-form-urlencoded", compressible, notBinary)
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, notBinary)
val `application/xml-dtd` = app("xml-dtd", compressible, notBinary)
val `application/xml` = app("xml", compressible, notBinary)
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 `audio/aiff` = aud("aiff", compressible, "aif", "aifc", "aiff")
@ -384,7 +406,7 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] {
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, notBinary, "svg")
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")
@ -481,4 +503,4 @@ object MediaTypes extends ObjectRegistry[(String, String), MediaType] {
val `video/x-sgi-movie` = vid("x-sgi-movie", "movie", "mv")
val `video/webm` = vid("webm", "webm")
// format: ON
}
}

View file

@ -27,7 +27,8 @@ private[parser] trait CommonActions {
case mainLower
MediaTypes.getForKey((mainLower, subType.toRootLowerCase)) match {
case Some(registered) if (params.isEmpty) registered else registered.withParams(params)
case None MediaType.custom(mainType, subType, params = params, allowArbitrarySubtypes = true)
case None MediaType.custom(mainType, subType, encoding = MediaType.Encoding.Open,
params = params, allowArbitrarySubtypes = true)
}
}
}

View file

@ -172,7 +172,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
|Server: akka-http/1.0.0
|Date: Thu, 25 Aug 2011 09:10:29 GMT
|Connection: close
|Content-Type: application/json; charset=UTF-8
|Content-Type: application/json
|
|""", close = true)
}
@ -184,7 +184,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
|Server: akka-http/1.0.0
|Date: Thu, 25 Aug 2011 09:10:29 GMT
|Connection: close
|Content-Type: application/json; charset=UTF-8
|Content-Type: application/json
|
|abcdefg""", close = true)
}
@ -212,7 +212,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
|Age: 30
|Server: akka-http/1.0.0
|Date: Thu, 25 Aug 2011 09:10:29 GMT
|Content-Type: application/json; charset=UTF-8
|Content-Type: application/json
|
|"""
}
@ -283,7 +283,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
|Server: akka-http/1.0.0
|Date: Thu, 25 Aug 2011 09:10:29 GMT
|Connection: close
|Content-Type: application/json; charset=UTF-8
|Content-Type: application/json
|
|abcdefg""", close = true)
}

View file

@ -17,7 +17,7 @@ import HttpEncodings._
import HttpMethods._
class HttpHeaderSpec extends FreeSpec with Matchers {
val `application/vnd.spray` = MediaType.custom("application/vnd.spray")
val `application/vnd.spray` = MediaType.custom("application/vnd.spray", MediaType.Encoding.Binary)
val PROPFIND = HttpMethod.custom("PROPFIND")
"The HTTP header model must correctly parse and render the headers" - {
@ -36,9 +36,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", params = Map("bar" -> "b>az")))
MediaType.custom("custom", "custom", MediaType.Encoding.Binary, params = Map("bar" -> "b>az")))
"Accept: application/*+xml; version=2" =!=
Accept(MediaType.custom("application", "*+xml", params = Map("version" -> "2")))
Accept(MediaType.custom("application", "*+xml", MediaType.Encoding.Binary, params = Map("version" -> "2")))
}
"Accept-Charset" in {
@ -190,8 +190,9 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
`Content-Type`(`application/pdf`)
"Content-Type: text/plain; charset=utf8" =!=
`Content-Type`(ContentType(`text/plain`, `UTF-8`)).renderedTo("text/plain; charset=UTF-8")
"Content-Type: text/xml; version=3; charset=windows-1252" =!=
`Content-Type`(ContentType(MediaType.custom("text", "xml", params = Map("version" -> "3")), HttpCharsets.getForKey("windows-1252")))
"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: text/plain; charset=fancy-pants" =!=
`Content-Type`(ContentType(`text/plain`, HttpCharset.custom("fancy-pants")))
"Content-Type: multipart/mixed; boundary=ABC123" =!=
@ -199,7 +200,8 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
"Content-Type: multipart/mixed; boundary=\"ABC/123\"" =!=
`Content-Type`(ContentType(`multipart/mixed` withBoundary "ABC/123"))
"Content-Type: application/*" =!=
`Content-Type`(ContentType(MediaType.custom("application", "*", allowArbitrarySubtypes = true)))
`Content-Type`(ContentType(MediaType.custom("application", "*", MediaType.Encoding.Binary,
allowArbitrarySubtypes = true)))
}
"Content-Range" in {

View file

@ -81,7 +81,7 @@ class HttpModelIntegrationSpec extends WordSpec with Matchers with BeforeAndAfte
}
val textHeaders: Seq[(String, String)] = entityTextHeaders ++ partialTextHeaders
textHeaders shouldEqual Seq(
"Content-Type" -> "application/json; charset=UTF-8",
"Content-Type" -> "application/json",
"Content-Length" -> "5",
"Host" -> "localhost",
"Origin" -> "null")