@@ -30,9 +30,14 @@ import java.util.TimeZone
30
30
import akka .actor .IO
31
31
import util .Failure
32
32
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 ._
33
37
34
38
trait HttpResponse {
35
39
def lastModified : Date
40
+ def etag : ByteString
36
41
def body : ByteString
37
42
def status : ByteString
38
43
def reason : ByteString
@@ -52,14 +57,7 @@ object HttpResponse {
52
57
val close = ByteString (" Close" )
53
58
val date = ByteString (" Date: " )
54
59
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: " )
63
61
64
62
def readFile (file : File ) = {
65
63
val resource = new Array [Byte ](file.length.toInt)
@@ -69,13 +67,21 @@ object HttpResponse {
69
67
ByteString (resource)
70
68
}
71
69
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
+
72
77
def bytes (rsp : HttpResponse ) = {
73
78
(new ByteStringBuilder ++=
74
79
version ++= SP ++= rsp.status ++= SP ++= rsp.reason ++= CRLF ++=
75
80
(if (rsp.body.nonEmpty || rsp.mimeType.nonEmpty) rsp.contentType ++ CRLF else ByteString .empty) ++=
76
81
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 (" " )) ++=
79
85
server ++= CRLF ++=
80
86
rsp.contentLength ++= CRLF ++=
81
87
connection ++= (if (rsp.shouldKeepAlive) keepAlive else close) ++= CRLF ++= CRLF ++= rsp.body).result
@@ -86,8 +92,8 @@ object HttpResponse {
86
92
version ++= SP ++= rsp.status ++= SP ++= rsp.reason ++= CRLF ++=
87
93
rsp.contentType ++ CRLF ++=
88
94
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 (" " )) ++=
91
97
server ++= CRLF ++=
92
98
connection ++= (if (rsp.shouldKeepAlive) keepAlive else close) ++= CRLF ++=
93
99
ByteString (" Transfer-Encoding: chunked" ) ++= CRLF ++= CRLF ).result
@@ -111,17 +117,7 @@ trait ChunkedEncodable extends HttpResponse {
111
117
def chunkedData : Enumerator [ByteString ]
112
118
}
113
119
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 {
125
121
val status = ByteString (" 200" )
126
122
val reason = ByteString (" OK" )
127
123
@@ -139,28 +135,39 @@ object OKResponse {
139
135
def stream (chunk : Enumerator [ByteString ]) = new OKResponse (body = ByteString .empty, mimeType = " text/html" ) with ChunkedEncodable {
140
136
def chunkedData : Enumerator [ByteString ] = chunk
141
137
}
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)))
142
145
}
143
146
144
147
case class NotModifiedResponse (body : ByteString = ByteString .empty, shouldKeepAlive : Boolean = false , mimeType : String = " " ) extends HttpResponse {
145
148
val status = ByteString (" 304" )
146
149
val reason = ByteString (" Not Modified" )
147
- val lastModified = null ;
150
+ val lastModified = null
151
+ val etag = null
148
152
}
149
153
150
154
case class NotFoundError (body : ByteString = ByteString .empty, shouldKeepAlive : Boolean = false , mimeType : String = " " ) extends HttpResponse {
151
155
val status = ByteString (" 404" )
152
156
val reason = ByteString (" Not Found" )
153
- val lastModified = null ;
157
+ val lastModified = null
158
+ val etag = null
154
159
}
155
160
156
161
case class MethodNotAllowedError (body : ByteString = ByteString .empty, shouldKeepAlive : Boolean = false , mimeType : String = " " ) extends HttpResponse {
157
162
val status = ByteString (" 405" )
158
163
val reason = ByteString (" Method Not Allowed" )
159
- val lastModified = null ;
164
+ val lastModified = null
165
+ val etag = null
160
166
}
161
167
162
168
case class InternalServerError (body : ByteString = ByteString .empty, shouldKeepAlive : Boolean = false , mimeType : String = " " ) extends HttpResponse {
163
169
val status = ByteString (" 500" )
164
170
val reason = ByteString (" Internal Server Error" )
165
- val lastModified = null ;
171
+ val lastModified = null
172
+ val etag = null
166
173
}
0 commit comments