Skip to content

Commit 700e1c1

Browse files
committed
cache: Support ETag, If-Match and If-None-Match.
Send ETag and handle If-Match and If-None-Match. This solves #15.
1 parent d93f6fd commit 700e1c1

File tree

4 files changed

+71
-33
lines changed

4 files changed

+71
-33
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.lascala.http
1919

2020
import akka.actor._
2121
import akka.util.ByteString
22+
import HttpRequest._
2223

2324
/**
2425
* iteratee: http://www.haskell.org/haskellwiki/Iteratee_I/O
@@ -33,7 +34,7 @@ object HttpIteratees {
3334
(meth, (path, query), httpver) = requestLine
3435
headers <- readHeaders
3536
body <- readBody(headers)
36-
} yield HttpRequest(meth, path, query, httpver, headers, body)
37+
} yield HttpRequest(meth, path, query, httpver, Headers(headers), body)
3738
}
3839

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

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ case class HttpRequest(
2828
path: List[String],
2929
query: Option[String],
3030
httpver: String,
31-
headers: List[Header],
31+
headers: Headers,
3232
body: Option[ByteString])
3333

3434
case class Header(name: String, value: String)
35+
36+
case class Headers(headers: List[Header]) {
37+
def get(name: String) = headers.find(_.name.toLowerCase == name.toLowerCase)
38+
}

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

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,14 @@ import java.util.TimeZone
3030
import akka.actor.IO
3131
import util.Failure
3232
import com.lascala.libs.Enumerator
33+
import java.security.MessageDigest
34+
import java.security.DigestInputStream
35+
import java.io.BufferedInputStream
36+
import com.lascala.http.HttpDate._
3337

3438
trait HttpResponse {
3539
def lastModified: Date
40+
def etag: ByteString
3641
def body: ByteString
3742
def status: ByteString
3843
def reason: ByteString
@@ -52,14 +57,7 @@ object HttpResponse {
5257
val close = ByteString("Close")
5358
val date = ByteString("Date: ")
5459
val lastModified = ByteString("Last-Modified: ")
55-
56-
def httpDateFormat = {
57-
val dateFormat = new SimpleDateFormat(RFC1123_DATE_PATTERN, Locale.ENGLISH)
58-
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"))
59-
dateFormat
60-
}
61-
62-
def httpDate(date: Date) = ByteString(httpDateFormat.format(date))
60+
val etag = ByteString("ETag: ")
6361

6462
def readFile(file: File) = {
6563
val resource = new Array[Byte](file.length.toInt)
@@ -69,13 +67,21 @@ object HttpResponse {
6967
ByteString(resource)
7068
}
7169

70+
def computeETag(file: File) = {
71+
val algorithm = MessageDigest.getInstance("SHA1")
72+
val dis = new DigestInputStream(new BufferedInputStream(new FileInputStream(file)), algorithm)
73+
while (dis.read() != -1) {}
74+
algorithm.digest().fold("")(_ + "%02x" format _).toString
75+
}
76+
7277
def bytes(rsp: HttpResponse) = {
7378
(new ByteStringBuilder ++=
7479
version ++= SP ++= rsp.status ++= SP ++= rsp.reason ++= CRLF ++=
7580
(if(rsp.body.nonEmpty || rsp.mimeType.nonEmpty) rsp.contentType ++ CRLF else ByteString.empty) ++=
7681
rsp.cacheControl ++= CRLF ++=
77-
date ++= httpDate(new Date) ++= CRLF ++=
78-
Option(rsp.lastModified).map(lastModified ++ httpDate(_) ++ CRLF).getOrElse(ByteString("")) ++=
82+
date ++= ByteString(HttpDate(new Date).asString) ++= CRLF ++=
83+
Option(rsp.lastModified).map((v) => lastModified ++ ByteString(HttpDate(v).asString) ++ CRLF).getOrElse(ByteString("")) ++=
84+
Option(rsp.etag).map(etag ++ _ ++ CRLF).getOrElse(ByteString("")) ++=
7985
server ++= CRLF ++=
8086
rsp.contentLength ++= CRLF ++=
8187
connection ++= (if (rsp.shouldKeepAlive) keepAlive else close) ++= CRLF ++= CRLF ++= rsp.body).result
@@ -86,8 +92,8 @@ object HttpResponse {
8692
version ++= SP ++= rsp.status ++= SP ++= rsp.reason ++= CRLF ++=
8793
rsp.contentType ++ CRLF ++=
8894
rsp.cacheControl ++= CRLF ++=
89-
date ++= httpDate(new Date) ++= CRLF ++=
90-
Option(rsp.lastModified).map(lastModified ++ httpDate(_) ++ CRLF).getOrElse(ByteString("")) ++=
95+
date ++= ByteString(HttpDate(new Date).asString) ++= CRLF ++=
96+
Option(rsp.lastModified).map((v) => lastModified ++ ByteString(HttpDate(v).asString) ++ CRLF).getOrElse(ByteString("")) ++=
9197
server ++= CRLF ++=
9298
connection ++= (if (rsp.shouldKeepAlive) keepAlive else close) ++= CRLF ++=
9399
ByteString("Transfer-Encoding: chunked") ++= CRLF ++= CRLF).result
@@ -111,17 +117,7 @@ trait ChunkedEncodable extends HttpResponse {
111117
def chunkedData: Enumerator[ByteString]
112118
}
113119

114-
object OKResponse {
115-
import HttpResponse._
116-
117-
def withFile(file: File) = OKResponse(
118-
body = readFile(file),
119-
shouldKeepAlive = true,
120-
mimeType = new Tika().detect(file),
121-
lastModified = new Date(file.lastModified))
122-
}
123-
124-
case class OKResponse(body: ByteString, shouldKeepAlive: Boolean = true, mimeType: String = "text/html", lastModified: Date = null) extends HttpResponse {
120+
case class OKResponse(body: ByteString = ByteString.empty, shouldKeepAlive: Boolean = true, mimeType: String = "text/html", lastModified: Date = null, etag: ByteString = null) extends HttpResponse {
125121
val status = ByteString("200")
126122
val reason = ByteString("OK")
127123

@@ -139,28 +135,39 @@ object OKResponse {
139135
def stream(chunk: Enumerator[ByteString]) = new OKResponse(body = ByteString.empty, mimeType = "text/html") with ChunkedEncodable {
140136
def chunkedData: Enumerator[ByteString] = chunk
141137
}
138+
139+
def fromFile(file: File) = new OKResponse(
140+
body = HttpResponse.readFile(file),
141+
shouldKeepAlive = true,
142+
mimeType = new Tika().detect(file),
143+
lastModified = new Date(file.lastModified),
144+
etag = ByteString(HttpResponse.computeETag(file)))
142145
}
143146

144147
case class NotModifiedResponse(body: ByteString = ByteString.empty, shouldKeepAlive: Boolean = false, mimeType: String = "") extends HttpResponse {
145148
val status = ByteString("304")
146149
val reason = ByteString("Not Modified")
147-
val lastModified = null;
150+
val lastModified = null
151+
val etag = null
148152
}
149153

150154
case class NotFoundError(body: ByteString = ByteString.empty, shouldKeepAlive: Boolean = false, mimeType: String = "") extends HttpResponse {
151155
val status = ByteString("404")
152156
val reason = ByteString("Not Found")
153-
val lastModified = null;
157+
val lastModified = null
158+
val etag = null
154159
}
155160

156161
case class MethodNotAllowedError(body: ByteString = ByteString.empty, shouldKeepAlive: Boolean = false, mimeType: String = "") extends HttpResponse {
157162
val status = ByteString("405")
158163
val reason = ByteString("Method Not Allowed")
159-
val lastModified = null;
164+
val lastModified = null
165+
val etag = null
160166
}
161167

162168
case class InternalServerError(body: ByteString = ByteString.empty, shouldKeepAlive: Boolean = false, mimeType: String = "") extends HttpResponse {
163169
val status = ByteString("500")
164170
val reason = ByteString("Internal Server Error")
165-
val lastModified = null;
171+
val lastModified = null
172+
val etag = null
166173
}

src/main/scala/common/main.scala

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,16 @@ package common
2626
import akka.actor._
2727
import com.lascala.http._
2828
import com.lascala.http.HttpResponse._
29+
import com.lascala.http.HttpDate._
2930
import com.lascala.http.HttpConstants._
3031
import akka.util.ByteString
3132
import java.io.File
3233
import com.lascala.libs.Enumerator
3334
import org.apache.tika.Tika
35+
import java.util.Date
36+
import java.text.SimpleDateFormat
37+
import java.util.Locale
38+
import java.util.TimeZone
3439

3540
/**
3641
* Sample Demo Application
@@ -44,6 +49,29 @@ object Main extends App {
4449
class RequestHandler extends Actor {
4550
val docroot = "."
4651

52+
def matchETag(headerValue: String, etag: String) =
53+
headerValue == "*" || headerValue == etag
54+
55+
def matchETag(headerValue: String, file: File): Boolean =
56+
matchETag(headerValue, computeETag(file))
57+
58+
def isModified(file: File, headers: Headers) =
59+
headers.get("if-modified-since") match {
60+
case Some(Header(_, value)) =>
61+
HttpDate(value).asDate.compareTo(HttpDate(file.lastModified).asDate) match {
62+
case -1 => false
63+
case 0 => headers.get("if-match") match {
64+
case Some(Header(_, value)) => matchETag(value, file)
65+
case _ => headers.get("if-none-match") match {
66+
case Some(Header(_, value)) => !matchETag(value, file)
67+
case _ => false
68+
}
69+
}
70+
case _ => true
71+
}
72+
case _ => true
73+
}
74+
4775
def receive = {
4876
case HttpRequest("GET", List("chunked"), _, _, headers, _) =>
4977
sender ! OKResponse.stream(getEnumerator)
@@ -52,10 +80,8 @@ class RequestHandler extends Actor {
5280
case HttpRequest("GET", pathSegments, _, _, headers, _) =>
5381
new File(docroot, "/" + pathSegments.mkString(File.separator)) match {
5482
case file if file.isFile() =>
55-
headers.find(_.name.toLowerCase == "if-modified-since") match {
56-
case Some(d) if HttpResponse.httpDateFormat.parse(d.value).compareTo(new java.util.Date(file.lastModified)) != -1 => sender ! NotModifiedResponse()
57-
case _ => sender ! OKResponse.withFile(file)
58-
}
83+
if (isModified(file, headers)) sender ! OKResponse.fromFile(file)
84+
else sender ! NotModifiedResponse()
5985
case _ => sender ! NotFoundError()
6086
}
6187
case _ => sender ! MethodNotAllowedError()

0 commit comments

Comments
 (0)