├── src ├── main │ └── scala │ │ └── webmachine │ │ ├── Route.scala │ │ ├── sample │ │ ├── SampleResource.scala │ │ └── Sample.scala │ │ ├── Dispatcher.scala │ │ ├── WebmachineJetty.scala │ │ ├── Request.scala │ │ ├── Response.scala │ │ ├── Resource.scala │ │ └── DecisionCore.scala └── test │ └── scala │ └── webmachine │ ├── TestHelper.scala │ ├── b12Spec.scala │ ├── b10Spec.scala │ ├── b07Spec.scala │ ├── b09Spec.scala │ ├── b11Spec.scala │ ├── b08Spec.scala │ ├── b06Spec.scala │ ├── b04Spec.scala │ ├── g11Spec.scala │ ├── b13Spec.scala │ ├── b03Spec.scala │ ├── c04Spec.scala │ ├── g07Spec.scala │ ├── b05Spec.scala │ ├── e06Spec.scala │ ├── d05Spec.scala │ └── f07Spec.scala ├── README.md └── pom.xml /src/main/scala/webmachine/Route.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import scala.collection.mutable.{Map => MM} 3 | object Route { 4 | val routes = MM[String, Resource]() 5 | def apply(path:String, resource:Resource) = routes + (path -> resource) 6 | } -------------------------------------------------------------------------------- /src/main/scala/webmachine/sample/SampleResource.scala: -------------------------------------------------------------------------------- 1 | package webmachine.sample 2 | 3 | import webmachine._ 4 | 5 | class SampleResource extends Resource { 6 | override def to_html(request: Request, response: Response) = hello world.toString 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/main/scala/webmachine/sample/Sample.scala: -------------------------------------------------------------------------------- 1 | package webmachine.sample 2 | 3 | import webmachine._ 4 | 5 | object Sample { 6 | 7 | def main(args: Array[String]) { 8 | Route("/", new SampleResource) 9 | WebmachineJetty.start(8080) 10 | } 11 | } -------------------------------------------------------------------------------- /src/main/scala/webmachine/Dispatcher.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | 3 | object Dispatcher { 4 | def d(requestUri: String, req: Request, res: Response) = { 5 | dispatch(Route.routes(requestUri), req, res) 6 | } 7 | def dispatch(resource: Resource, req: Request, res: Response) = { 8 | DecisionCore.handle_request(resource, req, res) 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Scala-WebMachine 2 | ================ 3 | 4 | The initial port of Basho's [WebMachine][basho]. 5 | 6 | To run the sample resource - mvn exec:java -Dexec.mainClass="webmachine.sample.Sample" 7 | 8 | TODO 9 | ==== 10 | * Implement missing unit tests 11 | * Refactor the Response object to be more immutable 12 | * Implement the State 13 | 14 | 15 | [basho]: http://bitbucket.org/justin/webmachine/wiki/Home 16 | -------------------------------------------------------------------------------- /src/test/scala/webmachine/TestHelper.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | 3 | import org.springframework.mock.web._ 4 | 5 | object TestHelper { 6 | 7 | def httpGetRequest = { 8 | val httpRequest = new MockHttpServletRequest 9 | httpRequest.setMethod("GET") 10 | httpRequest 11 | } 12 | 13 | def httpPostRequest = { 14 | val httpRequest = new MockHttpServletRequest 15 | httpRequest.setMethod("POST") 16 | httpRequest 17 | } 18 | 19 | def httpPutRequest = { 20 | val httpRequest = new MockHttpServletRequest 21 | httpRequest.setMethod("PUT") 22 | httpRequest 23 | } 24 | 25 | def httpOptionsRequest = { 26 | val httpRequest = new MockHttpServletRequest 27 | httpRequest.setMethod("OPTIONS") 28 | httpRequest 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/scala/webmachine/WebmachineJetty.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | 3 | import java.io.IOException 4 | import javax.servlet.ServletException 5 | import javax.servlet.http.HttpServletRequest 6 | import javax.servlet.http.HttpServletResponse 7 | 8 | import org.mortbay.jetty.Handler 9 | import org.mortbay.jetty.Server 10 | import org.mortbay.jetty.handler.AbstractHandler 11 | 12 | object WebmachineJettyHandler extends AbstractHandler { 13 | def handle(requestUri:String, httpRequest: HttpServletRequest, httpResponse:HttpServletResponse, dispatch: Int) { 14 | val req = Request(httpRequest) 15 | val res = Response(httpResponse) 16 | Dispatcher.d(requestUri, req, res) 17 | res.flush 18 | } 19 | } 20 | 21 | object WebmachineJetty { 22 | def start(port:Int) = { 23 | val server = new Server(port) 24 | server.setHandler(WebmachineJettyHandler) 25 | server.start() 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/b12Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class b12SpecTest extends JUnit4(b12Spec) 9 | 10 | object b12Spec extends Specification { 11 | trait TestResource extends Resource { 12 | override def known_methods(request: Request, response: Response) = List("GET") 13 | override def to_html(request: Request, response: Response) = "good stuff" 14 | } 15 | 16 | "webmachine" should { 17 | "respond with 200 when POST method is known by resource" >> { 18 | val req = new Request(httpGetRequest) 19 | val res = Response(new MockHttpServletResponse) 20 | dispatch(new Resource with TestResource, req, res) 21 | res.status must beEqualTo("200") 22 | res.body must beEqualTo("good stuff") 23 | } 24 | 25 | "respond with '501 Not Implemented' when method is not known" >> { 26 | val req = new Request(httpPutRequest) 27 | val res = Response(new MockHttpServletResponse) 28 | dispatch(new Resource with TestResource, req, res) 29 | res.status must beEqualTo("501") 30 | res.body must beEqualTo("") 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/b10Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class b10SpecTest extends JUnit4(b10Spec) 9 | 10 | object b10Spec extends Specification { 11 | trait TestResource extends Resource { 12 | override def allowed_methods(request: Request, response: Response) = List("GET") 13 | override def to_html(request: Request, response: Response) = "good stuff" 14 | } 15 | 16 | "webmachine" should { 17 | "respond with 200 when GET request is allowed" >> { 18 | val base = httpGetRequest 19 | val req = new Request(base) 20 | val res = Response(new MockHttpServletResponse) 21 | dispatch(new Resource with TestResource, req, res) 22 | res.status must beEqualTo("200") 23 | res.body must beEqualTo("good stuff") 24 | } 25 | 26 | "respond with '405 Method Not Allowed' when POST is not allowed" >> { 27 | val req = new Request(httpPostRequest) 28 | val res = Response(new MockHttpServletResponse) 29 | dispatch(new Resource with TestResource, req, res) 30 | res.status must beEqualTo("405") 31 | res.body must beEqualTo("") 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/b07Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class b07SpecTest extends JUnit4(b07Spec) 9 | 10 | 11 | object b07Spec extends Specification { 12 | 13 | "webmachine" should { 14 | "respond with 200 when resource is not forbidden" >> { 15 | trait TestResource extends Resource { 16 | override def forbidden(request: Request, response: Response) = false 17 | override def to_html(request: Request, response: Response) = "some body" 18 | } 19 | val req = new Request(httpGetRequest) 20 | val res = Response(new MockHttpServletResponse) 21 | dispatch(new Resource with TestResource, req, res) 22 | res.status must beEqualTo("200") 23 | res.body must beEqualTo("some body") 24 | } 25 | 26 | "respond with '403 forbidden' when resource is forbidden" >> { 27 | trait TestResource extends Resource { 28 | override def forbidden(request: Request, response: Response) = true 29 | } 30 | val req = new Request(httpGetRequest) 31 | val res = Response(new MockHttpServletResponse) 32 | dispatch(new Resource with TestResource, req, res) 33 | res.status must beEqualTo("403") 34 | res.body must beEqualTo("") 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/b09Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class b09SpecTest extends JUnit4(b09Spec) 9 | 10 | object b09Spec extends Specification { 11 | trait TestResource extends Resource { 12 | override def malformed_request(request: Request, response: Response) = { 13 | request.base.getParameter("important-stuff") == null 14 | } 15 | override def to_html(request: Request, response: Response) = "good stuff" 16 | } 17 | 18 | "webmachine" should { 19 | "respond with 200 when request is well formed" >> { 20 | val base = httpGetRequest 21 | base.setParameter("important-stuff", "2345") 22 | val req = new Request(base) 23 | val res = Response(new MockHttpServletResponse) 24 | dispatch(new Resource with TestResource, req, res) 25 | res.status must beEqualTo("200") 26 | res.body must beEqualTo("good stuff") 27 | } 28 | 29 | "respond with '400 bad request' when request is mal-formed" >> { 30 | val req = new Request(httpGetRequest) 31 | val res = Response(new MockHttpServletResponse) 32 | dispatch(new Resource with TestResource, req, res) 33 | res.status must beEqualTo("400") 34 | res.body must beEqualTo("") 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/b11Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class b11SpecTest extends JUnit4(b11Spec) 9 | 10 | object b11Spec extends Specification { 11 | trait TestResource extends Resource { 12 | override def uri_too_long(request: Request, response: Response) = request.base.getRequestURI().length() > 20 13 | override def to_html(request: Request, response: Response) = "good stuff" 14 | } 15 | 16 | "webmachine" should { 17 | "respond with 200 when uri is not too long" >> { 18 | val base = httpGetRequest 19 | base.setRequestURI("someuri" * 2) 20 | val req = new Request(base) 21 | val res = Response(new MockHttpServletResponse) 22 | dispatch(new Resource with TestResource, req, res) 23 | res.status must beEqualTo("200") 24 | res.body must beEqualTo("good stuff") 25 | } 26 | 27 | "respond with '414 Request URI Too Long' when uri is too long" >> { 28 | val base = httpGetRequest 29 | base.setRequestURI("someuri" * 10) 30 | val req = new Request(base) 31 | val res = Response(new MockHttpServletResponse) 32 | dispatch(new Resource with TestResource, req, res) 33 | res.status must beEqualTo("414") 34 | res.body must beEqualTo("") 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/b08Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class b08SpecTest extends JUnit4(b08Spec) 9 | 10 | object b08Spec extends Specification { 11 | 12 | "webmachine" should { 13 | "respond with 200 when resource is authorized" >> { 14 | trait TestResource extends Resource { 15 | override def is_authorized(request: Request, response: Response) = true 16 | override def to_html(request: Request, response: Response) = "some body" 17 | } 18 | val req = new Request(httpGetRequest) 19 | val res = Response(new MockHttpServletResponse) 20 | dispatch(new Resource with TestResource, req, res) 21 | res.status must beEqualTo("200") 22 | res.body must beEqualTo("some body") 23 | } 24 | 25 | "respond with '401 unauthorized' when resource is forbidden" >> { 26 | trait TestResource extends Resource { 27 | override def is_authorized(request: Request, response: Response) = { 28 | request.hasHeader("authorizaton-key") 29 | } 30 | } 31 | val req = new Request(httpGetRequest) 32 | val res = Response(new MockHttpServletResponse) 33 | dispatch(new Resource with TestResource, req, res) 34 | res.status must beEqualTo("401") 35 | res.body must beEqualTo("") 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/b06Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class b06SpecTest extends JUnit4(b06Spec) 9 | 10 | 11 | object b06Spec extends Specification { 12 | 13 | "webmachine" should { 14 | "respond with 200 when valid content headers" >> { 15 | trait TestResource extends Resource { 16 | override def valid_content_headers(request: Request, response: Response) = { 17 | true 18 | } 19 | override def to_html(request: Request, response: Response) = "some body" 20 | } 21 | val req = new Request(httpGetRequest) 22 | val res = Response(new MockHttpServletResponse) 23 | dispatch(new Resource with TestResource, req, res) 24 | res.status must beEqualTo("200") 25 | res.body must beEqualTo("some body") 26 | } 27 | 28 | "respond with '501 Not implemented' when invalid content headers" >> { 29 | trait TestResource extends Resource { 30 | override def valid_content_headers(request: Request, response: Response) = { 31 | false 32 | } 33 | } 34 | val req = new Request(httpGetRequest) 35 | val res = Response(new MockHttpServletResponse) 36 | dispatch(new Resource with TestResource, req, res) 37 | res.status must beEqualTo("501") 38 | res.body must beEqualTo("") 39 | } 40 | 41 | 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/scala/webmachine/Request.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | 3 | import javax.servlet.http.HttpServletRequest 4 | 5 | object Request { 6 | def apply(base : HttpServletRequest) = new Request(base) 7 | } 8 | 9 | class Request(val base: HttpServletRequest) { 10 | val method = base.getMethod 11 | val if_unmodified_since = base.getDateHeader("if-unmodified-since") 12 | val if_modified_since = base.getDateHeader("if-modified-since") 13 | val content_length = base.getContentLength 14 | val content_type: Option[String] = { 15 | if(base.getContentType() == null) None 16 | else Some(base.getContentType()) 17 | } 18 | 19 | def hasHeader(name:String) = header(name) != null 20 | def header(name:String) = base.getHeader(name) 21 | 22 | def accept_best_match(content_types: List[String]):Option[String] = { 23 | val accept = base.getHeader("accept") 24 | content_types.find { accept.contains(_) } 25 | } 26 | 27 | def accept_language_best_match(langs: List[String]): Option[String] = { 28 | val accept = base.getHeader("accept-language") 29 | langs.find { accept.contains(_) } 30 | } 31 | 32 | def accept_charset_best_match(charsets: List[String]): Option[String] = { 33 | val accept = base.getHeader("accept-charset") 34 | charsets.find { accept.contains(_) } 35 | } 36 | 37 | def accept_encoding_back_match(encodings: List[String]): Option[String] = { 38 | val accept = base.getHeader("accept-encoding") 39 | encodings.find { accept.contains(_) } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/b04Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class b04SpecTest extends JUnit4(b04Spec) 9 | 10 | 11 | object b04Spec extends Specification { 12 | trait TestResource extends Resource { 13 | override def valid_entity_length(request: Request, response: Response) = { 14 | request.content_length < 30 15 | } 16 | override def to_html(request: Request, response: Response) = "some body" 17 | } 18 | 19 | "webmachine" should { 20 | "respond with 200 when entity length is valid" >> { 21 | val httpRequest = httpGetRequest 22 | httpRequest.setContent("nilanjan".getBytes) 23 | val req = Request(httpRequest) 24 | val res = Response(new MockHttpServletResponse) 25 | dispatch(new Resource with TestResource, req, res) 26 | res.status must beEqualTo("200") 27 | res.body must beEqualTo("some body") 28 | } 29 | 30 | "set response status to '413 Request Entity Too Large' when entity length crosses limit" >> { 31 | val httpRequest = httpGetRequest 32 | httpRequest.setContent("This is a really big entity and should fail the length check".getBytes) 33 | val req = Request(httpRequest) 34 | val res = Response(new MockHttpServletResponse) 35 | Dispatcher.dispatch(new Resource with TestResource, req, res) 36 | res.status must beEqualTo("413") 37 | res.body must beEqualTo("") 38 | } 39 | 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/scala/webmachine/Response.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import javax.servlet.http.HttpServletResponse 3 | 4 | object Response { 5 | def apply(base: HttpServletResponse) = new Response(base) 6 | } 7 | class Response(val base:HttpServletResponse) { 8 | var content_language: Option[String] = None 9 | var content_encoding:Option[String] = None 10 | var last_modified:Option[Long] = None 11 | var expires: Option[Long] = None 12 | var charset:Option[String] = None 13 | var location:Option[String] = None 14 | var etag: Option[String] = None 15 | var allowed:List[String] = Nil 16 | var headers:Map[String, String] = Map() 17 | 18 | var status:String = _ 19 | var body:String = "" 20 | var content_type:String = _ 21 | 22 | //TODO: find a better way to update the base response object 23 | def flush = { 24 | base.setStatus(status.toInt) 25 | if(content_language.isDefined) base.addHeader("Content-Language", content_language.get) 26 | if(content_encoding.isDefined) base.addHeader("Content-Encoding", content_encoding.get) 27 | if(last_modified.isDefined) base.addDateHeader("Last-Modified", last_modified.get) 28 | if(expires.isDefined) base.addDateHeader("Expires", expires.get) 29 | charset match { 30 | case Some(char_encoding) => base.setContentType(content_type + "; charset=" + char_encoding) 31 | case None => base.setContentType(content_type) 32 | } 33 | //TODO: do we have to encode the url here? Not sure whether supporting the session yet 34 | if(location.isDefined) base.sendRedirect(location.get) 35 | if(etag.isDefined) base.addHeader("ETag", etag.get) 36 | base.addHeader("Allow", allowed.mkString(", ")) 37 | headers.foreach { (t: (String, String)) => base.addHeader(t._1, t._2) } 38 | base.getOutputStream.println(body) 39 | base.flushBuffer 40 | } 41 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/g11Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class g11SpecTest extends JUnit4(g11Spec) 9 | 10 | object g11Spec extends Specification { 11 | trait TestResource extends Resource { 12 | override def resource_exists(request: Request, response: Response) = true 13 | override def generate_etag(request: Request, response: Response) = Some("bar") 14 | override def to_html(request: Request, response: Response) = "foo" 15 | } 16 | 17 | "webmachine" should { 18 | "respond with 200 when no if-match found in request header" >> { 19 | val req = new Request(httpGetRequest) 20 | val res = Response(new MockHttpServletResponse) 21 | dispatch(new Resource with TestResource, req, res) 22 | res.status must beEqualTo("200") 23 | res.body must beEqualTo("foo") 24 | } 25 | "respond with 200 when if-match found in request header and it matches" >> { 26 | val base = httpGetRequest 27 | base.addHeader("if-match", "bar") 28 | val req = new Request(httpGetRequest) 29 | val res = Response(new MockHttpServletResponse) 30 | dispatch(new Resource with TestResource, req, res) 31 | res.status must beEqualTo("200") 32 | res.body must beEqualTo("foo") 33 | } 34 | 35 | "respond with '412 Precondition Failed' when if-match found in request header but doesn't match" >> { 36 | val base = httpGetRequest 37 | base.addHeader("if-match", "baz") 38 | val req = new Request(base) 39 | val res = Response(new MockHttpServletResponse) 40 | dispatch(new Resource with TestResource, req, res) 41 | res.status must beEqualTo("412") 42 | res.body must beEqualTo("") 43 | } 44 | 45 | } 46 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/b13Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class b13SpecTest extends JUnit4(b13Spec) 9 | 10 | object b13Spec extends Specification { 11 | 12 | "webmachine" should { 13 | "respond with 200 when resource responds to ping" >> { 14 | trait TestResource extends Resource { 15 | override def ping(request: Request, response: Response) = true 16 | override def to_html(request: Request, response: Response) = "i am available" 17 | } 18 | 19 | val req = new Request(httpGetRequest) 20 | val res = Response(new MockHttpServletResponse) 21 | dispatch(new Resource with TestResource, req, res) 22 | res.status must beEqualTo("200") 23 | res.body must beEqualTo("i am available") 24 | } 25 | 26 | "respond with '503 Service Unavailable' when resource doesn't respond to ping" >> { 27 | trait TestResource extends Resource { 28 | override def ping(request: Request, response: Response) = false 29 | } 30 | val req = new Request(httpGetRequest) 31 | val res = Response(new MockHttpServletResponse) 32 | dispatch(new Resource with TestResource, req, res) 33 | res.status must beEqualTo("503") 34 | res.body must beEqualTo("") 35 | } 36 | 37 | "respond with '503 Service Unavailable' when resource is not available" >> { 38 | trait TestResource extends Resource { 39 | override def service_available(request: Request, response: Response) = false 40 | } 41 | val req = new Request(httpGetRequest) 42 | val res = Response(new MockHttpServletResponse) 43 | dispatch(new Resource with TestResource, req, res) 44 | res.status must beEqualTo("503") 45 | res.body must beEqualTo("") 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/b03Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class b03SpecTest extends JUnit4(b03Spec) 9 | 10 | 11 | object b03Spec extends Specification { 12 | trait TestResource extends Resource { 13 | override def allowed_methods(request: Request, response: Response) = List("GET", "OPTIONS") 14 | override def options(request: Request, response: Response) = List(("X-mas", "happy holiday")) 15 | override def to_html(request: Request, response: Response) = hello world.toString 16 | } 17 | 18 | "webmachine" should { 19 | "respond with 200 when GET method is used" >> { 20 | val req = Request(httpGetRequest) 21 | val res = Response(new MockHttpServletResponse) 22 | dispatch(new Resource with TestResource, req, res) 23 | res.status must beEqualTo("200") 24 | res.body must beEqualTo("hello world") 25 | res.headers must notHaveKey("X-mas") 26 | } 27 | 28 | "have headers when method is OPTIONS" >> { 29 | val req = Request(httpOptionsRequest) 30 | val res = Response(new MockHttpServletResponse) 31 | dispatch(new Resource with TestResource, req, res) 32 | res.status must beEqualTo("200") 33 | res.body must beEqualTo("") 34 | res.headers must havePair(("X-mas", "happy holiday")) 35 | } 36 | 37 | "have non unicode body" >> { 38 | trait NonUnicode extends Resource { 39 | override def to_html(request: Request, response: Response) = { 40 | response.charset = None 41 | "hi" 42 | } 43 | } 44 | val req = Request(httpGetRequest) 45 | val res = Response(new MockHttpServletResponse) 46 | dispatch(new Resource with TestResource with NonUnicode, req, res) 47 | res.status must beEqualTo("200") 48 | res.body must beEqualTo("hi") 49 | } 50 | 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/c04Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class c04SpecTest extends JUnit4(c04Spec) 9 | 10 | object c04Spec extends Specification { 11 | 12 | trait TestResource extends Resource { 13 | override def content_types_provided(request: Request, response: Response) = { 14 | List(("application/json", to_json _), ("text/xml", to_xml _)) 15 | } 16 | 17 | def to_json(request: Request, response: Response) = "{'name' : 'Nilanjan'}" 18 | def to_xml(request: Request, response: Response) = Nilanjan.toString 19 | } 20 | 21 | "webmachine" should { 22 | "respond with first content type specified when no accept provided" >> { 23 | val req = new Request(httpGetRequest) 24 | val res = Response(new MockHttpServletResponse) 25 | dispatch(new Resource with TestResource, req, res) 26 | res.status must beEqualTo("200") 27 | res.body must beEqualTo("{'name' : 'Nilanjan'}") 28 | } 29 | 30 | "respond with content type matching the provided accept" >> { 31 | val base = httpGetRequest 32 | base.addHeader("accept", "text/xml") 33 | val req = new Request(base) 34 | val res = Response(new MockHttpServletResponse) 35 | dispatch(new Resource with TestResource, req, res) 36 | res.status must beEqualTo("200") 37 | res.body must beEqualTo("Nilanjan") 38 | } 39 | 40 | "respond with '406 Not Acceptable' when no content type is acceptable" >> { 41 | val base = httpGetRequest 42 | base.addHeader("accept", "image/jpeg") 43 | val req = new Request(base) 44 | val res = Response(new MockHttpServletResponse) 45 | dispatch(new Resource with TestResource, req, res) 46 | res.status must beEqualTo("406") 47 | res.body must beEqualTo("") 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/g07Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class g07SpecTest extends JUnit4(g07Spec) 9 | 10 | object g07Spec extends Specification { 11 | trait TestResource extends Resource { 12 | override def charsets_provided(request: Request, response: Response) = Some(List("utf-8", "iso-8859-1")) 13 | override def encodings_provided(request: Request, response: Response) = { 14 | Some(List( 15 | ("reverse", (x : String) => x.reverse), 16 | ("identity", (x : String) => x) 17 | )) 18 | } 19 | override def languages_provided(request: Request, response: Response) = Some(List("en", "es")) 20 | override def resource_exists(request: Request, response: Response) = true 21 | override def variances(request: Request, response: Response) = List("Cookie") 22 | override def content_types_provided(request: Request, response: Response) = { 23 | List(("application/json", to_json _), ("text/xml", to_xml _)) 24 | } 25 | 26 | def to_json(request: Request, response: Response) = "{'name' : 'Nilanjan'}" 27 | def to_xml(request: Request, response: Response) = Nilanjan.toString 28 | } 29 | 30 | "webmachine" should { 31 | "respond with variances" >> { 32 | val req = new Request(httpGetRequest) 33 | val res = Response(new MockHttpServletResponse) 34 | dispatch(new Resource with TestResource, req, res) 35 | res.status must beEqualTo("200") 36 | res.headers("Vary") must beEqualTo("Accept, Accept-Charset, Accept-Encoding, Accept-Language, Cookie") 37 | } 38 | 39 | "respond with '404 Not Found' when resource doesn't exists" >> { 40 | trait NoResource extends Resource { 41 | override def resource_exists(request: Request, response: Response) = false 42 | } 43 | val req = new Request(httpGetRequest) 44 | val res = Response(new MockHttpServletResponse) 45 | dispatch(new Resource with TestResource with NoResource, req, res) 46 | res.status must beEqualTo("404") 47 | res.body must beEqualTo("") 48 | } 49 | 50 | } 51 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/b05Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class b05SpecTest extends JUnit4(b05Spec) 9 | 10 | 11 | object b05Spec extends Specification { 12 | trait TestResource extends Resource { 13 | override def known_content_type(request: Request, response: Response) = { 14 | List("text/plain", "text/html").exists { _ == request.content_type.getOrElse("").split(';')(0) } 15 | } 16 | override def to_html(request: Request, response: Response) = "matching content type found" 17 | } 18 | 19 | "webmachine" should { 20 | "respond with 200 when matching content type found" >> { 21 | val httpRequest = httpGetRequest 22 | httpRequest.setContentType("text/html") 23 | val req = Request(httpRequest) 24 | val res = Response(new MockHttpServletResponse) 25 | dispatch(new Resource with TestResource, req, res) 26 | res.status must beEqualTo("200") 27 | res.body must beEqualTo("matching content type found") 28 | res.content_type must beEqualTo("text/html") 29 | } 30 | 31 | "respond with text content when text/plain content type requested" >> { 32 | trait PlainResource extends Resource { 33 | override def content_types_provided(req: Request, res: Response) = List(("text/plain", to_text _)) 34 | def to_text(request: Request, response: Response) = "just a plain text" 35 | } 36 | 37 | val httpRequest = httpGetRequest 38 | httpRequest.setContentType("text/plain") 39 | val req = Request(httpRequest) 40 | val res = Response(new MockHttpServletResponse) 41 | dispatch(new Resource with TestResource with PlainResource, req, res) 42 | res.status must beEqualTo("200") 43 | res.body must beEqualTo("just a plain text") 44 | res.content_type must beEqualTo("text/plain") 45 | } 46 | 47 | "respond with '415 Unsupported Media Type' when no match" >> { 48 | val httpRequest = httpGetRequest 49 | httpRequest.setContentType("application/json; charset=utf-8") 50 | val req = Request(httpRequest) 51 | val res = Response(new MockHttpServletResponse) 52 | dispatch(new Resource with TestResource, req, res) 53 | res.status must beEqualTo("415") 54 | res.body must beEqualTo("") 55 | } 56 | 57 | } 58 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/e06Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class e06SpecTest extends JUnit4(e06Spec) 9 | 10 | object e06Spec extends Specification { 11 | var supported_charsets: Option[List[String]] = _ 12 | trait TestResource extends Resource { 13 | override def charsets_provided(request: Request, response: Response) = { 14 | supported_charsets 15 | } 16 | override def to_html(request: Request, response: Response) = { 17 | response.charset match { 18 | case Some("UTF-8") => "unicode" 19 | case _ => "ascii" 20 | } 21 | } 22 | } 23 | 24 | "webmachine" should { 25 | "respond with to_html when resource don't provide supported charset" >> { 26 | supported_charsets = None 27 | val base = httpGetRequest 28 | base.addHeader("accept-charset", "iso-8859-1") 29 | val req = new Request(base) 30 | val res = Response(new MockHttpServletResponse) 31 | dispatch(new Resource with TestResource, req, res) 32 | res.status must beEqualTo("200") 33 | res.charset must beEqualTo(None) 34 | res.body must beEqualTo("ascii") 35 | } 36 | 37 | "respond with '406 Not Acceptable' when requested charset not supported" >> { 38 | supported_charsets = Some(List("UTF-8")) 39 | val base = httpGetRequest 40 | base.addHeader("accept-charset", "latin-1") 41 | val req = new Request(base) 42 | val res = Response(new MockHttpServletResponse) 43 | dispatch(new Resource with TestResource, req, res) 44 | res.status must beEqualTo("406") 45 | res.body must beEqualTo("") 46 | } 47 | 48 | "respond with UTF-8 content when resource support the charset" >> { 49 | supported_charsets = Some(List("UTF-8", "iso-8859-1")) 50 | val base = httpGetRequest 51 | base.addHeader("accept-charset", "UTF-8") 52 | val req = new Request(base) 53 | val res = Response(new MockHttpServletResponse) 54 | dispatch(new Resource with TestResource, req, res) 55 | res.status must beEqualTo("200") 56 | res.charset must beEqualTo(Some("UTF-8")) 57 | res.body must beEqualTo("unicode") 58 | } 59 | 60 | "respond with 'iso-8859-1' content when resource support the charset" >> { 61 | supported_charsets = Some(List("UTF-8", "iso-8859-1")) 62 | val base = httpGetRequest 63 | base.addHeader("accept-charset", "iso-8859-1") 64 | val req = new Request(base) 65 | val res = Response(new MockHttpServletResponse) 66 | dispatch(new Resource with TestResource, req, res) 67 | res.status must beEqualTo("200") 68 | res.charset must beEqualTo(Some("iso-8859-1")) 69 | res.body must beEqualTo("ascii") 70 | } 71 | 72 | } 73 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/d05Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class d05SpecTest extends JUnit4(d05Spec) 9 | 10 | object d05Spec extends Specification { 11 | var supported_langs: Option[List[String]] = _ 12 | trait TestResource extends Resource { 13 | override def languages_provided(request: Request, response: Response) = { 14 | supported_langs 15 | } 16 | override def to_html(request: Request, response: Response) = { 17 | response.content_language match { 18 | case Some("es") => hola.toString 19 | case _ => Hello.toString 20 | } 21 | } 22 | } 23 | 24 | "webmachine" should { 25 | "respond with to_html when resource don't provide languages" >> { 26 | supported_langs = None 27 | val base = httpGetRequest 28 | base.addHeader("accept-language", "en;q=03, es") 29 | val req = new Request(base) 30 | val res = Response(new MockHttpServletResponse) 31 | dispatch(new Resource with TestResource, req, res) 32 | res.status must beEqualTo("200") 33 | res.content_language must beEqualTo(None) 34 | res.body must beEqualTo("Hello") 35 | } 36 | 37 | "respond with '406 Not Acceptable' when requested language not supported" >> { 38 | supported_langs = Some(List("en", "es")) 39 | val base = httpGetRequest 40 | base.addHeader("accept-language", "jp") 41 | val req = new Request(base) 42 | val res = Response(new MockHttpServletResponse) 43 | dispatch(new Resource with TestResource, req, res) 44 | res.status must beEqualTo("406") 45 | res.body must beEqualTo("") 46 | } 47 | 48 | "respond with en content when requested language is supported" >> { 49 | supported_langs = Some(List("en", "es")) 50 | val base = httpGetRequest 51 | base.addHeader("accept-language", "en") 52 | val req = new Request(base) 53 | val res = Response(new MockHttpServletResponse) 54 | dispatch(new Resource with TestResource, req, res) 55 | res.status must beEqualTo("200") 56 | res.content_language must beEqualTo(Some("en")) 57 | res.body must beEqualTo("Hello") 58 | } 59 | 60 | "respond with es content when requested language is supported" >> { 61 | supported_langs = Some(List("en", "es")) 62 | val base = httpGetRequest 63 | base.addHeader("accept-language", "es") 64 | val req = new Request(base) 65 | val res = Response(new MockHttpServletResponse) 66 | dispatch(new Resource with TestResource, req, res) 67 | res.status must beEqualTo("200") 68 | res.content_language must beEqualTo(Some("es")) 69 | res.body must beEqualTo("hola") 70 | } 71 | 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/scala/webmachine/Resource.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | 3 | class Resource { 4 | def to_html(request: Request, response: Response) = { "Individual Resource should override this default to_html method " } 5 | 6 | def content_types_provided(req: Request, res: Response) = List(("text/html", to_html _)) 7 | 8 | def allowed_methods(req: Request, res: Response) = List("GET", "HEAD") 9 | 10 | def allow_missing_post(req: Request, res: Response) = false 11 | 12 | def auth_required(req: Request, res: Response) = true 13 | 14 | def charsets_provided(req: Request, res: Response):Option[List[String]] = None 15 | 16 | def content_types_accepted(req: Request, res: Response) = None 17 | 18 | def created_location(req: Request, res: Response): Option[String] = None 19 | 20 | def delete_completed(req: Request, res: Response) = true 21 | 22 | def delete_resource(req: Request, res: Response) = false 23 | 24 | def encodings_provided(req: Request, res: Response):Option[List[(String, (String) => String)]] = None 25 | 26 | def expires(req: Request, res: Response): Option[Long] = None 27 | 28 | def finish_request(req: Request, res: Response) = true 29 | 30 | def forbidden(req: Request, res: Response) = false 31 | 32 | def generate_etag(req: Request, res: Response): Option[String] = None 33 | 34 | def is_authorized(req: Request, res: Response): Any = true 35 | 36 | def is_conflict(req: Request, res: Response) = false 37 | 38 | def known_content_type(req: Request, res: Response) = true 39 | 40 | def known_methods(req: Request, res: Response) = List("GET", "HEAD", "POST", "PUT", "DELETE", 41 | "TRACE", "CONNECT", "OPTIONS") 42 | 43 | def languages_provided(req: Request, res: Response):Option[List[String]] = None 44 | 45 | def last_modified(req: Request, res: Response):Option[Long] = None 46 | 47 | def malformed_request(req: Request, res: Response) = false 48 | 49 | def moved_permanently(req: Request, res: Response):Option[String] = None 50 | 51 | def moved_temporarily(req: Request, res: Response): Option[String] = None 52 | 53 | def multiple_choices(req: Request, res: Response) = false 54 | 55 | def options(req: Request, res: Response):List[(String, String)] = Nil 56 | 57 | def ping(req: Request, res: Response) = true 58 | 59 | def post_is_create(req: Request, res: Response) = false 60 | 61 | def previously_existed(req: Request, res: Response) = false 62 | 63 | def process_post(req: Request, res: Response) = false 64 | 65 | def resource_exists(req: Request, res: Response) = true 66 | 67 | def service_available(req: Request, res: Response) = true 68 | 69 | def uri_too_long(req: Request, res: Response)= false 70 | 71 | def valid_content_headers(req: Request, res: Response)= true 72 | 73 | def valid_entity_length(req: Request, res: Response) = true 74 | 75 | def variances(req: Request, res: Response): List[String] = Nil 76 | } -------------------------------------------------------------------------------- /src/test/scala/webmachine/f07Spec.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | import org.specs._ 3 | import org.specs.runner.JUnit4 4 | import org.springframework.mock.web._ 5 | import TestHelper._ 6 | import Dispatcher._ 7 | 8 | class f07SpecTest extends JUnit4(f07Spec) 9 | 10 | object f07Spec extends Specification { 11 | var supported_encodings:Option[List[(String, (String) => String)]] = _ 12 | trait TestResource extends Resource { 13 | override def encodings_provided(request: Request, response: Response) = { 14 | supported_encodings 15 | } 16 | override def to_html(request: Request, response: Response) = "repl" 17 | } 18 | 19 | "webmachine" should { 20 | "respond with to_html when resource don't provide supported encodings" >> { 21 | supported_encodings = None 22 | val base = httpGetRequest 23 | base.addHeader("accept-encoding", "reverse") 24 | val req = new Request(base) 25 | val res = Response(new MockHttpServletResponse) 26 | dispatch(new Resource with TestResource, req, res) 27 | res.status must beEqualTo("200") 28 | res.content_encoding must beEqualTo(None) 29 | res.body must beEqualTo("repl") 30 | } 31 | 32 | "respond with '406 Not Acceptable' when requested encoding not supported" >> { 33 | supported_encodings = Some(List(("reverse", (x : String) => x.reverse))) 34 | val base = httpGetRequest 35 | base.addHeader("accept-encoding", "gzip") 36 | val req = new Request(base) 37 | val res = Response(new MockHttpServletResponse) 38 | dispatch(new Resource with TestResource, req, res) 39 | res.status must beEqualTo("406") 40 | res.body must beEqualTo("") 41 | } 42 | 43 | "respond with reverse encoded content when requested reverse encoding is supported" >> { 44 | supported_encodings = Some(List(("reverse", (x : String) => x.reverse))) 45 | val base = httpGetRequest 46 | base.addHeader("accept-encoding", "reverse") 47 | val req = new Request(base) 48 | val res = Response(new MockHttpServletResponse) 49 | dispatch(new Resource with TestResource, req, res) 50 | res.status must beEqualTo("200") 51 | res.content_encoding must beEqualTo(Some("reverse")) 52 | res.body must beEqualTo("lper") 53 | } 54 | 55 | "respond with identity content when requested identity encoding is supported" >> { 56 | supported_encodings = Some(List( 57 | ("reverse", (x : String) => x.reverse), 58 | ("identity", (x : String) => x) 59 | )) 60 | val base = httpGetRequest 61 | base.addHeader("accept-encoding", "identity") 62 | val req = new Request(base) 63 | val res = Response(new MockHttpServletResponse) 64 | dispatch(new Resource with TestResource, req, res) 65 | res.status must beEqualTo("200") 66 | res.content_encoding must beEqualTo(Some("identity")) 67 | res.body must beEqualTo("repl") 68 | } 69 | 70 | } 71 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | webmachine 5 | scala_webmachine 6 | 1.0-SNAPSHOT 7 | 2008 8 | 9 | 2.7.7 10 | 11 | 12 | 13 | 14 | scala-tools.org 15 | Scala-Tools Maven2 Repository 16 | http://scala-tools.org/repo-releases 17 | 18 | 19 | 20 | 21 | 22 | scala-tools.org 23 | Scala-Tools Maven2 Repository 24 | http://scala-tools.org/repo-releases 25 | 26 | 27 | 28 | 29 | 30 | org.scala-lang 31 | scala-library 32 | ${scala.version} 33 | 34 | 35 | org.scala-tools.testing 36 | specs 37 | 1.6.1 38 | test 39 | 40 | 41 | junit 42 | junit 43 | 4.4 44 | test 45 | 46 | 47 | org.mortbay.jetty 48 | jetty 49 | 6.1.21 50 | 51 | 52 | org.springframework 53 | spring-mock 54 | 2.0.8 55 | test 56 | 57 | 58 | org.springframework 59 | spring-core 60 | 2.5.6 61 | test 62 | 63 | 64 | 65 | 66 | src/main/scala 67 | src/test/scala 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-surefire-plugin 72 | 73 | 74 | **/TestHelper*.* 75 | 76 | 77 | 78 | 79 | org.codehaus.mojo 80 | exec-maven-plugin 81 | 1.1 82 | 83 | 84 | 85 | java 86 | 87 | 88 | 89 | 90 | 91 | org.scala-tools 92 | maven-scala-plugin 93 | 94 | 95 | 96 | compile 97 | testCompile 98 | 99 | 100 | 101 | 102 | ${scala.version} 103 | 104 | 105 | 106 | org.apache.maven.plugins 107 | maven-eclipse-plugin 108 | 109 | true 110 | 111 | ch.epfl.lamp.sdt.core.scalabuilder 112 | 113 | 114 | ch.epfl.lamp.sdt.core.scalanature 115 | 116 | 117 | org.eclipse.jdt.launching.JRE_CONTAINER 118 | ch.epfl.lamp.sdt.launching.SCALA_CONTAINER 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | org.scala-tools 128 | maven-scala-plugin 129 | 130 | ${scala.version} 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /src/main/scala/webmachine/DecisionCore.scala: -------------------------------------------------------------------------------- 1 | package webmachine 2 | 3 | object DecisionCore { 4 | def b03(resource: Resource, req: Request, res:Response) = { 5 | "Options?" 6 | req.method match { 7 | case "OPTIONS" => { 8 | resource.options(req, res).foreach { 9 | res.headers += _ 10 | } 11 | 200 12 | } 13 | case _ => c03(resource, req, res) 14 | } 15 | } 16 | 17 | def b04(resource: Resource, req: Request, res:Response) = { 18 | "Request entity too large?" 19 | resource.valid_entity_length(req, res) match { 20 | case true => b03(resource, req, res) 21 | case false => "413" 22 | } 23 | } 24 | 25 | def b05(resource: Resource, req: Request, res:Response) = { 26 | "Unknown Content-Type?" 27 | resource.known_content_type(req, res) match { 28 | case true => b04(resource, req, res) 29 | case false => "415" 30 | } 31 | } 32 | 33 | def b06(resource: Resource, req: Request, res:Response) = { 34 | "Unknown or unsupported Content-* header?" 35 | resource.valid_content_headers(req, res) match { 36 | case true => b05(resource, req, res) 37 | case _ => "501" 38 | } 39 | } 40 | 41 | def b07(resource: Resource, req: Request, res:Response) = { 42 | "Forbidden?" 43 | resource.forbidden(req, res) match { 44 | case true => "403" 45 | case _ => b06(resource, req, res) 46 | } 47 | 48 | } 49 | 50 | def b08(resource: Resource, req: Request, res:Response) = { 51 | "Authorized?" 52 | resource.is_authorized(req, res) match { 53 | case result: boolean => result match { 54 | case true => b07(resource, req, res) 55 | case false => "401" 56 | } 57 | case msg: String => res.headers("WWW-Authenticate") = msg; 401 58 | } 59 | } 60 | 61 | def b09(resource: Resource, req: Request, res:Response) = { 62 | "Malformed?" 63 | resource.malformed_request(req, res) match { 64 | case true => "400" 65 | case _ => b08(resource, req, res) 66 | } 67 | 68 | } 69 | 70 | def b10(resource: Resource, req: Request, res:Response) = { 71 | "Is method allowed?" 72 | resource.allowed_methods(req, res) exists { req.method == _ } match { 73 | case true => b09(resource, req, res) 74 | case _ => res.allowed = resource.allowed_methods(req, res); "405" 75 | } 76 | } 77 | 78 | def b11(resource: Resource, req: Request, res:Response) = { 79 | "URI too long?" 80 | resource.uri_too_long(req, res) match { 81 | case true => "414" 82 | case _ => b10(resource, req, res) 83 | } 84 | 85 | } 86 | 87 | def b12(resource: Resource, req: Request, res:Response) = { 88 | "Known method?" 89 | resource.known_methods(req, res) exists {req.method == _ } match { 90 | case true => b11(resource, req, res) 91 | case _ => "501" 92 | } 93 | } 94 | def b13(resource: Resource, req: Request, res:Response) = { 95 | "Service available" 96 | resource.ping(req, res) && resource.service_available(req, res) match { 97 | case true => b12(resource, req, res) 98 | case _ => "503" 99 | } 100 | } 101 | 102 | def c03(resource: Resource, req: Request, res:Response) = { 103 | "Accept exists?" 104 | req.hasHeader("accept") match { 105 | case true => c04(resource, req, res) 106 | case false => d04(resource, req, res) 107 | } 108 | } 109 | 110 | def c04(resource: Resource, req: Request, res:Response) = { 111 | "Acceptable media type available?" 112 | val content_types = resource.content_types_provided(req, res) map { _._1 } 113 | req.accept_best_match(content_types) match { 114 | case Some(content_type) => { 115 | res.content_type = content_type 116 | d04(resource, req, res) 117 | } 118 | case None => "406" 119 | } 120 | } 121 | 122 | def d04(resource: Resource, req: Request, res:Response) = { 123 | "Accept-Language exists?" 124 | req.hasHeader("accept-language") match { 125 | case true => d05(resource, req, res) 126 | case false => e05(resource, req, res) 127 | } 128 | } 129 | 130 | def d05(resource: Resource, req: Request, res:Response) = { 131 | "Accept-Language available?" 132 | resource.languages_provided(req, res) match { 133 | case Some(langs) => req.accept_language_best_match(langs) match { 134 | case Some(matched_lang) => { 135 | res.content_language = Some(matched_lang) 136 | e05(resource, req, res) 137 | } 138 | case None => "406" 139 | } 140 | case None => e05(resource, req, res) 141 | } 142 | } 143 | 144 | def e05(resource: Resource, req: Request, res:Response) = { 145 | "Accept-Charset exists?" 146 | req.hasHeader("accept-charset") match { 147 | case true => e06(resource, req, res) 148 | case false => f06(resource, req, res) 149 | } 150 | } 151 | 152 | def e06(resource: Resource, req: Request, res:Response) = { 153 | "Acceptable charset available?" 154 | resource.charsets_provided(req, res) match { 155 | case Some(charsets) => req.accept_charset_best_match(charsets) match { 156 | case Some(matched_charset) => { 157 | res.charset = Some(matched_charset) 158 | f06(resource, req, res) 159 | } 160 | case None => "406" 161 | } 162 | case None => f06(resource, req, res) 163 | } 164 | } 165 | 166 | def f06(resource: Resource, req: Request, res:Response) = { 167 | "Accept-Encoding exists?" 168 | req.hasHeader("accept-encoding") match { 169 | case true => f07(resource, req, res) 170 | case false => g07(resource, req, res) 171 | } 172 | } 173 | 174 | def f07(resource: Resource, req: Request, res:Response) = { 175 | "Acceptable encoding available?" 176 | resource.encodings_provided(req, res) match { 177 | case Some(encoding_mapping) => req.accept_encoding_back_match(encoding_mapping map {_._1}) match { 178 | case Some(matched_encoding) => { 179 | res.content_encoding = Some(matched_encoding) 180 | g07(resource, req, res) 181 | } 182 | case None => "406" 183 | } 184 | case None => g07(resource, req, res) 185 | } 186 | } 187 | 188 | def g07(resource: Resource, req: Request, res:Response) = { 189 | "Resource exists?" 190 | // Set variances now that conneg is done 191 | val variances = accept_maybe(resource, req, res) ++ 192 | accept_charset_maybe(resource, req, res) ++ 193 | accept_encoding_maybe(resource, req, res) ++ 194 | accept_languages_maybe(resource, req, res) ++ 195 | resource.variances(req, res) 196 | res.headers = res.headers + ("Vary" -> variances.mkString(", ")) 197 | resource.resource_exists(req, res) match { 198 | case true => g08(resource, req, res) 199 | case false => h07(resource, req, res) 200 | } 201 | } 202 | 203 | def g08(resource: Resource, req: Request, res:Response) = { 204 | "If-Match exists?" 205 | req.hasHeader("if-match") match { 206 | case true => g09(resource, req, res) 207 | case false => h10(resource, req, res) 208 | } 209 | } 210 | 211 | def g09(resource: Resource, req: Request, res:Response) = { 212 | "If-Match: * exists?" 213 | req.header("if-match") match { 214 | case "*" => h10(resource, req, res) 215 | case _ => g11(resource, req, res) 216 | } 217 | } 218 | 219 | def g11(resource: Resource, req: Request, res:Response) = { 220 | "Etag in If-Match?" 221 | resource.generate_etag(req, res) match { 222 | case Some(eTag) => eTag == req.header("if-match") match { 223 | case true => h10(resource, req, res) 224 | case false => "412" 225 | } 226 | case None => "412" 227 | } 228 | } 229 | 230 | def h07(resource: Resource, req: Request, res:Response) = { 231 | "If-Match: * exists?" 232 | req.header("if-match") match { 233 | case "*" => "412" 234 | case _ => i07(resource, req, res) 235 | } 236 | } 237 | 238 | def h10(resource: Resource, req: Request, res:Response) = { 239 | "If-Unmodified-Since exists?" 240 | req.hasHeader("if-unmodified-since") match { 241 | case true => h11(resource, req, res) 242 | case false => i12(resource, req, res) 243 | } 244 | } 245 | 246 | def h11(resource: Resource, req: Request, res:Response) = { 247 | "If-Unmodified-Since is a valid date?" 248 | req.if_unmodified_since match { 249 | case -1 => i12(resource, req, res) 250 | case _ => h12(resource, req, res) 251 | } 252 | } 253 | 254 | def h12(resource: Resource, req: Request, res:Response) = { 255 | "Last-Modified > If-Unmodified-Since?" 256 | res.last_modified = resource.last_modified(req, res) 257 | res.last_modified match { 258 | case Some(date) => { 259 | if(date > req.if_unmodified_since) "412" 260 | else i12(resource, req, res) 261 | } 262 | case None => i12(resource, req, res) 263 | } 264 | } 265 | 266 | def i04(resource: Resource, req: Request, res:Response) = { 267 | "Apply to a different URI?" 268 | res.location = resource.moved_permanently(req, res) 269 | res.location match { 270 | case Some(uri) => "301" 271 | case None => p03(resource, req, res) 272 | } 273 | } 274 | 275 | def i07(resource: Resource, req: Request, res:Response) = { 276 | "PUT?" 277 | req.method match { 278 | case "PUT" => i04(resource, req, res) 279 | case _ => k07(resource, req, res) 280 | } 281 | } 282 | 283 | def i12(resource: Resource, req: Request, res:Response) = { 284 | "If-None-Match exists?" 285 | req.hasHeader("if-none-match") match { 286 | case true => i13(resource, req, res) 287 | case false => l13(resource, req, res) 288 | } 289 | } 290 | 291 | def i13(resource: Resource, req: Request, res:Response) = { 292 | "If-None-Match: * exists?" 293 | req.header("if-none-match") match { 294 | case "*" => j18(resource, req, res) 295 | case _ => k13(resource, req, res) 296 | } 297 | } 298 | 299 | def j18(resource: Resource, req: Request, res:Response) = { 300 | "GET/HEAD?" 301 | req.method match { 302 | case "GET" | "HEAD"=> "304" 303 | case _ => "412" 304 | } 305 | } 306 | 307 | def k05(resource: Resource, req: Request, res:Response) = { 308 | "Resource moved permanently?" 309 | res.location = resource.moved_permanently(req, res) 310 | res.location match { 311 | case Some(uri) => "301" 312 | case None => l05(resource, req, res) 313 | } 314 | } 315 | 316 | def k07(resource: Resource, req: Request, res:Response) = { 317 | "Resource previously existed?" 318 | resource.previously_existed(req, res) match { 319 | case true => k05(resource, req, res) 320 | case false => l07(resource, req, res) 321 | } 322 | } 323 | 324 | def k13(resource: Resource, req: Request, res:Response) = { 325 | "Etag in If-None-Match?" 326 | res.etag = resource.generate_etag(req, res) 327 | res.etag match { 328 | case Some(eTag) => eTag == req.header("if-none-match") match { 329 | case true => j18(resource, req, res) 330 | case false => l13(resource, req, res) 331 | } 332 | case None => l13(resource, req, res) 333 | } 334 | } 335 | 336 | def l05(resource: Resource, req: Request, res:Response) = { 337 | "Resource moved temporarily?" 338 | res.location = resource.moved_temporarily(req, res) 339 | res.location match { 340 | case Some(uri) => "307" 341 | case None => m05(resource, req, res) 342 | } 343 | } 344 | 345 | def l07(resource: Resource, req: Request, res:Response) = { 346 | "POST?" 347 | req.method match { 348 | case "POST" => m07(resource, req, res) 349 | case _ => "404" 350 | } 351 | } 352 | 353 | def l13(resource: Resource, req: Request, res:Response) = { 354 | "If-Modified-Since exists?" 355 | req.hasHeader("if-modified-since") match { 356 | case true => l14(resource, req, res) 357 | case false => m16(resource, req, res) 358 | } 359 | } 360 | 361 | def l14(resource: Resource, req: Request, res:Response) = { 362 | "If-Modified-Since is a valid date?" 363 | req.if_modified_since match { 364 | case -1 => m16(resource, req, res) 365 | case _ => l15(resource, req, res) 366 | } 367 | } 368 | 369 | def l15(resource: Resource, req: Request, res:Response) = { 370 | "If-Modified-Since > Now?" 371 | val now = System.currentTimeMillis() 372 | if(req.if_modified_since > now) m16(resource, req, res) 373 | else l17(resource, req, res) 374 | } 375 | 376 | def l17(resource: Resource, req: Request, res:Response) = { 377 | "Last-Modified > If-Modified-Since?" 378 | res.last_modified = resource.last_modified(req, res) 379 | res.last_modified match { 380 | case Some(date) => { 381 | if(date > req.if_modified_since) m16(resource, req, res) 382 | else "304" 383 | } 384 | case None => "304" 385 | } 386 | } 387 | 388 | def m05(resource: Resource, req: Request, res:Response) = { 389 | "POST?" 390 | req.method match { 391 | case "POST" => n05(resource, req, res) 392 | case _ => "410" 393 | } 394 | } 395 | 396 | 397 | def m07(resource: Resource, req: Request, res:Response) = { 398 | "Server permits POST to missing resource?" 399 | resource.allow_missing_post(req, res) match { 400 | case true => n11(resource, req, res) 401 | case false => "404" 402 | } 403 | } 404 | 405 | def m16(resource: Resource, req: Request, res:Response) = { 406 | "DELETE?" 407 | req.method match { 408 | case "DELETE" => m20(resource, req, res) 409 | case _ => n16(resource, req, res) 410 | } 411 | } 412 | 413 | def m20(resource: Resource, req: Request, res:Response) = { 414 | "Delete enacted?" 415 | resource.delete_completed(req, res) match { 416 | case true => o20(resource, req, res) 417 | case false => "202" 418 | } 419 | } 420 | 421 | def n05(resource: Resource, req: Request, res:Response) = { 422 | "Server permits POST to missing resource?" 423 | resource.allow_missing_post(req, res) match { 424 | case true => n11(resource, req, res) 425 | case false => "410" 426 | } 427 | } 428 | 429 | def n11(resource: Resource, req: Request, res:Response) = { 430 | "Redirect?" 431 | resource.post_is_create(req, res) match { 432 | case true => { 433 | handle_request_body(resource, req, res) 434 | res.location = resource.created_location(req, res) 435 | res.location match { 436 | case Some(uri) => "303" 437 | case None => p11(resource, req, res) 438 | } 439 | } 440 | case false => { 441 | if(!resource.process_post(req, res)) throw new RuntimeException("Failed to process POST") 442 | p11(resource, req, res) 443 | } 444 | } 445 | } 446 | 447 | def n16(resource: Resource, req: Request, res:Response) = { 448 | "POST?" 449 | req.method match { 450 | case "POST" => n11(resource, req, res) 451 | case _ => o16(resource, req, res) 452 | } 453 | } 454 | 455 | def o14(resource: Resource, req: Request, res:Response) = { 456 | "Is conflict?" 457 | resource.is_conflict(req, res) match { 458 | case true => "409" 459 | case false => p11(resource, req, res) 460 | } 461 | } 462 | 463 | def o16(resource: Resource, req: Request, res:Response) = { 464 | "PUT?" 465 | req.method match { 466 | case "PUT" => o14(resource, req, res) 467 | case _ => o18(resource, req, res) 468 | } 469 | } 470 | 471 | 472 | def o18(resource: Resource, req: Request, res:Response) = { 473 | "Multiple representations? (Build GET/HEAD body)" 474 | req.method match { 475 | case "GET" | "HEAD" => { 476 | handle_response_body(resource, req, res) 477 | if(resource.multiple_choices(req, res)) "300" 478 | else "200" 479 | } 480 | case _ => { 481 | if(resource.multiple_choices(req, res)) "300" 482 | else "200" 483 | } 484 | } 485 | } 486 | 487 | def o20(resource: Resource, req: Request, res:Response) = { 488 | "Response includes entity?" 489 | res.body match { 490 | case "" => "204" 491 | case _ => o18(resource, req, res) 492 | } 493 | 494 | } 495 | def p03(resource: Resource, req: Request, res:Response) = { 496 | "Conflict?" 497 | resource.is_conflict(req, res) match { 498 | case true => "409" 499 | case false => { 500 | handle_request_body(resource, req, res) 501 | p11(resource, req, res) 502 | } 503 | } 504 | } 505 | 506 | def p11(resource: Resource, req: Request, res:Response) = { 507 | "New resource?" 508 | res.location match { 509 | case Some(uri) => "201" 510 | case None => o20(resource, req, res) 511 | } 512 | } 513 | 514 | def handle_request(resource: Resource, req: Request, res:Response) { 515 | val content_types = resource.content_types_provided(req, res) 516 | if(content_types.size > 0) { 517 | res.content_type = content_types.head._1 518 | } 519 | res.status = b13(resource, req, res).toString 520 | } 521 | 522 | private def handle_request_body(resource: Resource, req: Request, res: Response) = { 523 | val content_type = req.content_type match { 524 | case Some(ctype) => ctype.split(";", 1)(0) 525 | case None => "application/octet-stream" 526 | } 527 | resource.content_types_provided(req, res).filter { _._1 == content_type } match { 528 | case Nil => throw new RuntimeException("Resource does not support the requested content type") 529 | case List(matching_content_mapping) => matching_content_mapping._2(req, res) 530 | } 531 | } 532 | 533 | private def handle_response_body(resource: Resource, req: Request, res: Response) = { 534 | res.etag = resource.generate_etag(req, res) 535 | res.last_modified = resource.last_modified(req, res) 536 | res.expires = resource.expires(req, res) 537 | 538 | val body = resource.content_types_provided(req, res).find { _._1 == res.content_type } match { 539 | case None => throw new RuntimeException("Resource does not support the requested content type") 540 | case Some(matching_content_mapping) => matching_content_mapping._2(req, res) 541 | } 542 | res.charset match { 543 | case Some(charset) => res.body = unicode(body) 544 | case None => res.body = body 545 | } 546 | 547 | if(res.content_encoding.isDefined) { 548 | resource.encodings_provided(req, res) match { 549 | case Some(encoding_mappings) => { 550 | encoding_mappings.find { _._1 == res.content_encoding.get } match { 551 | case None => throw new RuntimeException("Resource does not support the requested encoding") 552 | case Some(matching_encoding_mapping) => res.body = matching_encoding_mapping._2(res.body) 553 | } 554 | } 555 | case None => throw new RuntimeException("Resource does not support the requested encoding") 556 | } 557 | } 558 | } 559 | 560 | private def unicode(s: String) = { 561 | new String(s.getBytes("UTF-8")) 562 | } 563 | 564 | private def accept_maybe(resource:Resource, req: Request, res:Response) = { 565 | resource.content_types_provided(req, res).size match { 566 | case 0 | 1 => Nil 567 | case _ => "Accept" :: Nil 568 | } 569 | } 570 | 571 | private def accept_charset_maybe(resource:Resource, req: Request, res:Response) = { 572 | resource.charsets_provided(req, res) match { 573 | case Some(charsets) => charsets.size match { 574 | case 0 | 1 => Nil 575 | case _ => "Accept-Charset" :: Nil 576 | } 577 | case None => Nil 578 | } 579 | } 580 | 581 | private def accept_encoding_maybe(resource:Resource, req: Request, res:Response) = { 582 | resource.encodings_provided(req, res) match { 583 | case Some(encodings) => encodings.size match { 584 | case 0 | 1 => Nil 585 | case _ => "Accept-Encoding" :: Nil 586 | } 587 | case None => Nil 588 | } 589 | } 590 | 591 | private def accept_languages_maybe(resource:Resource, req: Request, res:Response) = { 592 | resource.languages_provided(req, res) match { 593 | case Some(languages) => languages.size match { 594 | case 0 | 1 => Nil 595 | case _ => "Accept-Language" :: Nil 596 | } 597 | case None => Nil 598 | } 599 | } 600 | 601 | 602 | } --------------------------------------------------------------------------------