Skip to content

Commit 0cdc56b

Browse files
author
Eun Woo Song
committed
content encoding - q param parser
1 parent 8f47731 commit 0cdc56b

File tree

7 files changed

+173
-46
lines changed

7 files changed

+173
-46
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* This software is licensed under the Apache 2 license, quoted below.
3+
*
4+
* Copyright 2009-2012 Typesafe Inc. <http://www.typesafe.com>
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
7+
* use this file except in compliance with the License. You may obtain a copy of
8+
* the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15+
* License for the specific language governing permissions and limitations under
16+
* the License.
17+
*/
18+
package com.lascala.http
19+
20+
case class Header(name: String, value: String)
21+
22+
object Header {
23+
final val ACCEPT = "Accept";
24+
final val ACCEPT_CHARSET = "Accept-Charset";
25+
final val ACCEPT_ENCODING = "Accept-Encoding";
26+
final val ACCEPT_LANGUAGE = "Accept-Language";
27+
final val ACCEPT_RANGES = "Accept-Ranges";
28+
final val AGE = "Age";
29+
final val ALLOW = "Allow";
30+
final val AUTHORIZATION = "Authorization";
31+
final val CACHE_CONTROL = "Cache-Control";
32+
final val CONNECTION = "Connection";
33+
final val CONTENT_ENCODING = "Content-Encoding";
34+
final val CONTENT_LANGUAGE = "Content-Language";
35+
final val CONTENT_LENGTH = "Content-Length";
36+
final val CONTENT_LOCATION = "Content-Location";
37+
final val CONTENT_MD5 = "Content-MD5";
38+
final val CONTENT_RANGE = "Content-Range";
39+
final val CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
40+
final val CONTENT_TYPE = "Content-Type";
41+
final val COOKIE = "Cookie";
42+
final val DATE = "Date";
43+
final val ETAG = "Etag";
44+
final val EXPECT = "Expect";
45+
final val EXPIRES = "Expires";
46+
final val FROM = "From";
47+
final val HOST = "Host";
48+
final val IF_MATCH = "If-Match";
49+
final val IF_MODIFIED_SINCE = "If-Modified-Since";
50+
final val IF_NONE_MATCH = "If-None-Match";
51+
final val IF_RANGE = "If-Range";
52+
final val IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
53+
final val LAST_MODIFIED = "Last-Modified";
54+
final val LOCATION = "Location";
55+
final val MAX_FORWARDS = "Max-Forwards";
56+
final val PRAGMA = "Pragma";
57+
final val PROXY_AUTHENTICATE = "Proxy-Authenticate";
58+
final val PROXY_AUTHORIZATION = "Proxy-Authorization";
59+
final val RANGE = "Range";
60+
final val REFERER = "Referer";
61+
final val RETRY_AFTER = "Retry-After";
62+
final val SERVER = "Server";
63+
final val SET_COOKIE = "Set-Cookie";
64+
final val SET_COOKIE2 = "Set-Cookie2";
65+
final val TE = "Te";
66+
final val TRAILER = "Trailer";
67+
final val TRANSFER_ENCODING = "Transfer-Encoding";
68+
final val UPGRADE = "Upgrade";
69+
final val USER_AGENT = "User-Agent";
70+
final val VARY = "Vary";
71+
final val VIA = "Via";
72+
final val WARNING = "Warning";
73+
final val WWW_AUTHENTICATE = "WWW-Authenticate";
74+
final val ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
75+
final val ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
76+
final val ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
77+
final val ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
78+
final val ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
79+
final val ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
80+
final val ORIGIN = "Origin";
81+
final val ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
82+
final val ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
83+
}
84+
85+
case class Headers(headers: List[Header]) {
86+
def get(name: String): Option[Header] = headers.find(_.name.toLowerCase == name.toLowerCase)
87+
def topContentEncoding: Option[String] = get(Header.CONTENT_ENCODING).flatMap(Headers.parseQparameters(_).headOption).map(_.value)
88+
def contentEncodings: Seq[String] = get(Header.CONTENT_ENCODING).map(Headers.parseQparameters(_).map(_.value)).getOrElse(Seq.empty[String])
89+
}
90+
91+
object Headers {
92+
private final val VALUE_SEPARATOR = ","
93+
private final val Q_PARAM_SEPARATOR = ";"
94+
private final val Q_PARAM = "q="
95+
96+
case class QParamHeader(value: String, qParam: Double)
97+
98+
def parseQparameters(header: Header) = header.value.split(VALUE_SEPARATOR).toSeq.map{
99+
_.split(Q_PARAM_SEPARATOR) match {
100+
case Array(value, qParam) =>
101+
val qRate = if(qParam.contains(Q_PARAM)) qParam.split(Q_PARAM).last.toDouble else 1.0
102+
QParamHeader(value.trim, qRate)
103+
case Array(value) => QParamHeader(value.trim, 1.0)
104+
}
105+
}.sortWith(_.qParam > _.qParam)
106+
}

src/main/scala/com/lascala/http/HttpConstants.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
/*
2+
* This software is licensed under the Apache 2 license, quoted below.
3+
*
4+
* Copyright 2009-2012 Typesafe Inc. <http://www.typesafe.com>
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
7+
* use this file except in compliance with the License. You may obtain a copy of
8+
* the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15+
* License for the specific language governing permissions and limitations under
16+
* the License.
17+
*/
118
package com.lascala.http
219

320
import akka.util.ByteString

src/main/scala/com/lascala/http/request/HttpRequest.scala

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package com.lascala.http.request
1919

2020
import akka.util.ByteString
21+
import com.lascala.http.Headers
2122

2223
/**
2324
* http request 객체 및
@@ -30,27 +31,3 @@ case class HttpRequest(
3031
httpver: String,
3132
headers: Headers,
3233
body: Option[ByteString])
33-
34-
case class Header(name: String, value: String)
35-
36-
object Header {
37-
val VALUE_SEPARATOR = ","
38-
val Q_PARAM_SEPARATOR = ";"
39-
val Q_PARAM = "q="
40-
41-
case class QParamHeader(value: String, qParam: Double)
42-
43-
def parseQparameters(header: Header) = header.value.split(VALUE_SEPARATOR).toSeq.map{
44-
_ match {
45-
case h if h contains(Q_PARAM_SEPARATOR) =>
46-
val Array(value, qParam) = h.split(Q_PARAM_SEPARATOR)
47-
val qRate = if(qParam.contains("q=")) qParam.split("q=").last.toDouble else 1.0
48-
QParamHeader(value.trim, qRate)
49-
case h => QParamHeader(h.trim, 1.0)
50-
}
51-
}.sortWith(_.qParam > _.qParam)
52-
}
53-
54-
case class Headers(headers: List[Header]) {
55-
def get(name: String) = headers.find(_.name.toLowerCase == name.toLowerCase)
56-
}

src/main/scala/com/lascala/http/response/OKResponse.scala

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ import java.io.File
2424
import java.util.Date
2525
import org.apache.tika.Tika
2626

27-
case class OKResponse(body: ByteString = ByteString.empty, shouldKeepAlive: Boolean = true,
28-
mimeType: String = "text/html", lastModified: Date = null,
29-
etag: ByteString = null) extends HttpResponse {
27+
case class OKResponse(body: ByteString = ByteString.empty,
28+
shouldKeepAlive: Boolean = true,
29+
mimeType: String = "text/html",
30+
lastModified: Date = null,
31+
etag: ByteString = null) extends HttpResponse {
3032
val status = ByteString("200")
3133
val reason = ByteString("OK")
3234

3335
def withMimeType(mimeType: String) = this match {
34-
// In case of ChunkedEncodable, need to manually instantiate a new OKResponse with ChunkedEncodable
36+
// In case of ChunkedEncodable, need to manually instantiate a new OKResponse with ChunkedEncodable
3537
// Instead of just using copy method in order to preserve the ChunkedEncodable type.
36-
case t: ChunkedEncodable =>
38+
case t: ChunkedEncodable =>
3739
new OKResponse(this.body, this.shouldKeepAlive, mimeType) with ChunkedEncodable {
3840
def chunkedData: Enumerator[ByteString] = t.chunkedData
3941
}
@@ -43,15 +45,15 @@ case class OKResponse(body: ByteString = ByteString.empty, shouldKeepAlive: Bool
4345

4446
object OKResponse {
4547

46-
def stream(chunk: Enumerator[ByteString]) =
48+
def stream(chunk: Enumerator[ByteString]) =
4749
new OKResponse(body = ByteString.empty, mimeType = "text/html") with ChunkedEncodable {
4850
def chunkedData: Enumerator[ByteString] = chunk
4951
}
5052

5153
def fromFile(file: File) = new OKResponse(
52-
body = HttpResponse.readFile(file),
54+
body = HttpResponse.readFile(file),
5355
shouldKeepAlive = true,
54-
mimeType = new Tika().detect(file),
55-
lastModified = new Date(file.lastModified),
56-
etag = ByteString(HttpResponse.computeETag(file)))
56+
mimeType = new Tika().detect(file),
57+
lastModified = new Date(file.lastModified),
58+
etag = ByteString(HttpResponse.computeETag(file)))
5759
}

src/main/scala/com/lascala/http/server/HttpIteratees.scala

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ package com.lascala.http.server
1919

2020
import akka.actor._
2121
import akka.util.ByteString
22-
import com.lascala.http.HttpConstants
23-
import com.lascala.http.request.{Header, Headers, HttpRequest}
22+
import com.lascala.http.{Header, Headers, HttpConstants}
23+
import com.lascala.http.request.HttpRequest
2424

2525
/**
2626
* iteratee: http://www.haskell.org/haskellwiki/Iteratee_I/O
@@ -33,9 +33,12 @@ object HttpIteratees {
3333
for {
3434
requestLine <- readRequestLine
3535
(meth, (path, query), httpver) = requestLine
36-
eheaders <- readHeaders
36+
headers <- readHeaders
3737
body <- readBody(headers)
38-
} yield HttpRequest(meth, path, query, httpver, Headers(headers), body)
38+
} yield {
39+
40+
HttpRequest(meth, path, query, httpver, Headers(headers), body)
41+
}
3942
}
4043

4144
def ascii(bytes: ByteString): String = bytes.decodeString("US-ASCII").trim

src/main/scala/common/main.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import com.lascala.http._
2727
import com.lascala.http.HttpResponse._
2828
import java.io.File
2929
import com.lascala.libs.Enumerator
30-
import request.{HttpRequest, Header, Headers}
30+
import request.HttpRequest
3131
import response.error.{MethodNotAllowedError, NotFoundError}
3232
import response.OKResponse
3333
import response.other.NotModifiedResponse

src/test/scala/com/lascala/http/request/HeaderTest.scala renamed to src/test/scala/com/lascala/http/HeadersTest.scala

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@
1515
* License for the specific language governing permissions and limitations under
1616
* the License.
1717
*/
18-
package com.lascala.http.request
18+
package com.lascala.http
1919

2020
import org.scalatest._
2121
import org.scalatest.matchers._
22-
import Header.QParamHeader
22+
import com.lascala.http.Headers.QParamHeader
2323

24-
class HeaderTest extends FlatSpec with ShouldMatchers {
25-
"Header" can "process Content-Encoding header with q parameters correctly" in {
26-
val res = Header.parseQparameters(Header("Content-Encoding","gzip; q=0.7, deflate; q=0.8, test; q=1.000, test2; q=0.000"))
24+
class HeadersTest extends FlatSpec with ShouldMatchers {
25+
26+
"Headers" can "process Content-Encoding header with q parameters correctly" in {
27+
val res = Headers.parseQparameters(Header("Content-Encoding","gzip; q=0.7, deflate; q=0.8, test; q=1.000, test2; q=0.000"))
2728
val expectedQParamHeaders = Seq(
2829
QParamHeader("test", 1.000),
2930
QParamHeader("deflate", 0.8),
@@ -34,7 +35,7 @@ class HeaderTest extends FlatSpec with ShouldMatchers {
3435
}
3536

3637
it can "process Content-Encoding header WITHOUT q parameters correctly" in {
37-
val res = Header.parseQparameters(Header("Content-Encoding","gzip, deflate, test, test2"))
38+
val res = Headers.parseQparameters(Header("Content-Encoding","gzip, deflate, test, test2"))
3839
val expectedQParamHeaders = Seq(
3940
QParamHeader("gzip", 1.0),
4041
QParamHeader("deflate", 1.0),
@@ -45,7 +46,7 @@ class HeaderTest extends FlatSpec with ShouldMatchers {
4546
}
4647

4748
it can "process Content-Encoding header with values that may or may not have q values" in {
48-
val res = Header.parseQparameters(Header("Content-Encoding","gzip; q=0.7, deflate, test; q=0.5, test2"))
49+
val res = Headers.parseQparameters(Header("Content-Encoding","gzip; q=0.7, deflate, test; q=0.5, test2"))
4950

5051
val expectedQParamHeaders = Seq(
5152
QParamHeader("deflate", 1.0),
@@ -55,4 +56,25 @@ class HeaderTest extends FlatSpec with ShouldMatchers {
5556

5657
res should be (expectedQParamHeaders)
5758
}
59+
60+
it can "retrieve the highest priority content encoding based on q parameter" in {
61+
val headers = List(
62+
Header(Header.ACCEPT_CHARSET, "Accept-Charset: utf-8"),
63+
Header(Header.CONTENT_ENCODING, "gzip; q=0.7, deflate, test; q=0.5, test2"),
64+
Header(Header.ACCEPT, "Accept: text/plain")
65+
)
66+
67+
Headers(headers).topContentEncoding should be (Some("deflate"))
68+
}
69+
70+
it can "retrieve content encodings in priority order based on q parameter" in {
71+
val headers = List(
72+
Header(Header.ACCEPT_CHARSET, "Accept-Charset: utf-8"),
73+
Header(Header.CONTENT_ENCODING, "gzip; q=0.7, deflate, test; q=0.5, test2"),
74+
Header(Header.ACCEPT, "Accept: text/plain")
75+
)
76+
77+
Headers(headers).contentEncodings should not be (Seq("test2", "test", "gzip", "deflate"))
78+
Headers(headers).contentEncodings should be (Seq("deflate", "test2", "gzip", "test"))
79+
}
5880
}

0 commit comments

Comments
 (0)