From 8cbc9c6ea016e25a7baf6eb67073d7f2ca90d7d0 Mon Sep 17 00:00:00 2001 From: Yi EungJun Date: Sun, 13 Jan 2013 21:43:33 +0900 Subject: [PATCH] cache: Handle Last-Modified and If-Modified-Since. This solves #13. --- .../scala/com/lascala/http/HttpResponse.scala | 67 +++++++++++++------ src/main/scala/common/main.scala | 24 +++---- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/main/scala/com/lascala/http/HttpResponse.scala b/src/main/scala/com/lascala/http/HttpResponse.scala index aedccf2..607bc2e 100644 --- a/src/main/scala/com/lascala/http/HttpResponse.scala +++ b/src/main/scala/com/lascala/http/HttpResponse.scala @@ -17,47 +17,85 @@ */ package com.lascala.http +import com.lascala.http.HttpConstants._ + import akka.util.{ ByteString, ByteStringBuilder } -import HttpConstants._ +import java.io.File +import org.apache.tika.Tika +import java.io.FileInputStream +import java.util.Date +import java.util.Locale +import java.text.SimpleDateFormat +import java.util.TimeZone trait HttpResponse { + def lastModified: Date = null def body: ByteString def status: ByteString def reason: ByteString def mimeType: String def shouldKeepAlive: Boolean - // default charset to utf-8 for now but should be editable in the future - def contentType = if (!mimeType.isEmpty) ByteString(s"Content-Type: ${mimeType}") else ByteString("") + def contentType = ByteString(s"Content-Type: ${mimeType}") def cacheControl = ByteString("Cache-Control: no-cache") def contentLength = ByteString(s"Content-Length: ${body.length.toString}") } object HttpResponse { val version = ByteString("HTTP/1.1") - val date = ByteString("Date: ") val server = ByteString("Server: lascala-http") val connection = ByteString("Connection: ") val keepAlive = ByteString("Keep-Alive") val close = ByteString("Close") - + val date = ByteString("Date: ") + val lastModified = ByteString("Last-Modified: ") + + def httpDateFormat = { + val dateFormat = new SimpleDateFormat(RFC1123_DATE_PATTERN, Locale.ENGLISH) + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")) + dateFormat + } + + def httpDate(date: Date) = ByteString(httpDateFormat.format(date)) + def bytes(rsp: HttpResponse) = { (new ByteStringBuilder ++= version ++= SP ++= rsp.status ++= SP ++= rsp.reason ++= CRLF ++= - (if(rsp.body.nonEmpty) rsp.contentType ++ CRLF else ByteString.empty) ++= + (if(rsp.body.nonEmpty || rsp.mimeType.nonEmpty) rsp.contentType ++ CRLF else ByteString.empty) ++= rsp.cacheControl ++= CRLF ++= - date ++= ByteString(new java.util.Date().toString) ++= CRLF ++= + date ++= httpDate(new Date) ++= CRLF ++= + Option(rsp.lastModified).map(lastModified ++ httpDate(_) ++ CRLF).getOrElse(ByteString("")) ++= server ++= CRLF ++= rsp.contentLength ++= CRLF ++= connection ++= (if (rsp.shouldKeepAlive) keepAlive else close) ++= CRLF ++= CRLF ++= rsp.body).result } } +case class OKFileResponse(file: File, shouldKeepAlive: Boolean = true) extends HttpResponse { + def readFile(file: File) = { + val resource = new Array[Byte](file.length.toInt) + val in = new FileInputStream(file) + in.read(resource) + in.close() + ByteString(resource) + } + val body = readFile(file) + val mimeType = new Tika().detect(file) + val status = ByteString("200") + val reason = ByteString("OK") + override def lastModified = new Date(file.lastModified) +} + case class OKResponse(body: ByteString, shouldKeepAlive: Boolean = true, mimeType: String = "text/html") extends HttpResponse { val status = ByteString("200") val reason = ByteString("OK") } +case class NotModifiedResponse(body: ByteString = ByteString.empty, shouldKeepAlive: Boolean = false, mimeType: String = "") extends HttpResponse { + val status = ByteString("304") + val reason = ByteString("Not Modified") +} + case class NotFoundError(body: ByteString = ByteString.empty, shouldKeepAlive: Boolean = false, mimeType: String = "") extends HttpResponse { val status = ByteString("404") val reason = ByteString("Not Found") @@ -72,18 +110,3 @@ case class InternalServerError(body: ByteString = ByteString.empty, shouldKeepAl val status = ByteString("500") val reason = ByteString("Internal Server Error") } - -/** - * HTTP 상수 모음 - */ -object HttpConstants { - val SP = ByteString(" ") - val HT = ByteString("\t") - val CRLF = ByteString("\r\n") - val COLON = ByteString(":") - val PERCENT = ByteString("%") - val PATH = ByteString("/") - val QUERY = ByteString("?") -} - - diff --git a/src/main/scala/common/main.scala b/src/main/scala/common/main.scala index 5e0e4b2..533cb6c 100644 --- a/src/main/scala/common/main.scala +++ b/src/main/scala/common/main.scala @@ -26,10 +26,9 @@ package common import akka.actor._ import com.lascala.http._ import com.lascala.http.HttpResponse._ +import com.lascala.http.HttpConstants._ import akka.util.ByteString import java.io.File -import org.apache.tika.Tika -import java.io.FileInputStream /** * Sample Demo Application @@ -43,24 +42,17 @@ object Main extends App { class RequestHandler extends Actor { val docroot = "." - def readFile(file: File) = { - val resource = new Array[Byte](file.length.toInt) - val in = new FileInputStream(file) - in.read(resource) - in.close() - ByteString(resource) - } - - def mimeType(file: File) = new Tika().detect(file) - def receive = { - case HttpRequest("GET", pathSegments, _, _, _, _) => + case HttpRequest("GET", pathSegments, _, _, headers, _) => new File(docroot, "/" + pathSegments.mkString(File.separator)) match { case file if file.isFile() => - sender ! OKResponse(readFile(file), true, mimeType(file)) + headers.find(_.name.toLowerCase == "if-modified-since") match { + case Some(d) if HttpResponse.httpDateFormat.parse(d.value).compareTo(new java.util.Date(file.lastModified)) != -1 => sender ! NotModifiedResponse() + case _ => sender ! OKFileResponse(file, true) + } case _ => - sender ! NotFoundError + sender ! NotFoundError() } - case _ => sender ! MethodNotAllowedError + case _ => sender ! MethodNotAllowedError() } }