├── 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 | }
--------------------------------------------------------------------------------