Skip to content

Commit

Permalink
cache: Handle Last-Modified and If-Modified-Since.
Browse files Browse the repository at this point in the history
This solves #13.
  • Loading branch information
eungjun-yi committed Jan 13, 2013
1 parent 43ad2f6 commit 8cbc9c6
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 38 deletions.
67 changes: 45 additions & 22 deletions src/main/scala/com/lascala/http/HttpResponse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

This comment has been minimized.

Copy link
@eungjun-yi

eungjun-yi Jan 14, 2013

Author Member

File 객체를 받는 OKResponse가 필요해서 부득이 이렇게 분리해버렸는데, OKResponse 하나로 다 처리하는 방법은 없을까요? @codeport/http-pull

This comment has been minimized.

Copy link
@rampart81

rampart81 Jan 14, 2013

Factory pattern 을 쓰면 어떻할까요? 즉 OKResponse 의 object 를 만드신후 ByteString 대신 file 을 받고 OKResponse 를 리턴하는 메소드 를 제공 해주시면 syntax 상으로 봤을때 깔끔하고 편리할꺼 같은데요. 예를 들어:

object OKResponse {
  //  readFile 메서드 는 HttpResponse 오브젝트로 옴김.
  import HttpResponse._

  def withFile(file: File) = OKResponse(
    body = readFile(file), 
    shouldKeepAlive = true,
    mimeType =  new Tika().detect(file))
}

위에 처럼 하면 밑에 처럼 파일을 이용한 OKResponse 를 리턴할수 있습니다.

OKResponse.withFile(file)

Play

This comment has been minimized.

Copy link
@rampart81

rampart81 Jan 14, 2013

글을 쓰다가 모르고 enter 눌러버렸네요 ㅎㅎ

위의 방식은 Play Framework 에서도 자주 쓰는 방식인거 같네요 ^^ 저도 Chunked body 를 서포트 하는 방식을 위에 처럼 하려고 합니다.
그래서 밑에 처럼 call 이 가능하게요 ^^

OKResponse.withChunkedData(source)

This comment has been minimized.

Copy link
@eungjun-yi

eungjun-yi Jan 14, 2013

Author Member

@rampart81 좋은 것 같습니다!

그런데 스칼라에서 class(case class?)와 같은 이름의 object를 만드는 일이 흔한 것인가요? 헷갈릴 것 같은데 의외로 많이 쓰는 것도 같아서 정착된 패턴인가 하는 생각이 드네요.

This comment has been minimized.

Copy link
@rampart81

rampart81 Jan 14, 2013

@npcode

네 자주 쓰이는 패턴 입니다 ^^ 이미 아시겠지만 class/case class/trait 등등 과 object 가 이름이 같은것을 companion object 라고 하는데 자주 쓰이지요. 예로 스칼라의 Extractor 기능을 들수 있겠네요. companion object 에 apply/unapply 메소드를 제공하여 패턴매칭 에서 유용하게 쓰이지요. case class 의 경우는 이미 companion object 가 자동적으로 생성되는 경우입니다. 저의 경우에도 companion object 가 없는 클라스 는 거의 쓰지 않는것 같습니다 ^^

This comment has been minimized.

Copy link
@eungjun-yi

eungjun-yi Jan 14, 2013

Author Member

@rampart81 아 companion object라고 하는군요! 이건 Programming in Scala 좀 읽어봐야겠네요.

자세한 설명 고맙습니다. 또 하나 배우고 가네요.

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")
Expand All @@ -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("?")
}


24 changes: 8 additions & 16 deletions src/main/scala/common/main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
}
}

0 comments on commit 8cbc9c6

Please sign in to comment.