diff --git a/core/src/main/scala/org/dorest/server/DefaultResponseHeaders.scala b/core/src/main/scala/org/dorest/server/DefaultResponseHeaders.scala
index 00ae287..8de85bf 100644
--- a/core/src/main/scala/org/dorest/server/DefaultResponseHeaders.scala
+++ b/core/src/main/scala/org/dorest/server/DefaultResponseHeaders.scala
@@ -20,8 +20,8 @@ package org.dorest.server
*
* @author Michael Eichberg
*/
-class DefaultResponseHeaders(private var headers: Map[String, String] = Map())
- extends ResponseHeaders {
+class DefaultResponseHeaders(private var headers: Map[String, String] = Map()) extends ResponseHeaders {
+
def this(header: Tuple2[String, String]) {
this(Map() + header)
@@ -31,22 +31,27 @@ class DefaultResponseHeaders(private var headers: Map[String, String] = Map())
this(Map() ++ headers)
}
- def set(key: String, value: String): Unit = {
- headers = headers.updated(key, value)
+ def set(field: String, value: String): this.type = {
+ headers = headers.updated(field, value)
+ this
}
def foreach[U](f: ((String, String)) ⇒ U) {
headers.foreach(f)
}
+ def apply(field: String): String = {
+ headers(field)
+ }
+
+ def get(field: String): Option[String] = {
+ headers.get(field)
+ }
}
object DefaultResponseHeaders {
- def apply(headers: (String, String)*): DefaultResponseHeaders = {
- val responseHeaders = new DefaultResponseHeaders(headers.toMap)
- responseHeaders
- }
+ def apply(headers: (String, String)*): DefaultResponseHeaders = new DefaultResponseHeaders(headers.toMap)
}
diff --git a/core/src/main/scala/org/dorest/server/DoRestApp.scala b/core/src/main/scala/org/dorest/server/DoRestApp.scala
index 6ec2dbb..daa1405 100644
--- a/core/src/main/scala/org/dorest/server/DoRestApp.scala
+++ b/core/src/main/scala/org/dorest/server/DoRestApp.scala
@@ -18,20 +18,26 @@ package org.dorest.server
import collection.mutable.Buffer
/**
- * Enables the registration of [[org.dorest.server.HandlerFactory]] objects.
+ * Enables the registration of [[org.dorest.server.HandlerFactory]] objects. When processing a request the server
+ * will will use/has to use the first HandlerFactory that returns a Handler object to process the request.
+ * The factories are/have to be tried in the order in which they are registered using the register method.
*
- * This trait is to be implemented by DoRest servers.
+ * This trait is generally implemented by DoRest servers.
*
* @author Michael Eichberg
*/
trait DoRestApp {
- private var _factories: Buffer[HandlerFactory] = Buffer()
-
- def factories = _factories
+ /**
+ * The list of all registered ˚HandlerFactory˚ objects.
+ */
+ protected[this] var factories = Buffer[HandlerFactory]()
+ /**
+ * Appends the given handler factory to the list of previously registered `HandlerFactory` objects.
+ */
def register(handlerFactory: HandlerFactory) {
- _factories += handlerFactory
+ factories += handlerFactory
}
}
diff --git a/core/src/main/scala/org/dorest/server/DoRestServer.scala b/core/src/main/scala/org/dorest/server/DoRestServer.scala
index 76c6364..567590c 100644
--- a/core/src/main/scala/org/dorest/server/DoRestServer.scala
+++ b/core/src/main/scala/org/dorest/server/DoRestServer.scala
@@ -17,9 +17,9 @@ package org.dorest.server
/**
* Implements common functionality required when embedding DoRest.
- *
+ *
* '''Remark''' This class is generally only relevant for developers who want to extend/embed DoRest.
- *
+ *
* @author Michael Eichberg
* @author Mateusz Parzonka
*/
diff --git a/core/src/main/scala/org/dorest/server/HTTPMethod.scala b/core/src/main/scala/org/dorest/server/HTTPMethod.scala
index ebf07bf..df6a164 100644
--- a/core/src/main/scala/org/dorest/server/HTTPMethod.scala
+++ b/core/src/main/scala/org/dorest/server/HTTPMethod.scala
@@ -15,8 +15,8 @@
*/
package org.dorest.server
-/** The list of all HTTP Methods as defined by "Hypertext Transfer Protocol -- HTTP/1.1 (RFC 2616)" and also
- * "PATCH Method for HTTP (RFC 5789)".
+/** The list of all HTTP Methods as defined by RFC 2616: "Hypertext Transfer Protocol -- HTTP/1.1" and also
+ * by RFC 5789: "PATCH Method for HTTP".
*
* @author Michael Eichberg
* @author Mateusz Parzonka
diff --git a/core/src/main/scala/org/dorest/server/Handler.scala b/core/src/main/scala/org/dorest/server/Handler.scala
index 4bed097..cd4541d 100644
--- a/core/src/main/scala/org/dorest/server/Handler.scala
+++ b/core/src/main/scala/org/dorest/server/Handler.scala
@@ -34,7 +34,7 @@ import java.io._
trait Handler {
/**
- * The used protocol. E.g., HTTP/1.1
+ * The used protocol. E.g., HTTP/0.9, HTTP/1.0 or HTTP/1.1
*
* '''Control Flow''':
* This field will be set by the server before [[#processRequest(InputStream):Response]] is called.
diff --git a/core/src/main/scala/org/dorest/server/HandlerFactory.scala b/core/src/main/scala/org/dorest/server/HandlerFactory.scala
index 896988e..bfa0968 100644
--- a/core/src/main/scala/org/dorest/server/HandlerFactory.scala
+++ b/core/src/main/scala/org/dorest/server/HandlerFactory.scala
@@ -18,17 +18,30 @@ package org.dorest.server
import java.lang.Long
/**
- * HandlerCreators are responsible for matching URIs and – if the URI matches – to create a new Handler
- * that will then handle the request.
+ * A HandlerFactory is responsible for matching URIs and – if an URI matches – to return a [[org.dorest.server.Handler]]
+ * that will then be used to handle the request and to create the response.
*
- * '''Thread Safety'''
- * Handler factories have to be thread safe. I.e., handler factories have to support the simultaneous
- * matching of URIs; the DoRest framework use a single Handler factory for matching URIs.
+ * '''Thread Safety''': Handler factories have to be thread safe. I.e., handler factories have to support the
+ * simultaneous matching of URIs; the DoRest framework use a single Handler factory for matching URIs. However,
+ * a Handler object has to be thread safe if and only if the same handler object is returned more than once.
+ * Hence, to avoid/limit concurrency issues it is recommended to return a new Handler whenever a path matches.
*
* @author Michael Eichberg
*/
trait HandlerFactory {
+ /**
+ * Tries to match the path and query part of a given URI.
+ *
+ * '''Control Flow''': This method is called by the DoRest framework when an HTTP request is made.
+ * For example, imagine an HTTP request with the following
+ * URI is made: `http://www.opal-project.de/pages?search=DoRest`. In this case DoRest will analyze the URI
+ * and split it up. The path part would be `/pages` and the query string would be `search=DoRest`.
+ *
+ * @param path A URI's path part.
+ * @param query A URI's query part.
+ * @return `Some` handler if the path and query were successfully matched. `None` otherwise.
+ */
def matchURI(path: String, query: String): Option[Handler]
}
diff --git a/core/src/main/scala/org/dorest/server/NotAcceptable.scala b/core/src/main/scala/org/dorest/server/NotAcceptable.scala
index c50d44e..ac2078e 100644
--- a/core/src/main/scala/org/dorest/server/NotAcceptable.scala
+++ b/core/src/main/scala/org/dorest/server/NotAcceptable.scala
@@ -17,16 +17,18 @@
package org.dorest.server
/**
- * '''From the HTTP Spec.''':{{{
+ * '''From the HTTP Spec.''':
+ *
* Note: HTTP/1.1 servers are allowed to return responses which are
* not acceptable according to the accept headers sent in the
* request. In some cases, this may even be preferable to sending a
* 406 response. User agents are encouraged to inspect the headers of
* an incoming response to determine if it is acceptable.
- * }}}
+ *
+ *
* @author Michael Eichberg
*/
-object NotAcceptableResponse extends PlainResponse(406)
+object NotAcceptableResponse extends PlainResponse(406)
diff --git a/core/src/main/scala/org/dorest/server/OkResponse.scala b/core/src/main/scala/org/dorest/server/OkResponse.scala
index 30e5256..c18d028 100644
--- a/core/src/main/scala/org/dorest/server/OkResponse.scala
+++ b/core/src/main/scala/org/dorest/server/OkResponse.scala
@@ -15,32 +15,30 @@
*/
package org.dorest.server
-
/**
* Encapsulates an OK response.
- *
- * From RFC 2616 (HTTP/1.1):
- *
- * The request has succeeded. The information returned with the response is dependent on the method used in the request, for example:
*
- * GET an entity corresponding to the requested resource is sent in the response;
- *
- * HEAD the entity-header fields corresponding to the requested resource are sent in the response without any message-body;
- *
- * POST an entity describing or containing the result of the action;
- *
- * TRACE an entity containing the request message as received by the end server.
- *
- * For further details see Hypertext Transfer Protocol -- HTTP/1.1 (RFC 2616)
- *
+ * '''RFC 2616 (HTTP/1.1)''':
+ * The request has succeeded. The information returned with the response is dependent on the method used in
+ * the request, for example:
+ *
+ * - GET an entity corresponding to the requested resource is sent in the response;
+ *
+ * - HEAD the entity-header fields corresponding to the requested resource are sent in the response without any message-body;
+ *
+ * - POST an entity describing or containing the result of the action;
+ *
+ * - TRACE an entity containing the request message as received by the end server.
+ *
+ * @see [[http://www.w3.org/Protocols/rfc2616/rfc2616.html RFC 2616 HTTP/1.1]]
+ *
* @author Michael Eichberg
*/
abstract class OkResponse extends Response {
final def code = 200 //OK
-
-}
+}
object OkResponse {
diff --git a/core/src/main/scala/org/dorest/server/PlainResponse.scala b/core/src/main/scala/org/dorest/server/PlainResponse.scala
index df103a4..9d6e350 100644
--- a/core/src/main/scala/org/dorest/server/PlainResponse.scala
+++ b/core/src/main/scala/org/dorest/server/PlainResponse.scala
@@ -15,15 +15,15 @@
*/
package org.dorest.server
-
/**
* @author Michael Eichberg
*/
-abstract class PlainResponse(val code: Int) extends Response {
-
- def headers = new DefaultResponseHeaders()
+abstract class PlainResponse(
+ val code: Int,
+ val headers: ResponseHeaders = new DefaultResponseHeaders())
+ extends Response {
- def body = None
+ final def body = None
}
diff --git a/core/src/main/scala/org/dorest/server/Redirect.scala b/core/src/main/scala/org/dorest/server/Redirect.scala
index 820d246..183de61 100644
--- a/core/src/main/scala/org/dorest/server/Redirect.scala
+++ b/core/src/main/scala/org/dorest/server/Redirect.scala
@@ -28,44 +28,29 @@ import java.io._
*/
class Redirect(val location: String) extends Handler {
- val response = new Response {
- def code = 303;
-
- val headers = new DefaultResponseHeaders("Location" -> location)
-
- def body = None
- }
+ val response = new PlainResponse(303) { headers.set("Location", location) }
def processRequest(requestBody: ⇒ InputStream) = {
// we actually don't process the request at all
response;
}
-
}
abstract class DynamicRedirect extends Handler {
- def location : Option[String]
+ def location: Option[String]
private def response(): Response = {
location match {
- case Some(l) ⇒
- return new Response {
- def code = 303;
-
- val headers = new DefaultResponseHeaders("Location" -> l)
-
- def body = None
- }
- case None ⇒ NotFoundResponse
+ case Some(l) ⇒ new PlainResponse(303) { headers.set("Location", l) }
+ case None ⇒ NotFoundResponse
}
}
- def processRequest(requestBody: => InputStream) : Response = {
+ def processRequest(requestBody: ⇒ InputStream): Response = {
// we actually don't process the request at all
response();
}
-
}
diff --git a/core/src/main/scala/org/dorest/server/Response.scala b/core/src/main/scala/org/dorest/server/Response.scala
index f2ca1f2..f6903b3 100644
--- a/core/src/main/scala/org/dorest/server/Response.scala
+++ b/core/src/main/scala/org/dorest/server/Response.scala
@@ -15,43 +15,63 @@
*/
package org.dorest.server
-/** A response object encapsulates a request's response.
- *
- * The precise structure of a response depends on the request and is defined by
- * RFC 2616.
- *
- * @author Michael Eichberg
- */
+/**
+ * A response object encapsulates a request's response.
+ *
+ * The precise/expected structure of a response depends on the request and is defined by
+ * [[http://www.w3.org/Protocols/rfc2616/rfc2616.html RFC 2616]].
+ *
+ * @author Michael Eichberg
+ */
trait Response {
- /** The status code of the response.
- *
- * Go to: HTTP Status Codes for further
- * details.
- */
+ /**
+ * The status code of the response.
+ *
+ * @see [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 HTTP Status Codes]]
+ */
def code: Int
- /** A response's headers.
- *
- * The response headers for the Content-type and Content-length are automatically set based on the
- * response body.
- *
- * '''Remark''': ResponseHeaders must not be null and mutable.
- */
+ /**
+ * A response's headers.
+ *
+ * @note The HTTP headers Content-Type and Content-Length are automatically set based on the
+ * response body.
+ * @return This response's header fields (non-null).
+ */
def headers: ResponseHeaders
- /** The body that is send back to the client.
- */
+ /**
+ * The body that is send back to the client.
+ */
def body: Option[ResponseBody]
}
-
+/**
+ * Factory object for creating a [[org.dorest.server.Response]] object.
+ */
object Response {
- def apply(responseCode: Int, responseHeaders: ResponseHeaders, responseBody: Option[ResponseBody]) =
+ /**
+ * Creates a new, generic [[org.dorest.server.Response]] object.
+ *
+ * @param responseCode A valid HTTP response code. See also [[org.dorest.server.Response]].code
+ * @param responseHeaders The HTTP response headers. See also [[org.dorest.server.Response]].headers
+ * @param responseBody The body of the HTTP response. See also [[org.dorest.server.Response]].body
+ * @return A new response object.
+ */
+ def apply(responseCode: Int, responseHeaders: ResponseHeaders, responseBody: Option[ResponseBody]) = {
+ require(responseHeaders ne null)
+ require(responseBody match {
+ case Some(_) ⇒ true // the body may be lazily initialized; hence we do not check that `body.length > 0`
+ case None ⇒ true
+ case null ⇒ false
+ });
+
new Response {
val code: Int = responseCode
val headers: ResponseHeaders = responseHeaders
val body: Option[ResponseBody] = responseBody
}
+ }
}
\ No newline at end of file
diff --git a/core/src/main/scala/org/dorest/server/ResponseBody.scala b/core/src/main/scala/org/dorest/server/ResponseBody.scala
index 9880f65..7195f1b 100644
--- a/core/src/main/scala/org/dorest/server/ResponseBody.scala
+++ b/core/src/main/scala/org/dorest/server/ResponseBody.scala
@@ -18,7 +18,6 @@ package org.dorest.server
import java.nio.charset.Charset
import java.io.OutputStream
-
/**
* Encapsulates a response's body.
*
@@ -37,8 +36,11 @@ trait ResponseBody {
def length: Int
/**
- * Called by the framework – after sending the HTTP header – to write
+ * Called by the DoRest framework – after sending the HTTP header – to write
* out the specific representation as the response's body.
+ *
+ * '''Contract'''
+ * Exactly as many bytes have to be written as specified by length.
*/
def write(responseBody: OutputStream): Unit
}
diff --git a/core/src/main/scala/org/dorest/server/ResponseHeaders.scala b/core/src/main/scala/org/dorest/server/ResponseHeaders.scala
index f26cbfa..4a0e28e 100644
--- a/core/src/main/scala/org/dorest/server/ResponseHeaders.scala
+++ b/core/src/main/scala/org/dorest/server/ResponseHeaders.scala
@@ -1,5 +1,5 @@
/*
- Copyright 2011 Michael Eichberg et al
+ Copyright 2011, 2012 Michael Eichberg et al
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,24 +15,138 @@
*/
package org.dorest.server
+import scala.collection.Traversable
+
/**
- * A response's headers.
+ * An HTTP response's headers.
+ *
+ * For an overview go to [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14 HTTP Header Fields]].
*
* @author Michael Eichberg
*/
-trait ResponseHeaders extends collection.Traversable[Tuple2[String,String]] {
+trait ResponseHeaders extends Traversable[(String, String)] {
/**
- * Sets the value of the specified response header.
+ * Sets the value (value) of the specified response/entity header (field).
+ *
+ * If this method is directly used, it is
+ * the responsibility of the caller to make sure that the value is valid.
+ * Cf. [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14 HTTP Header Fields]] for further
+ * details. In general, it is recommended to use one of the predefined methods to set an entity/a response
+ * header field.
*
- * Cf. HTTP Header Fields.
+ * @note Though, names of request and response header fields are case-insensitive
+ * (cf. [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html#sec2 HTTP/1.1 Notational Conventions]]),
+ * they are typically used using small letters and DoRest follows this convention.
+ *
+ * @param field The name of the response header; e.g., "age".
+ * @param value The value of the response header; e.g., "10021" in case of the "age" response header field.
*/
- def set(key: String, value: String): Unit
+ def set(field: String, value: String): this.type
/**
- * Enables you to iterate over all response headers.
+ * Sets the Accept-Ranges header field.
+ *
+ * @see [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.5 RFC 2616 - Accept-Ranges]]
+ */
+ def setAcceptRanges(acceptableRanges: String): this.type = {
+ require(acceptableRanges ne null)
+ require(acceptableRanges.length > 0)
+
+ set("accept-ranges", acceptableRanges)
+ this
+ }
+
+ /**
+ * Sets the Age header field.
+ *
+ * @see [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.6 RFC 2616 - Age]]
+ */
+ def setAge(timeInSecs: Long): this.type = {
+ require(timeInSecs >= 0)
+
+ set("age", String.valueOf(timeInSecs))
+ this
+ }
+
+ /**
+ * Sets the Allow header field.
+ *
+ * @see [[org.dorest.core.ResponseHeaders.setAllow(Seq[HTTPMethod])]]
+ */
+ def setAllow(methods: HTTPMethod*): this.type = setAllow(methods.toSeq)
+
+ /**
+ * Sets the Allow header field.
+ *
+ * @see [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7 RFC 2616 - Allow]]
+ * @note An Allow header field MUST be present in a 405 (Method Not Allowed) response.
+ * @param methods The non-empty set of supported HTTP methods.
+ */
+ def setAllow(methods: Traversable[HTTPMethod]): this.type = {
+ require(methods.toSet.size == methods.size)
+
+ set("allow", methods.mkString(", "))
+ this
+ }
+
+ /**
+ * Sets the Content-Language header field.
+ *
+ * @see [[org.dorest.core.ResponseHeaders.setContentLanguage(Seq[java.util.Locale])]]
+ */
+ def setContentLanguage(languageTags: java.util.Locale*): this.type = setContentLanguage(languageTags.toSeq)
+
+ /**
+ * Sets the content language header field.
+ *
+ * @see [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.12 RFC 2616 - Content-Language]]
+ * @note Language tags are case-insensitive. However, the ISO 639/ISO 3166 convention is that language names
+ * are written in lower case, while country codes are written in upper case (e.g., "en-US").
+ *
+ * Other language tags, such as, "x-pig-latin" (cf. [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.10 RFC 2616 - Language Tags]]) could
+ * theoretically also be specified, but are not supported by this method. If you need to specify
+ * such a language tag, use the generic `set(field,value)` method.
+ *
+ * @param languageTags A non-empty list of locales for which the language tag MUST be set.
+ */
+ def setContentLanguage(languageTags: Traversable[java.util.Locale]): this.type = {
+ require(languageTags.size > 0)
+
+ set(
+ "content-language",
+ (localeToLanguageTag(languageTags.head) /: languageTags.tail)(_ + (", ") + localeToLanguageTag(_))
+ )
+ this
+ }
+
+ /**
+ * Converts a locale object with a valid, non-empty language part into a language tag as defined by
+ * [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.10 RFC 2616 - Language Tags]].
+ *
+ * @return A valid language tag.
+ */
+ def localeToLanguageTag(locale: java.util.Locale): String = {
+ require(locale.getLanguage.length > 0)
+
+ var languageTag = locale.getLanguage
+ val country = locale.getCountry
+ if (country.length > 0) {
+ languageTag += "-" + country
+ }
+ languageTag
+ }
+
+ // TODO implement support for the rest of the HTTP/1.1 response/entity header fields
+
+ /**
+ * Iterates over all response headers.
+ *
+ * '''Typical Usage'''
+ * This method is used by DoRest to iterate over the set of specified response
+ * headers.
*/
- def foreach[U](f: ((String, String)) => U): Unit
+ def foreach[U](f: ((String, String)) ⇒ U): Unit
}
diff --git a/core/src/main/scala/org/dorest/server/SupportedMethodsResponse.scala b/core/src/main/scala/org/dorest/server/SupportedMethodsResponse.scala
index d759be3..7bda4ca 100644
--- a/core/src/main/scala/org/dorest/server/SupportedMethodsResponse.scala
+++ b/core/src/main/scala/org/dorest/server/SupportedMethodsResponse.scala
@@ -17,19 +17,20 @@ package org.dorest.server
/**
- * '''HTTP 1.1 Specification Method Not Allowed (405)''': The
+ * '''[[http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.6 HTTP 1.1 Specification Method Not Allowed (405)]] The
* method specified in the Request-Line is not allowed for the resource identified by the Request-URI. The response
* MUST include an Allow header containing a list of valid methods for the requested resource.
*
* @author Michael Eichberg
*/
-class SupportedMethodsResponse(val allowedMethods: List[HTTPMethod], val code: Int = 200) extends Response {
+class SupportedMethodsResponse(val allowedMethods: Seq[HTTPMethod], val code: Int = 200/* TODO is this return value correct?*/) extends Response {
def this(allowedMethod: HTTPMethod) {
this (allowedMethod :: Nil)
}
-
- val headers = new DefaultResponseHeaders(("Allow", allowedMethods.mkString(", ")))
+
+ val headers = new DefaultResponseHeaders()
+ headers.setAllow(allowedMethods)
def body = None
diff --git a/core/src/main/scala/org/dorest/server/UnsupportedMediaTypeResponse.scala b/core/src/main/scala/org/dorest/server/UnsupportedMediaTypeResponse.scala
index 078099f..302b26b 100644
--- a/core/src/main/scala/org/dorest/server/UnsupportedMediaTypeResponse.scala
+++ b/core/src/main/scala/org/dorest/server/UnsupportedMediaTypeResponse.scala
@@ -16,8 +16,15 @@
package org.dorest.server
/**
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
- * '''The server is refusing to service the request because the entity of the request is in a format not supported by the requested resource for the requested method.'''
+ * Represents an `Unsupported Media Type Response`.
+ *
+ * From the specification:
+ *
+ * The server is refusing to service the request because the entity of the request is in a format not
+ * supported by the requested resource for the requested method.
+ *
+ *
+ * @see [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html RFC 2616 - Unsupported Media Type Response]]
*
* @author Michael Eichberg
*/
diff --git a/core/src/main/scala/org/dorest/server/http/Server.scala b/core/src/main/scala/org/dorest/server/http/Server.scala
index 2d9048d..6ed1b9f 100644
--- a/core/src/main/scala/org/dorest/server/http/Server.scala
+++ b/core/src/main/scala/org/dorest/server/http/Server.scala
@@ -15,6 +15,7 @@
*/
package org.dorest.server.http
-class Server(val port : Int) {
- // TODO implement
+// TODO implement
+class Server(val port: Int) {
+
}
\ No newline at end of file
diff --git a/core/src/main/scala/org/dorest/server/jdk/Demo.scala b/core/src/main/scala/org/dorest/server/jdk/Demo.scala
index 0178df1..145c29f 100644
--- a/core/src/main/scala/org/dorest/server/jdk/Demo.scala
+++ b/core/src/main/scala/org/dorest/server/jdk/Demo.scala
@@ -27,7 +27,7 @@ import java.net.{ InetAddress, URI, URL }
//
// CONFIGURATION OF VARIOUS RESOURCES
//
-// ------------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------------
class Time
extends RESTInterface
@@ -42,7 +42,7 @@ class Time
}
get returns HTML {
- "The current (server) time is: "+new java.util.Date().toString+""
+ "The current (server) time is: " + new java.util.Date().toString + ""
}
get returns XML {
@@ -54,12 +54,13 @@ object Time extends Time with PerformanceMonitor
class User(var user: String) extends RESTInterface with TEXTSupport {
get returns TEXT {
- "Welcome "+user
+ "Welcome " + user
}
}
-/** Implementation of a very primitive, thread-safe key-value store.
- */
+/**
+ * Implementation of a very primitive, thread-safe key-value store.
+ */
object KVStore {
private val ds = new scala.collection.mutable.HashMap[Long, String]()
@@ -101,10 +102,11 @@ object KVStore {
}
}
-/** '''Usage'''
- * To store a new value, send a post request where the content-type header is set to application/xml and
- * where the value is stored in an "value" XML element (e.g., <value>My Value</value>).
- */
+/**
+ * '''Usage'''
+ * To store a new value, send a post request where the content-type header is set to application/xml and
+ * where the value is stored in an "value" XML element (e.g., <value>My Value</value>).
+ */
class Keys extends RESTInterface with XMLSupport {
get returns XML {
@@ -121,7 +123,7 @@ class Keys extends RESTInterface with XMLSupport {
// convenience method: Location(URI)
// Alternatively, it is possible to directly set the response headers
// using the corresponding response headers data structure.
- Location(new URL("http://"+InetAddress.getLocalHost.getHostName+":9009/keys/"+id.toString)) // TODO enable to specify the relative path
+ Location(new URL("http://" + InetAddress.getLocalHost.getHostName + ":9009/keys/" + id.toString)) // TODO enable to specify the relative path
// the "response body"
{ value }
@@ -136,8 +138,7 @@ class Key(val id: Long) extends RESTInterface with XMLSupport {
if (!KVStore.contains(id)) {
responseCode = 404 // 404 = NOT FOUND
None // EMPTY BODY
- }
- else {
+ } else {
val value = KVStore(id)
{ value }
}
@@ -149,8 +150,7 @@ class Key(val id: Long) extends RESTInterface with XMLSupport {
if (!KVStore.contains(id)) {
responseCode = 404 // NOT FOUND
None
- }
- else {
+ } else {
KVStore.updated(id, XMLRequestBody.text)
{ XMLRequestBody.text }
}
@@ -183,9 +183,9 @@ object MonitoredMappedDirectory {
// ------------------------------------------------------------------------------------------------------
trait DemoRESTInterface extends DoRestApp with URIsMatcher {
- addPathMatcher {
+ addURIMatcher {
/ {
- case "keys" ⇒ / {
+ case "keys" ⇒ / {
case MATCHED() ⇒ new Keys
case LONG(id) ⇒ new Key(id)
}
@@ -193,19 +193,24 @@ trait DemoRESTInterface extends DoRestApp with URIsMatcher {
case STRING(userId) ⇒ new User(userId) with PerformanceMonitor with ConsoleLogging
}
case "time" ⇒
- /** Reusing one instance of a resource to handle all requests requires that the resource is thread safe.
- * If you are unsure, just create a new instance for each request!
- *
- * If your resource is not trivially thread-safe, we recommend that you do not try to make it thread safe
- * and instead just create a new instance.
- *
- * In general, whenever you have to extract path parameters or have to process a request body or your
- * object representing the resource has some kind of mutable state, it is relatively certain that you
- * have to create a new instance to handle a request.
- */
+ /**
+ * Reusing one instance of a resource to handle all requests requires that the resource is thread safe.
+ * If you are unsure, just create a new instance for each request!
+ *
+ * If your resource is not trivially thread-safe, we recommend that you do not try to make it thread safe
+ * and instead just create a new instance.
+ *
+ * In general, whenever you have to extract path parameters or have to process a request body or your
+ * object representing the resource has some kind of mutable state, it is relatively certain that you
+ * have to create a new instance to handle a request.
+ */
Time
case "static" ⇒ bind path (MonitoredMappedDirectory(System.getProperty("user.home")))
- }
+ case "echo" ⇒ / {
+ case "query" ⇒ bind query ((query) ⇒ new TEXTResource { get returns TEXT { query } })
+ case "path" ⇒ bind path ((remainingPath) ⇒ new TEXTResource { get returns TEXT { remainingPath } })
+ }
+ }
}
}
@@ -215,12 +220,13 @@ trait DemoRESTInterface extends DoRestApp with URIsMatcher {
//
// ------------------------------------------------------------------------------------------------------
-/** To test the restful web service you can use, e.g., curl. For example, to
- * add a value to the simple key-value store you can use:
- *
- * curl -v -X POST -d "Test" -H content-type:application/xml http://localhost:9009/keys
- * curl http://localhost:9009/keys
- */
+/**
+ * To test the restful web service you can use, e.g., curl. For example, to
+ * add a value to the simple key-value store you can use:
+ *
+ * curl -v -X POST -d "Test" -H content-type:application/xml http://localhost:9009/keys
+ * curl http://localhost:9009/keys
+ */
object HTTPDemo
extends JDKServer(9009)
with DemoRESTInterface
@@ -229,12 +235,13 @@ object HTTPDemo
start()
}
-/** To test the restful web service you can use, e.g., curl. For example, to
- * add a value to the simple key-value store you can use:
- *
- * curl -v -X POST -d "Test" -H content-type:application/xml http://localhost:9009/keys
- * curl https://localhost:9099/keys
- */
+/**
+ * To test the restful web service you can use, e.g., curl. For example, to
+ * add a value to the simple key-value store you can use:
+ *
+ * curl -v -X POST -d "Test" -H content-type:application/xml http://localhost:9009/keys
+ * curl https://localhost:9099/keys
+ */
class HTTPSDemo(port: Int) extends HttpsJDKServer(port) with DemoRESTInterface
object HTTPSDemo extends HTTPSDemo(9099) with scala.App {
diff --git a/core/src/main/scala/org/dorest/server/rest/RESTInterface.scala b/core/src/main/scala/org/dorest/server/rest/RESTInterface.scala
index 8e19a45..67858bb 100644
--- a/core/src/main/scala/org/dorest/server/rest/RESTInterface.scala
+++ b/core/src/main/scala/org/dorest/server/rest/RESTInterface.scala
@@ -207,7 +207,7 @@ trait RESTInterface extends Handler {
final object post {
/**
- * @see [[#of(RequestBodyProcessor)]]
+ * @see [[org.dorest.server.rest.RESTInterface.post#of(RequestBodyProcessor)]]
*/
def sends(requestBodyHandler: RequestBodyProcessor) = of(requestBodyHandler)
@@ -226,7 +226,7 @@ trait RESTInterface extends Handler {
final object put {
/**
- * @see [[#of(RequestBodyProcessor)]]
+ * @see [[org.dorest.server.rest.RESTInterface.put#of(RequestBodyProcessor)]]
*/
def sends(requestBodyHandler: RequestBodyProcessor) = of(requestBodyHandler)
@@ -245,7 +245,7 @@ trait RESTInterface extends Handler {
final object patch {
/**
- * @see [[#of(RequestBodyProcessor)]]
+ * @see [[org.dorest.server.rest.RESTInterface.patch#of(RequestBodyProcessor)]]
*/
def sends(requestBodyHandler: RequestBodyProcessor) = of(requestBodyHandler)
diff --git a/core/src/main/scala/org/dorest/server/rest/TEXTSupport.scala b/core/src/main/scala/org/dorest/server/rest/TEXTSupport.scala
index 5f34d1e..81bd56c 100644
--- a/core/src/main/scala/org/dorest/server/rest/TEXTSupport.scala
+++ b/core/src/main/scala/org/dorest/server/rest/TEXTSupport.scala
@@ -26,11 +26,15 @@ import io.Codec
trait TEXTSupport {
// TODO add support for posting TEXT
-
- protected implicit def textToSomeText(textPlain: String) : Option[String] = Some(textPlain)
- def TEXT(getText: => Option[String]) =
+ protected implicit def textToSomeText(textPlain: String): Option[String] = Some(textPlain)
+
+ def TEXT(getText: ⇒ Option[String]) =
RepresentationFactory(MediaType.TEXT_PLAIN) {
- getText map ((text) => new UTF8BasedRepresentation(MediaType.TEXT_PLAIN, Codec.toUTF8(text)))
+ getText map ((text) ⇒ new UTF8BasedRepresentation(MediaType.TEXT_PLAIN, Codec.toUTF8(text)))
}
+}
+
+class TEXTResource extends RESTInterface with TEXTSupport {
+
}
\ No newline at end of file
diff --git a/core/src/main/scala/org/dorest/server/rest/URIsMatcher.scala b/core/src/main/scala/org/dorest/server/rest/URIsMatcher.scala
index 04fc662..4294d01 100644
--- a/core/src/main/scala/org/dorest/server/rest/URIsMatcher.scala
+++ b/core/src/main/scala/org/dorest/server/rest/URIsMatcher.scala
@@ -16,119 +16,86 @@
package org.dorest.server
package rest
-/** Utility function to facilitate the matching of URIs.
- *
- * @author Michael Eichberg
- */
+/**
+ * Utility function to facilitate the matching of URIs.
+ *
+ * @author Michael Eichberg
+ */
trait URIsMatcher {
- type PathMatcher = ( /*path*/ String) ⇒ Option[( /*query*/ String) ⇒ Option[Handler]]
-
// NEEDS TO BE PROVIDED BY THE CLASS WHERE THIS CLASS IS MIXED IN
def register(handlerFactory: HandlerFactory): Unit
- def addPathMatcher(pathMatcher: PathMatcher) {
+ /**
+ * Register a partial function that tries to match a request's path and query string.
+ *
+ * @example
+ * {{{
+ * val PathExtractor = """/static/(.*)""".r
+ * val SuffixExtractor = """.*suffix=(.+)""".r
+ * addMatcher({
+ * case (PathExtractor(path),SuffixExtractor(suffix)) => new Handler{...}
+ * })
+ * }}}
+ *
+ * @param matcher A partial function that takes a tuple of strings, where the first string will be the
+ * current path and the second string will be the query part. The return value has to (if both are matched) a
+ * valid (non-null) handler object.
+ */
+ def addMatcher(matcher: PartialFunction[( /*path*/ String, /*query*/ String), Handler]) {
register(new HandlerFactory {
- def matchURI(path: String, query: String): Option[Handler] = {
- pathMatcher(path) match {
- case Some(qm) ⇒ qm(query)
- case None ⇒ None
- }
- }
+ def matchURI(path: String, query: String): Option[Handler] = matcher.lift((path, query))
})
}
- /** Registers a partial function that tries to match a request's path and query string.
- *
- * '''Usage Scenario'''
- * {{{
- * val PathExtractor = """/static/(.*)""".r
- * val SuffixExtractor = """.*suffix=(.+)""".r
- * addMatcher({
- * case (PathExtractor(path),SuffixExtractor(suffix)) => new Handler{...}
- * })
- * }}}
- *
- * @param matcher A partial function that takes a tuple of strings, where the first string is the
- * current path and the second string is the query part. The return value is (if both are matched) a
- * valid (non-null) handler object.
- */
- def addMatcher(matcher: PartialFunction[( /*path*/ String, /*query*/ String), Handler]) {
+ type URIMatcher = ( /*path*/ String) ⇒ Option[( /*query*/ String) ⇒ Option[Handler]]
+
+ def addURIMatcher(uriMatcher: URIMatcher) {
register(new HandlerFactory {
def matchURI(path: String, query: String): Option[Handler] = {
- if (matcher.isDefinedAt((path, query)))
- Some(matcher((path, query)))
- else
- None
+ uriMatcher(path) match {
+ case Some(qm) ⇒ qm(query)
+ case None ⇒ None
+ }
}
})
}
- /** Use ROOT to match a URI that ends with "/" and where all previous segments have been successfully
- * matched.
- */
+ //
+ //
+ // CODE RELATED TO MATCHING THE PATH OF AN URI
+ //
+ //
+
+ /**
+ * Use ROOT to match (the remaining) path of an URI that ends with "/" and where all previous segments have been
+ * successfully matched.
+ */
object ROOT {
def unapply(pathSegment: String): Boolean = {
!(pathSegment eq null) && pathSegment.length == 0
}
}
- /** Use MATCHED() to match a URI that does not end with "/".
- */
+ /**
+ * Use MATCHED() to match a URI that does not end with "/".
+ */
object MATCHED {
def unapply(pathSegment: String): Boolean = {
pathSegment eq null
}
}
- /** Use STRING(s) to match a path segment that is non-empty.
- */
- object STRING {
- def unapply(pathSegment: String): Option[String] = {
- if (!(pathSegment eq null) && pathSegment.length > 0)
- Some(pathSegment)
- else
- None
- }
- }
+ case class /(matcher: PartialFunction[String, URIMatcher]) extends URIMatcher {
- object LONG {
- def unapply(pathSegment: String): Option[Long] = {
- if (pathSegment eq null) return None
-
- try {
- Some(java.lang.Long.parseLong(pathSegment, 10))
- }
- catch {
- case e: NumberFormatException ⇒ None
- case e ⇒ throw e;
- }
- }
- }
-
- object INT {
- def unapply(pathSegment: String): Option[Int] = {
- if (pathSegment eq null) return None
-
- try {
- Some(java.lang.Integer.parseInt(pathSegment, 10))
- }
- catch {
- case e: NumberFormatException ⇒ None
- case e ⇒ throw e;
- }
- }
- }
-
- case class /(matcher: PartialFunction[String, PathMatcher]) extends PathMatcher {
-
- /** @param completePath A valid URI path (or the yet unmatched part of the URI).
- * The completePath is either null or is a string that starts with a "/".
- * The semantics of null is that the complete path was matched; i.e., there
- * is no remaining part.
- */
- def apply(completePath: String): Option[(String) ⇒ Option[Handler]] = {
- if (completePath == null)
+ /**
+ * @param trailingPath A valid URI path (or the yet unmatched part of the path of an URI).
+ * The trailingPath is either null or is a string that starts with a "/".
+ * The semantics of null is that the complete path was matched; i.e., there
+ * is no remaining part.
+ */
+ def apply(trailingPath: String): Option[(String) ⇒ Option[Handler]] = {
+ if (trailingPath == null)
return {
if (matcher.isDefinedAt(null))
matcher(null)(null)
@@ -138,10 +105,10 @@ trait URIsMatcher {
None
}
- if (completePath.charAt(0) != '/')
- throw new IllegalArgumentException("The provided path: \""+completePath+"\" is invalid; it must start with a /.")
+ if (trailingPath.charAt(0) != '/')
+ throw new IllegalArgumentException("The provided path: \"" + trailingPath + "\" is invalid; it must start with a /.")
- val path = completePath.substring(1) // we truncate the trailing "/"
+ val path = trailingPath.substring(1) // we truncate the trailing "/"
val separatorIndex = path.indexOf('/')
val head = if (separatorIndex == -1) path else path.substring(0, separatorIndex)
val tail = if (separatorIndex == -1 || (separatorIndex == 0 && path.length == 1)) null else path.substring(separatorIndex)
@@ -149,11 +116,10 @@ trait URIsMatcher {
matcher(head)(tail)
else
None
-
}
}
- implicit def HandlerToPathMatcher(h: Handler): PathMatcher = {
+ implicit def HandlerToPathMatcher(h: Handler): URIMatcher = {
(pathSegment: String) ⇒
{
if (pathSegment eq null)
@@ -163,13 +129,158 @@ trait URIsMatcher {
}
}
+ //
+ //
+ // CODE RELATED TO MATCHING THE QUERY PART OF URIs
+ //
+ //
+
+ case class ?(matcher: PartialFunction[URIQuery, Handler]) extends URIMatcher {
+ def apply(path: String): Option[String ⇒ Option[Handler]] = {
+ if (path ne null)
+ None
+ else {
+ Some((query: String) ⇒ {
+ val splitUpQuery = org.dorest.server.utils.URIUtils.decodeRawURLQueryString(query)
+ if (matcher.isDefinedAt(splitUpQuery)) {
+ Some(matcher(splitUpQuery))
+ } else {
+ None
+ }
+ })
+ }
+ }
+ }
+
+ type URIQuery = Map[String /*query key*/ , Seq[String] /* values associated with the key*/ ]
+
+ trait QueryMatcher {
+ protected def apply[T](kvMatcher: KeyValueMatcher[T], uriQuery: URIQuery): Option[T] = {
+ val (key, valueMatcher) = kvMatcher
+ uriQuery.get(key) match {
+ case Some(values) ⇒ {
+ if (valueMatcher.isDefinedAt(values))
+ Some(valueMatcher(values))
+ else
+ None
+ }
+ case None ⇒ None
+ }
+ }
+ }
+ type KeyValueMatcher[T] = (String, PartialFunction[Seq[String], T])
+
+ class QueryMatcher1[T1](val kvMatcher1: KeyValueMatcher[T1]) extends QueryMatcher {
+ def unapply(uriQuery: URIQuery): Some[Option[T1]] = {
+ Some(apply(kvMatcher1, uriQuery))
+ }
+ }
+
+ class QueryMatcher2[T1, T2](val kvm1: KeyValueMatcher[T1], val kvm2: KeyValueMatcher[T2]) extends QueryMatcher {
+ def unapply(uriQuery: URIQuery): Some[(Option[T1], Option[T2])] = {
+ Some((apply(kvm1, uriQuery), apply(kvm2, uriQuery)))
+ }
+ }
+
+ object QueryMatcher {
+ def apply[T1](kvm1: KeyValueMatcher[T1]) =
+ new QueryMatcher1(kvm1)
+
+ def apply[T1, T2](kvm1: KeyValueMatcher[T1], kvm2: KeyValueMatcher[T2]) =
+ new QueryMatcher2(kvm1, kvm2)
+
+ }
+
+ //
+ //
+ // CODE RELATED TO MATCHING THE PATH AND THE QUERY PART OF URIs
+ //
+ //
+
+ /**
+ * Use STRING(s) to match a path segment that is non-empty.
+ */
+ object STRING {
+ def apply(key: String): KeyValueMatcher[String] = (
+ key,
+ { case Seq(head, _*) ⇒ head }
+ )
+ def unapply(pathSegment: String): Option[String] = {
+ if (!(pathSegment eq null) && pathSegment.length > 0)
+ Some(pathSegment)
+ else
+ None
+ }
+ }
+
+ object LONG {
+ def apply(key: String): KeyValueMatcher[Long] = (
+ key,
+ new PartialFunction[Seq[String], Long] {
+ def isDefinedAt(values: Seq[String]): Boolean = {
+ values.exists(_ match { case LONG(_) ⇒ true; case _ ⇒ false })
+ }
+
+ def apply(values: Seq[String]): Long = {
+ values.collectFirst({ case LONG(l) ⇒ l }).get
+ }
+ }
+ )
+ def unapply(string: String): Option[Long] = {
+ if (string eq null)
+ None
+ else
+ try {
+ Some(java.lang.Long.parseLong(string, 10))
+ } catch {
+ case e: NumberFormatException ⇒ None
+ case e ⇒ throw e;
+ }
+ }
+ }
+
+ object INT {
+
+ def apply(key: String): KeyValueMatcher[Int] = (
+ key,
+ new PartialFunction[Seq[String], Int] {
+ def isDefinedAt(values: Seq[String]): Boolean = {
+ values.exists(_ match { case INT(_) ⇒ true; case _ ⇒ false })
+ }
+
+ def apply(values: Seq[String]): Int = {
+ values.collectFirst({ case INT(i) ⇒ i }).get
+ }
+ }
+ )
+
+ def unapply(pathSegment: String): Option[Int] = {
+ if (pathSegment eq null) None
+ else
+ try {
+ Some(java.lang.Integer.parseInt(pathSegment, 10))
+ } catch {
+ case e: NumberFormatException ⇒ None
+ case e ⇒ throw e;
+ }
+ }
+ }
+
object bind {
- def path(handlerInitializer: (String) ⇒ Handler): PathMatcher =
+
+ def path(handlerInitializer: (String) ⇒ Handler): URIMatcher =
(pathSegment: String) ⇒
if (pathSegment eq null)
None
else
- Some((query: String) ⇒ Some(handlerInitializer(pathSegment)))
+ Some((query: String) /* the query string is ignored */ ⇒ Some(handlerInitializer(pathSegment)))
+
+ def query(handlerInitializer: (String) ⇒ Handler): URIMatcher =
+ (pathSegment: String) ⇒
+ if (pathSegment ne null)
+ None
+ else
+ Some((query: String) ⇒ Some(handlerInitializer(query)))
}
}
diff --git a/core/src/main/scala/org/dorest/server/utils/URIUtils.scala b/core/src/main/scala/org/dorest/server/utils/URIUtils.scala
index 0356944..3bab997 100644
--- a/core/src/main/scala/org/dorest/server/utils/URIUtils.scala
+++ b/core/src/main/scala/org/dorest/server/utils/URIUtils.scala
@@ -22,7 +22,7 @@ import java.nio.charset.Charset
*/
object URIUtils {
- def decodeRawURLQueryString(query: String): Option[Map[String, List[Option[String]]]] = {
+ def decodeRawURLQueryString(query: String): Map[String, List[String]] = {
decodeRawURLQueryString(query, scala.io.Codec.UTF8)
}
@@ -34,26 +34,25 @@ object URIUtils {
* @param charset the charset that has to be used to decode the string.
* @todo check that all special cases (and in particular URLs that are tampered with) do not cause any unexpected behavior
*/
- def decodeRawURLQueryString(query: String, charset: Charset): Option[Map[String, List[Option[String]]]] = {
- if ((query eq null) || (query.length == 0))
- return None
+ def decodeRawURLQueryString(query: String, charset: Charset): Map[String, List[String]] = {
+ var param_values = Map[String, List[String]]().withDefaultValue(List[String]())
- try {
- var param_values = Map[String, List[Option[String]]]().withDefaultValue(List[Option[String]]())
+ if ((query eq null) || (query.length == 0))
+ param_values
+ else {
for (param_value ← query.split('&')) {
val index = param_value.indexOf('=')
if (index == -1) {
+ // there is only a key...
val param = decodePercentEncodedString(param_value, charset)
- param_values = param_values.updated(param, param_values(param) :+ None)
+ param_values = param_values.updated(param, param_values(param))
} else {
val param = decodePercentEncodedString(param_value.substring(0, index), charset)
val value = decodePercentEncodedString(param_value.substring(index + 1), charset)
- param_values = param_values.updated(param, param_values(param) :+ Some(value))
+ param_values = param_values.updated(param, param_values(param) :+ value)
}
}
- Some(param_values)
- } catch {
- case _ ⇒ None
+ param_values
}
}
diff --git a/core/src/test/scala/org/dorest/server/ResponseHeadersTest.scala b/core/src/test/scala/org/dorest/server/ResponseHeadersTest.scala
new file mode 100644
index 0000000..d833319
--- /dev/null
+++ b/core/src/test/scala/org/dorest/server/ResponseHeadersTest.scala
@@ -0,0 +1,55 @@
+/*
+ Copyright 2012 Michael Eichberg et al
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+package org.dorest.server
+package rest
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import org.scalatest.FlatSpec
+import org.scalatest.matchers.ShouldMatchers
+import org.scalatest.BeforeAndAfterEach
+
+/**
+ * Tests the [[org.dorest.server.ResponseHeaders]] trait.
+ *
+ * @author Michael Eichberg
+ */
+@RunWith(classOf[JUnitRunner])
+class ResponseHeadersTest extends FlatSpec with ShouldMatchers with BeforeAndAfterEach {
+
+ private var responseHeaders: DefaultResponseHeaders = _
+
+ override def beforeEach {
+ responseHeaders = DefaultResponseHeaders()
+ }
+
+ "setAcceptRanges" should "set the Accept-Ranges response header" in {
+ responseHeaders.setAcceptRanges("bytes")("accept-ranges") should be ("bytes")
+ }
+
+ "setAge" should "set the Age response header" in {
+ responseHeaders.setAge(1000l)("age") should be ("1000")
+ }
+
+ "setAllow" should "set the Allow response header" in {
+ responseHeaders.setAllow(GET,OPTIONS)("allow") should be ("GET, OPTIONS")
+ }
+
+ "setContentLanguage" should "set the Content-Language response header" in {
+ import java.util.Locale._
+ responseHeaders.setContentLanguage(GERMAN,UK)("content-language") should be ("de, en-GB")
+ }
+}
\ No newline at end of file
diff --git a/core/src/test/scala/org/dorest/server/rest/URIsMatcherTest.scala b/core/src/test/scala/org/dorest/server/rest/URIsMatcherTest.scala
index a527675..a703d48 100644
--- a/core/src/test/scala/org/dorest/server/rest/URIsMatcherTest.scala
+++ b/core/src/test/scala/org/dorest/server/rest/URIsMatcherTest.scala
@@ -21,18 +21,17 @@ import org.scalatest.junit.JUnitRunner
import org.scalatest.FlatSpec
import org.scalatest.matchers.ShouldMatchers
-/** Tests the matching of URIs.
- *
- * @author Michael Eichberg
- */
+/**
+ * Tests the matching of URIs.
+ *
+ * @author Michael Eichberg
+ */
@RunWith(classOf[JUnitRunner])
class URIsMatcherTest extends FlatSpec with ShouldMatchers {
// just some dummy handlers
class DummyHandler extends Handler {
- def processRequest(requestBody: ⇒ java.io.InputStream): Response = {
- null
- }
+ def processRequest(requestBody: ⇒ java.io.InputStream): Response = null
}
object AHandler extends DummyHandler
object BHandler extends DummyHandler
@@ -43,16 +42,20 @@ class URIsMatcherTest extends FlatSpec with ShouldMatchers {
object GHandler extends DummyHandler
case class LongHandler(l: Long) extends DummyHandler
- case class PathHandler(p: String) extends DummyHandler
+ case class StringHandler(p: String) extends DummyHandler
+ case class OptionalLongHandler(l: Option[Long]) extends DummyHandler
+ case class MultiHandler(l: Option[Long], s: String) extends DummyHandler
// some URIMatcher instance
- val URIMatcher = new URIsMatcher {
+ val URIsMatcher = new URIsMatcher {
- def register(handlerFactory: HandlerFactory) { throw new Error() }
+ var factories = List[HandlerFactory]()
+
+ def register(handlerFactory: HandlerFactory) { factories = factories :+ handlerFactory }
}
- import URIMatcher._
+ import URIsMatcher._
val exhaustiveMatcher = / {
case "" ⇒ AHandler
@@ -66,11 +69,11 @@ class URIsMatcherTest extends FlatSpec with ShouldMatchers {
case "comments" ⇒ GHandler
}
}
- case "static" ⇒ (path) ⇒ Some((query) ⇒ Some(PathHandler(path)))
- case "sub" ⇒ bind path (PathHandler)
+ case "static" ⇒ (path) ⇒ Some((query) ⇒ Some(StringHandler(path)))
+ case "sub" ⇒ bind path (StringHandler) // same as above
}
- "A RESTURIsMatcher" should "correctly match valid URIs" in {
+ "A PathMatcher" should "correctly match valid URIs" in {
exhaustiveMatcher("/").get(null) should be(Some(AHandler))
exhaustiveMatcher("/lectures").get(null) should be(Some(BHandler))
exhaustiveMatcher("/users").get(null) should be(Some(CHandler))
@@ -78,10 +81,10 @@ class URIsMatcherTest extends FlatSpec with ShouldMatchers {
exhaustiveMatcher("/users/121212").get(null) should be(Some(LongHandler(121212)))
exhaustiveMatcher("/users/23233321212/").get(null) should be(Some(FHandler))
exhaustiveMatcher("/users/23233321212/comments").get(null) should be(Some(GHandler))
- exhaustiveMatcher("/static/").get(null) should be(Some(PathHandler("/")))
- exhaustiveMatcher("/static/index.html").get(null) should be(Some(PathHandler("/index.html")))
- exhaustiveMatcher("/static").get(null) should be(Some(PathHandler(null)))
- exhaustiveMatcher("/sub/index.html").get(null) should be(Some(PathHandler("/index.html")))
+ exhaustiveMatcher("/static/").get(null) should be(Some(StringHandler("/")))
+ exhaustiveMatcher("/static/index.html").get(null) should be(Some(StringHandler("/index.html")))
+ exhaustiveMatcher("/static").get(null) should be(Some(StringHandler(null)))
+ exhaustiveMatcher("/sub/index.html").get(null) should be(Some(StringHandler("/index.html")))
}
it should "handle URIs that do not match without throwing exceptions" in {
@@ -96,4 +99,43 @@ class URIsMatcherTest extends FlatSpec with ShouldMatchers {
exhaustiveMatcher("/lectures/slides") should be(None)
exhaustiveMatcher("/users/23233321212/comments/2323") should be(None)
}
+
+ val SID = QueryMatcher(STRING("id"))
+ val LID = QueryMatcher(LONG("id"))
+ val MULTI = QueryMatcher(LONG("id"), STRING("search"))
+
+ /*
+ * Intended Semantics:
+ * If I match a query string, I want to specify which parameters are required and which are optional (default)
+ * If all required parameters can be matched, the query matcher succeeds.
+ */
+ val matcherWithQueryStrings = / {
+ case "lectures" ⇒ ? { case SID(Some(id)) ⇒ StringHandler(id) }
+ case "slides" ⇒ ? { case LID(id) ⇒ OptionalLongHandler(id) }
+ case "users" ⇒ ? { case MULTI(id, Some(search)) ⇒ MultiHandler(id, search) }
+ }
+
+ "A QueryMatcher" should "correctly extract a single required query parameter" in {
+ matcherWithQueryStrings("/lectures").get("id=Yes") should be(Some(StringHandler("Yes")))
+ matcherWithQueryStrings("/lectures").get("foo=3434&id=2323") should be(Some(StringHandler("2323")))
+ matcherWithQueryStrings("/lectures").get("") should be(None)
+ matcherWithQueryStrings("/lectures").get(null) should be(None)
+ }
+
+ it should "correctly extract an optional query parameter" in {
+ matcherWithQueryStrings("/slides").get("") should be(Some(OptionalLongHandler(None)))
+ matcherWithQueryStrings("/slides").get(null) should be(Some(OptionalLongHandler(None)))
+ matcherWithQueryStrings("/slides").get("id=sdfsdf") should be(Some(OptionalLongHandler(None)))
+ matcherWithQueryStrings("/slides").get("id=121") should be(Some(OptionalLongHandler(Some(121l))))
+ matcherWithQueryStrings("/slides").get("id=absct&id=121") should be(Some(OptionalLongHandler(Some(121l))))
+ }
+
+ it should "correctly extract many query parameters" in {
+ matcherWithQueryStrings("/users").get("") should be(None)
+ matcherWithQueryStrings("/users").get(null) should be(None)
+ matcherWithQueryStrings("/users").get("search=sdfsdf") should be(Some(MultiHandler(None, "sdfsdf")))
+ matcherWithQueryStrings("/users").get("id=121") should be(None)
+ matcherWithQueryStrings("/users").get("id=121&search=abc") should be(Some(MultiHandler(Some(121), "abc")))
+ }
+
}
\ No newline at end of file
diff --git a/core/src/test/scala/org/dorest/server/utils/URIUtilsTest.scala b/core/src/test/scala/org/dorest/server/utils/URIUtilsTest.scala
index 3fe8b2a..5e904c5 100644
--- a/core/src/test/scala/org/dorest/server/utils/URIUtilsTest.scala
+++ b/core/src/test/scala/org/dorest/server/utils/URIUtilsTest.scala
@@ -19,30 +19,35 @@ import org.scalatest.junit.JUnitSuite
import org.junit.Assert._
import org.junit.Test
+/**
+ * Tests the decoding of URL query strings.
+ *
+ * @author Michael Eichberg
+ */
class URIUtilsTest extends JUnitSuite {
import URIUtils._
@Test def testDecodePercentEncodedString() {
- assert( decodePercentEncodedString("a+b%20c",scala.io.Codec.UTF8) === "a b c" )
+ assert(decodePercentEncodedString("a+b%20c", scala.io.Codec.UTF8) === "a b c")
- intercept[IllegalArgumentException]{
- decodePercentEncodedString("a+b%2",scala.io.Codec.UTF8)
+ intercept[IllegalArgumentException] {
+ decodePercentEncodedString("a+b%2", scala.io.Codec.UTF8)
}
}
@Test def testdecodeRawURLQueryString() {
- assert( decodeRawURLQueryString("") === None )
+ assert(decodeRawURLQueryString("") === Map())
- assert( decodeRawURLQueryString("foo") === Some(Map("foo" -> List(None))))
- assert( decodeRawURLQueryString("foo&bar") === Some(Map("foo" -> List(None), "bar" -> List(None))))
+ assert(decodeRawURLQueryString("foo") === Map("foo" -> List()))
+ assert(decodeRawURLQueryString("foo&bar") === Map("foo" -> List(), "bar" -> List()))
- assert( decodeRawURLQueryString("=foo") === Some(Map("" -> List(Some("foo")))))
- assert( decodeRawURLQueryString("=foo&=bar") === Some(Map("" -> List(Some("foo"),Some("bar")))))
- assert( decodeRawURLQueryString("=foo&=bar&=") === Some(Map("" -> List(Some("foo"),Some("bar"),Some("")))))
+ assert(decodeRawURLQueryString("=foo") === Map("" -> List("foo")))
+ assert(decodeRawURLQueryString("=foo&=bar") === Map("" -> List("foo", "bar")))
+ assert(decodeRawURLQueryString("=foo&=bar&=") === Map("" -> List("foo", "bar", "")))
- assert( decodeRawURLQueryString("start=1&end=2&search=\"%20+++\"") === Some(Map("start" -> List(Some("1")),"end" -> List(Some("2")),"search" -> List(Some("\" \"")))))
+ assert(decodeRawURLQueryString("start=1&end=2&search=\"%20+++\"") === Map("start" -> List("1"), "end" -> List("2"), "search" -> List("\" \"")))
}
}
\ No newline at end of file
diff --git a/demo/HelloWorld/src/main/scala/helloworld/HelloWorldServer.scala b/demo/HelloWorld/src/main/scala/helloworld/HelloWorldServer.scala
index 714f788..7637b04 100644
--- a/demo/HelloWorld/src/main/scala/helloworld/HelloWorldServer.scala
+++ b/demo/HelloWorld/src/main/scala/helloworld/HelloWorldServer.scala
@@ -16,24 +16,19 @@
package helloworld
import org.dorest.server.jdk.JDKServer
-import org.dorest.server.rest.RESTInterface
-import org.dorest.server.rest.TEXTSupport
+import org.dorest.server.rest.TEXTResource
object HelloWorldServer extends JDKServer(9010) with App {
- addPathMatcher(
+ addURIMatcher(
/ {
- case "hello" ⇒ new RESTInterface with TEXTSupport {
+ case "hello" ⇒ new TEXTResource {
get returns TEXT { "Hello World!" }
}
case "echo" ⇒ / {
- case MATCHED() ⇒ new RESTInterface with TEXTSupport {
- get returns TEXT { "This is the echo service." }
- }
- case STRING(text) ⇒ new RESTInterface with TEXTSupport {
- get returns TEXT { text }
- }
+ case MATCHED() ⇒ new TEXTResource { get returns TEXT { "This is the echo service." } }
+ case STRING(text) ⇒ new TEXTResource { get returns TEXT { text } }
}
}
)
diff --git a/demo/iSmallNotes/src/main/scala/de/smallnotes/SmallNotesApplication.scala b/demo/iSmallNotes/src/main/scala/de/smallnotes/SmallNotesApplication.scala
index 5b837f9..65f11db 100644
--- a/demo/iSmallNotes/src/main/scala/de/smallnotes/SmallNotesApplication.scala
+++ b/demo/iSmallNotes/src/main/scala/de/smallnotes/SmallNotesApplication.scala
@@ -15,7 +15,7 @@ object SmallNotesApplication extends JDKServer(8182) with App {
"src/main/resources/webapp"
}
- this addPathMatcher (
+ this addURIMatcher (
/ {
case "api" ⇒ / {
case "tags" ⇒ / {
diff --git a/ext/basicauth/src/main/scala/org/dorest/server/auth/Demo.scala b/ext/basicauth/src/main/scala/org/dorest/server/auth/Demo.scala
index 657254d..a9a3706 100644
--- a/ext/basicauth/src/main/scala/org/dorest/server/auth/Demo.scala
+++ b/ext/basicauth/src/main/scala/org/dorest/server/auth/Demo.scala
@@ -99,7 +99,7 @@ object Demo
val userHomeDir = System.getProperty("user.home")
- addPathMatcher(
+ addURIMatcher(
/ {
case "time" ⇒ new Time
case "info" ⇒ / {
diff --git a/ext/digestauth/src/main/scala/org/dorest/server/auth/DigestAuthentication.scala b/ext/digestauth/src/main/scala/org/dorest/server/auth/DigestAuthentication.scala
index 09d1b33..9e644bf 100644
--- a/ext/digestauth/src/main/scala/org/dorest/server/auth/DigestAuthentication.scala
+++ b/ext/digestauth/src/main/scala/org/dorest/server/auth/DigestAuthentication.scala
@@ -19,10 +19,11 @@ package auth
import java.io.InputStream
import StringUtils._
-/** Implementation of Digest Access Authentication (RFC 2617).
- *
- * @author Mateusz Parzonka
- */
+/**
+ * Implementation of Digest Access Authentication (RFC 2617).
+ *
+ * @author Mateusz Parzonka
+ */
trait DigestAuthentication extends Authentication with Handler {
private[this] var _authenticatedUser: String = _
@@ -60,7 +61,7 @@ trait DigestAuthentication extends Authentication with Handler {
if (nameValuePairs.length == nameValueMappings.size)
nameValueMappings
else
- throw new RequestException(response = BadRequest("Malformed authorization header: "+authorizationHeader))
+ throw new RequestException(response = BadRequest("Malformed authorization header: " + authorizationHeader))
}
def unauthorizedDigestResponse(stale: Boolean): Response = {
@@ -72,9 +73,9 @@ trait DigestAuthentication extends Authentication with Handler {
def validate(r: AuthorizationRequest): ProcessedAuthorizationRequest = {
password(r.username) match {
case Some(pwd: String) ⇒ {
- val ha1 = hexEncode(md5(r.username+":"+r.realm+":"+pwd))
- val ha2 = hexEncode(md5(r.method+":"+r.uri))
- val response = hexEncode(md5(ha1+":"+r.nonce+":"+r.nc+":"+r.cnonce+":"+r.qop+":"+ha2))
+ val ha1 = hexEncode(md5(r.username + ":" + r.realm + ":" + pwd))
+ val ha2 = hexEncode(md5(r.method + ":" + r.uri))
+ val response = hexEncode(md5(ha1 + ":" + r.nonce + ":" + r.nc + ":" + r.cnonce + ":" + r.qop + ":" + ha2))
(response == r.response) match {
case true if (NonceStorage.contains(r.nonce, r.nc)) ⇒ ValidatedRequest
case true ⇒ StaleRequest
@@ -86,10 +87,11 @@ trait DigestAuthentication extends Authentication with Handler {
}
}
-/** Thread-safe nonce-storage with background-deletion of expired nonces.
- *
- * @author Mateusz Parzonka
- */
+/**
+ * Thread-safe nonce-storage with background-deletion of expired nonces.
+ *
+ * @author Mateusz Parzonka
+ */
object NonceStorage {
import scala.collection.JavaConversions._
@@ -103,19 +105,30 @@ object NonceStorage {
nonceMap += (nonce -> (0, System.currentTimeMillis))
}
- /** Checks if the storage contains the given nonce assuring the given nc was not used before.
- */
+ /**
+ * Checks if the storage contains the given nonce assuring the given nc was not used before.
+ */
def contains(nonce: String, nc: String): Boolean = {
- val curNc = Integer.parseInt(nc, 16)
- nonceMap.get(nonce) match {
- case Some((oldNc: Int, time: Long)) if curNc > oldNc ⇒ { nonceMap.replace(nonce, (curNc, time)); true }
- case _ ⇒ false
+ try {
+ val curNc = Integer.parseInt(nc, 16)
+ nonceMap.get(nonce) match {
+ case Some(old @ (oldNc: Int, time: Long)) if curNc > oldNc ⇒ { nonceMap.replace(nonce, old, (curNc, time)); true }
+ case _ ⇒ false
+ }
+ } catch {
+ case e: NumberFormatException ⇒ {
+ // FIXME Log this exception! I currently assume that either the client is not working correctly or someone is trying to bring this service down.
+ e.printStackTrace()
+ return false;
+ }
}
}
def clean() {
val currentTime = System.currentTimeMillis
- for ((nonce, (nc, time)) ← nonceMap if (currentTime - time) > nonceValidityPeriod) nonceMap.-=(nonce)
+ for ((nonce, (nc, time)) ← nonceMap if (currentTime - time) > nonceValidityPeriod) {
+ nonceMap.-=(nonce)
+ }
}
val nonceCleaner = new Thread() {
diff --git a/ext/digestauth/src/test/scala/org/dorest/server/auth/DigestAuthenticationTest.scala b/ext/digestauth/src/test/scala/org/dorest/server/auth/DigestAuthenticationTest.scala
index 18bf401..0c35d98 100644
--- a/ext/digestauth/src/test/scala/org/dorest/server/auth/DigestAuthenticationTest.scala
+++ b/ext/digestauth/src/test/scala/org/dorest/server/auth/DigestAuthenticationTest.scala
@@ -41,7 +41,7 @@ import scala.xml.{ XML, Utility }
*/
object DigestAuthTestServer extends JDKServer(9999) {
- addPathMatcher(
+ addURIMatcher(
/ {
case ROOT() ⇒ new RESTInterface with XMLSupport {
get returns XML { "Hello!" }
diff --git a/ext/gson/src/main/scala/org/dorest/server/rest/representation/gson/Demo.scala b/ext/gson/src/main/scala/org/dorest/server/rest/representation/gson/Demo.scala
index 5fe1337..386e27d 100644
--- a/ext/gson/src/main/scala/org/dorest/server/rest/representation/gson/Demo.scala
+++ b/ext/gson/src/main/scala/org/dorest/server/rest/representation/gson/Demo.scala
@@ -63,7 +63,7 @@ class Demo
object Demo extends JDKServer(9000) with App {
- addPathMatcher(
+ addURIMatcher(
(path) ⇒
if ("/demos" == path) {
Some((query: String) ⇒ Some(new DemosResource))
diff --git a/ext/multipart/src/test/scala/org/dorest/server/rest/representation/multipart/MultipartSupportTest.scala b/ext/multipart/src/test/scala/org/dorest/server/rest/representation/multipart/MultipartSupportTest.scala
index 156c7ab..d3bceac 100644
--- a/ext/multipart/src/test/scala/org/dorest/server/rest/representation/multipart/MultipartSupportTest.scala
+++ b/ext/multipart/src/test/scala/org/dorest/server/rest/representation/multipart/MultipartSupportTest.scala
@@ -40,7 +40,7 @@ object MultipartSupportTestServer extends JDKServer(9998) {
import org.apache.commons.io.{ IOUtils, FileUtils }
- this addPathMatcher ((path) ⇒ if ("/upload" == path) Some((query) => Some(new UploadResource)) else None)
+ this addURIMatcher ((path) ⇒ if ("/upload" == path) Some((query) => Some(new UploadResource)) else None)
start()
diff --git a/ext/orgjson/src/main/scala/org/dorest/server/rest/representation/orgjson/Demo.scala b/ext/orgjson/src/main/scala/org/dorest/server/rest/representation/orgjson/Demo.scala
index a0d751f..bc86707 100644
--- a/ext/orgjson/src/main/scala/org/dorest/server/rest/representation/orgjson/Demo.scala
+++ b/ext/orgjson/src/main/scala/org/dorest/server/rest/representation/orgjson/Demo.scala
@@ -98,7 +98,7 @@ class Demo
object Demo extends JDKServer(9000) with App {
- addPathMatcher(
+ addURIMatcher(
/ {
case "echo" ⇒ new Echo()
case "time" ⇒ new Time() with PerformanceMonitor with ConsoleLogging
diff --git a/ext/stream/src/test/scala/org/dorest/server/rest/representation/stream/StreamSupportTest.scala b/ext/stream/src/test/scala/org/dorest/server/rest/representation/stream/StreamSupportTest.scala
index a752be6..8ca79b1 100644
--- a/ext/stream/src/test/scala/org/dorest/server/rest/representation/stream/StreamSupportTest.scala
+++ b/ext/stream/src/test/scala/org/dorest/server/rest/representation/stream/StreamSupportTest.scala
@@ -24,7 +24,7 @@ import org.apache.commons.io.{ IOUtils, FileUtils }
*/
object StreamSupportTestServer extends JDKServer(9999) {
- addPathMatcher(
+ addURIMatcher(
/ {
case "bytestream" ⇒ / {
case MATCHED() ⇒ new ByteStreamResource