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