├── .gitignore
├── LICENSE
├── README.md
├── core
├── .ensime
└── src
│ ├── main
│ └── scala
│ │ └── org
│ │ └── scalatra
│ │ └── ssgi
│ │ ├── Application.scala
│ │ ├── Cookie.scala
│ │ ├── HttpMethod.scala
│ │ ├── Implicits.scala
│ │ ├── Logging.scala
│ │ ├── MimeUtil.scala
│ │ ├── Renderable.scala
│ │ ├── Request.scala
│ │ ├── Response.scala
│ │ ├── ResponseBuilder.scala
│ │ ├── package.scala
│ │ └── util
│ │ └── RicherString.scala
│ └── test
│ └── scala
│ └── org
│ └── scalatra
│ └── ssgi
│ ├── CookieSpec.scala
│ ├── HttpMethodSpec.scala
│ ├── MimeUtilSpec.scala
│ └── RenderableSpec.scala
├── examples
└── servlet
│ └── src
│ └── main
│ ├── scala
│ └── org
│ │ └── scalatra
│ │ └── ssgi
│ │ └── examples
│ │ └── servlet
│ │ └── HelloWorldApp.scala
│ └── webapp
│ └── WEB-INF
│ └── web.xml
├── project
├── build.properties
└── build
│ └── SsgiProject.scala
└── servlet
├── .ensime
└── src
├── main
└── scala
│ └── org
│ └── scalatra
│ └── ssgi
│ └── servlet
│ ├── ByteArrayServletOutputStream.scala
│ ├── ServletRequest.scala
│ ├── SsgiServlet.scala
│ ├── SsgiServletResponse.scala
│ └── testing
│ ├── MockRequestDispatcher.scala
│ └── MockServletContext.scala
└── test
└── scala
└── org
└── scalatra
└── ssgi
└── servlet
├── ServletRequestSpec.scala
└── SsgiServletResponseSpec.scala
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | lib_managed/
3 | src_managed/
4 | project/boot/
5 | *.iml
6 | .idea/
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Scalatra is distributed under the following terms:
2 |
3 | Copyright (c) 2010, The Scalatra Project [http://scalatra.org/]
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions
8 | are met:
9 |
10 | 1. Redistributions of source code must retain the above copyright
11 | notice, this list of conditions and the following disclaimer.
12 |
13 | 2. Redistributions in binary form must reproduce the above copyright
14 | notice, this list of conditions and the following disclaimer in the
15 | documentation and/or other materials provided with the distribution.
16 |
17 | THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 | ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27 | SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SSGI: the Scala Server Gateway Interface
2 |
3 | SSGI is a low-level API for developing web applications and web frameworks in [Scala](http://www.scala-lang.org/).
4 |
5 | ## Similar frameworks
6 |
7 | SSGI is influenced by server gateways in several other languages:
8 |
9 | * Ruby's [Rack](http://rack.rubyforge.org/)
10 | * Python's [WSGI](http://www.python.org/dev/peps/pep-333/)
11 | * Perl's [PSGI/Plack](http://plackperl.org/)
12 | * JavaScript's [JSGI/Jack](http://jackjs.org/)
13 | * Clojure's [Ring](http://github.com/mmcgrana/ring)
14 |
15 | ## Infrequently Asked Questions
16 |
17 | ### Why not just use the Java Servlet API?
18 |
19 | As a JVM language, the [Java Servlet API](http://www.oracle.com/technetwork/java/index-jsp-135475.html) is a viable option for Scala web development. However, we do not find that the Servlet API is suitable for _idiomatic_ Scala development, with its mutable variables, null-returning methods, and archaic collection types. SSGI lets you deploy to servlet containers without coupling your app to the Servlet specification.
20 |
21 | ### How is this project related to Scalatra?
22 |
23 | This project was initially conceived by the [Scalatra](http://scalatra.org/) development team. It may in the future be used to break Scalatra's hard dependency on the Servlet API. It may flourish as a separate project without ever being integrated into Scalatra. It may prove to be a horrendous idea and be left to rot on GitHub. Only time will tell.
24 |
--------------------------------------------------------------------------------
/core/.ensime:
--------------------------------------------------------------------------------
1 | ;; This config was generated using ensime-config-gen. Feel free to customize its contents manually.
2 |
3 | (
4 |
5 | :server-root "~/.emacs.d/ensime"
6 |
7 | :project-package "org.scalatra.ssgi"
8 |
9 | :use-sbt t
10 |
11 | :compile-jars ("./lib_managed/scala_2.8.0/compile/" "../project/boot/scala-2.8.0/lib/")
12 |
13 |
14 | )
15 |
16 |
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalatra/ssgi/Application.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | /**
4 | * An application is a function that takes exactly one argument, a request, and returns a response.
5 | */
6 | trait Application extends (Request => Response[_])
7 |
8 | object Application {
9 | implicit def apply(f: Request => Response[_]) = new Application { def apply(req: Request) = f(req) }
10 | }
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalatra/ssgi/Cookie.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | import java.util.Locale
4 | import java.net.URLEncoder
5 |
6 | case class CookieOptions(
7 | domain : String = "",
8 | path : String = "",
9 | maxAge : Int = -1,
10 | secure : Boolean = false,
11 | comment : String = "",
12 | version: Int = 1,
13 | httpOnly: Boolean = false,
14 | encoding: String = "UTF-8" )
15 |
16 | case class Cookie(name: String, value: String)(implicit cookieOptions: CookieOptions = CookieOptions()) {
17 |
18 | import Implicits._
19 |
20 | def toCookieString = {
21 | val encoding = if(cookieOptions.encoding.isNonBlank) cookieOptions.encoding else "UTF-8"
22 | val sb = new StringBuffer
23 | sb append URLEncoder.encode(name, encoding) append "="
24 | sb append URLEncoder.encode(value, encoding)
25 |
26 | if(cookieOptions.domain.isNonBlank) sb.append("; Domain=").append(
27 | (if (!cookieOptions.domain.startsWith(".")) "." + cookieOptions.domain else cookieOptions.domain).toLowerCase(Locale.ENGLISH)
28 | )
29 |
30 | val pth = cookieOptions.path
31 | if(pth.isNonBlank) sb append "; Path=" append (if(!pth.startsWith("/")) {
32 | "/" + pth
33 | } else { pth })
34 |
35 | if(cookieOptions.comment.isNonBlank) sb append ("; Comment=") append cookieOptions.comment
36 |
37 | if(cookieOptions.maxAge > -1) sb append "; Max-Age=" append cookieOptions.maxAge
38 |
39 | if (cookieOptions.secure) sb append "; Secure"
40 | if (cookieOptions.httpOnly) sb append "; HttpOnly"
41 | if (cookieOptions.version > 0) sb append "; Version=" append cookieOptions.version
42 | sb.toString
43 | }
44 | }
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalatra/ssgi/HttpMethod.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | import java.util.Locale
4 |
5 | sealed trait HttpMethod {
6 | val isSafe: Boolean
7 | }
8 | case object Options extends HttpMethod {
9 | val isSafe = true
10 | override def toString = "OPTIONS"
11 | }
12 | case object Get extends HttpMethod {
13 | val isSafe = true
14 | override def toString = "GET"
15 | }
16 | case object Head extends HttpMethod {
17 | val isSafe = true
18 | override def toString = "HEAD"
19 | }
20 | case object Post extends HttpMethod {
21 | val isSafe = false
22 | override def toString = "POST"
23 | }
24 | case object Put extends HttpMethod {
25 | val isSafe = false
26 | override def toString = "PUT"
27 | }
28 | case object Delete extends HttpMethod {
29 | val isSafe = false
30 | override def toString = "DELETE"
31 | }
32 | case object Trace extends HttpMethod {
33 | val isSafe = true
34 | override def toString = "TRACE"
35 | }
36 | case object Connect extends HttpMethod {
37 | val isSafe = false
38 | override def toString = "CONNECT"
39 | }
40 | case class ExtensionMethod(name: String) extends HttpMethod {
41 | val isSafe = false
42 | }
43 |
44 | object HttpMethod {
45 | private val methodMap =
46 | Map(List(Options, Get, Head, Post, Put, Delete, Trace, Connect) map {
47 | method => (method.toString, method)
48 | } : _*)
49 |
50 | def apply(name: String): HttpMethod = {
51 | val canonicalName = name.toUpperCase(Locale.ENGLISH)
52 | methodMap.getOrElse(canonicalName, ExtensionMethod(canonicalName))
53 | }
54 |
55 | val methods: Set[HttpMethod] = methodMap.values.toSet
56 | }
57 |
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalatra/ssgi/Implicits.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | import util.RicherString
4 |
5 | object Implicits {
6 | implicit def string2RicherString(orig: String) = new RicherString(orig)
7 | }
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalatra/ssgi/Logging.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | import org.slf4j.{LoggerFactory, Logger}
4 | import java.io.{StringWriter, PrintWriter}
5 | import java.net.{InetAddress, UnknownHostException}
6 |
7 | trait Logging {
8 | @transient @volatile protected[scalatra] var log = LoggerFactory.getLogger(getClass)
9 | }
10 |
11 | /**
12 | * LoggableException is a subclass of Exception and can be used as the base exception
13 | * for application specific exceptions.
14 | *
15 | * It keeps track of the exception is logged or not and also stores the unique id,
16 | * so that it can be carried all along to the client tier and displayed to the end user.
17 | * The end user can call up the customer support using this number.
18 | *
19 | * @author Jonas Bonér
20 | */
21 | class LoggableException extends Exception with Logging {
22 | private val uniqueId = getExceptionID
23 | private var originalException: Option[Exception] = None
24 | private var isLogged = false
25 |
26 | def this(baseException: Exception) = {
27 | this()
28 | originalException = Some(baseException)
29 | }
30 |
31 | def logException = synchronized {
32 | if (!isLogged) {
33 | originalException match {
34 | case Some(e) => log.error("Logged Exception [%s] %s", uniqueId, getStackTraceAsString(e))
35 | case None => log.error("Logged Exception [%s] %s", uniqueId, getStackTraceAsString(this))
36 | }
37 | isLogged = true
38 | }
39 | }
40 |
41 | private def getExceptionID: String = {
42 | val hostname: String = try {
43 | InetAddress.getLocalHost.getHostName
44 | } catch {
45 | case e: UnknownHostException =>
46 | log.error("Could not get hostname to generate loggable exception")
47 | "N/A"
48 | }
49 | hostname + "_" + System.currentTimeMillis
50 | }
51 |
52 | private def getStackTraceAsString(exception: Throwable): String = {
53 | val sw = new StringWriter
54 | val pw = new PrintWriter(sw)
55 | exception.printStackTrace(pw)
56 | sw.toString
57 | }
58 | }
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalatra/ssgi/MimeUtil.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | import eu.medsea.util.EncodingGuesser
4 | import eu.medsea.mimeutil.{MimeType, MimeUtil2}
5 | import collection.JavaConversions._
6 |
7 | /**
8 | * A utility to help with mime type detection for a given file path or url
9 | */
10 | object MimeUtil extends Logging {
11 |
12 | val DEFAULT_MIME = "application/octet-stream"
13 |
14 | private val mimeUtil = new MimeUtil2()
15 | quiet { mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector") }
16 | quiet { mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.ExtensionMimeDetector") }
17 | registerEncodingsIfNotSet
18 |
19 | /**
20 | * Sets supported encodings for the mime-util library if they have not been
21 | * set. Since the supported encodings is stored as a static Set we
22 | * synchronize access.
23 | */
24 | private def registerEncodingsIfNotSet: Unit = synchronized {
25 | if (EncodingGuesser.getSupportedEncodings.size == 0) {
26 | val enc = Set("UTF-8", "ISO-8859-1", "windows-1252", EncodingGuesser.getDefaultEncoding)
27 | EncodingGuesser.setSupportedEncodings(enc)
28 | }
29 | }
30 |
31 | /**
32 | * Detects the mime type of a given file path.
33 | *
34 | * @param path The path for which to detect the mime type
35 | * @param fallback A fallback value in case no mime type can be found
36 | */
37 | def mimeType(path: String, fallback: String = DEFAULT_MIME) = {
38 | try {
39 | MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(path, new MimeType(fallback))).toString
40 | } catch {
41 | case e => {
42 | log.error("There was an error detecting the mime type", e)
43 | fallback
44 | }
45 | }
46 | }
47 |
48 |
49 | private def quiet(fn: => Unit) = {
50 | try { fn }
51 | catch { case e => log.error("An error occurred while registering a mime type detector", e) }
52 | }
53 | }
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalatra/ssgi/Renderable.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | import scala.xml.NodeSeq
4 | import java.nio.charset.Charset
5 | import annotation.tailrec
6 | import java.nio.ByteBuffer
7 | import java.io._
8 | import resource._
9 | import java.nio.channels.{Channels, WritableByteChannel, ReadableByteChannel}
10 |
11 | trait Renderable {
12 | def writeTo(out: OutputStream, charset: Charset): Unit
13 | }
14 |
15 | /**
16 | * Extension for character-based types that can be more efficiently rendered to a Writer.
17 | */
18 | trait CharRenderable extends Renderable {
19 | def writeTo(writer: Writer): Unit
20 | }
21 |
22 | trait InputStreamRenderable extends Renderable {
23 | def in: InputStream
24 |
25 | def writeTo(out: OutputStream, cs: Charset) {
26 | val in = this.in // it's a def, and we only want one
27 | streamCopy(Channels.newChannel(in), Channels.newChannel(out))
28 | in.close()
29 | }
30 |
31 | private def streamCopy(src: ReadableByteChannel, dest: WritableByteChannel) {
32 | val buffer = getByteBuffer(4 * 1024)
33 | @tailrec
34 | def loop() {
35 | if(src.read(buffer) > 0) {
36 | buffer.flip
37 | dest.write(buffer)
38 | buffer.compact
39 | loop
40 | }
41 | }
42 | loop
43 |
44 | buffer.flip
45 | while(buffer.hasRemaining) {
46 | dest.write(buffer)
47 | }
48 | }
49 |
50 | protected def getByteBuffer(size: Int) = ByteBuffer.allocate(size)
51 | }
52 |
53 | /**
54 | * Extension for types that exist as a File. This is useful primarily for zero-copy optimizations.
55 | */
56 | trait FileRenderable extends InputStreamRenderable {
57 | def file: File
58 |
59 | def in: InputStream = new FileInputStream(file)
60 |
61 | override protected def getByteBuffer(size: Int) = ByteBuffer.allocateDirect(size) // zero-copy byte buffer
62 | }
63 |
64 | object Renderable {
65 | implicit def byteTraversableToRenderable(bytes: Traversable[Byte]) = new Renderable {
66 | def writeTo(out: OutputStream, cs: Charset) { for (b <- bytes) out.write(b) }
67 | }
68 |
69 | implicit def byteArrayToRenderable(bytes: Array[Byte]) = new Renderable {
70 | def writeTo(out: OutputStream, cs: Charset) { out.write(bytes) }
71 | }
72 |
73 | implicit def stringToRenderable(string: String) = new CharRenderable {
74 | def writeTo(out: OutputStream, cs: Charset) { out.write(string.getBytes(cs)) }
75 | def writeTo(writer: Writer) { writer.write(string) }
76 | }
77 |
78 | implicit def nodeSeqToRenderable(nodeSeq: NodeSeq) = stringToRenderable(nodeSeq.toString)
79 |
80 | implicit def inputStreamToRenderable(input: InputStream) = new InputStreamRenderable { def in = input }
81 |
82 | implicit def fileToRenderable(theFile: File) = new FileRenderable { def file = theFile }
83 |
84 | implicit def writesToWriterToCharRenderable(writesToWriter: { def writeTo(writer: Writer): Unit }) =
85 | new CharRenderable {
86 | def writeTo(out: OutputStream, cs: Charset) {
87 | for (writer <- managed(new OutputStreamWriter(out, cs))) {
88 | writeTo(writer)
89 | }
90 | }
91 | def writeTo(writer: Writer) { writesToWriter.writeTo(writer) }
92 | }
93 |
94 | implicit def writesToStreamToRenderable(writesToStream: { def writeTo(out: OutputStream): Unit }) =
95 | new Renderable {
96 | def writeTo(out: OutputStream, cs: Charset) { writesToStream.writeTo(out) }
97 | }
98 | }
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalatra/ssgi/Request.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | import scala.collection.Map
4 | import scala.collection.mutable.{Map => MMap}
5 | import java.io.{PrintWriter, InputStream}
6 |
7 | /**
8 | * A representation of an HTTP request. Based on the Rack specification and Python WSGI (PEP 333).
9 | */
10 | trait Request {
11 | /**
12 | * The HTTP request method, such as GET or POST
13 | */
14 | def requestMethod: HttpMethod
15 |
16 | /**
17 | * The initial portion of the request URL's "path" that corresponds to the application object, so that the
18 | * application knows its virtual "location". This may be an empty string, if the application corresponds to
19 | * the "root" of the server.
20 | */
21 | def scriptName: String
22 |
23 | /**
24 | * The remainder of the request URL's "path", designating the virtual "location" of the request's target within
25 | * the application. This may be an empty string, if the request URL targets the application root and does not have
26 | * a trailing slash.
27 | */
28 | def pathInfo: String
29 |
30 | /**
31 | * The portion of the request URL that follows the ?, if any. May be empty, but is always required!
32 | */
33 | def queryString: String
34 |
35 | /**
36 | * The contents of any Content-Type fields in the HTTP request, or None if absent.
37 | */
38 | def contentType: Option[String]
39 |
40 | /**
41 | * The contents of any Content-Length fields in the HTTP request, or None if absent.
42 | */
43 | def contentLength: Option[Int]
44 |
45 | /**
46 | * When combined with scriptName, pathInfo, and serverPort, these variables can be used to complete the URL.
47 | * Note, however, that the "Host" header, if present, should be used in preference to serverName for reconstructing
48 | * the request URL.
49 | */
50 | def serverName: String
51 |
52 | /**
53 | * When combined with scriptName, pathInfo, and serverName, these variables can be used to complete the URL.
54 | * See serverName for more details.
55 | */
56 | def serverPort: Int
57 |
58 | /**
59 | * The version of the protocol the client used to send the request. Typically this will be something like "HTTP/1.0"
60 | * or "HTTP/1.1" and may be used by the application to determine how to treat any HTTP request headers.
61 | */
62 | def serverProtocol: String
63 |
64 | /**
65 | * A map corresponding to the client-supplied HTTP request headers.
66 | *
67 | * TODO If the header is absent from the request, should get return Some(Seq.empty) or None?
68 | */
69 | def headers: Map[String, Seq[String]]
70 |
71 | /**
72 | * A tuple of the major and minor version of SSGI.
73 | */
74 | def ssgiVersion: (Int, Int) = SsgiVersion
75 |
76 | /**
77 | * A string representing the "scheme" portion of the URL at which the application is being invoked. Normally, this
78 | * will have the value "http" or "https", as appropriate.
79 | */
80 | def scheme: String
81 |
82 | /**
83 | * An input stream from which the HTTP request body can be read. (The server or gateway may perform reads on-demand
84 | * as requested by the application, or it may pre-read the client's request body and buffer it in-memory or on disk,
85 | * or use any other technique for providing such an input stream, according to its preference.)
86 | *
87 | * It is the responsibility of the caller to close the input stream.
88 | */
89 | def inputStream: InputStream
90 |
91 | /**
92 | * A print writer to which error output can be written, for the purpose of recording program or other errors in a
93 | * standardized and possibly centralized location.
94 | */
95 | def errors: PrintWriter = new PrintWriter(System.err)
96 |
97 | /**
98 | * A map in which the server or application may store its own data.
99 | */
100 | def attributes: MMap[String, Any] = MMap.empty
101 |
102 | /**
103 | * A Map of the parameters of this request. Parameters are contained in the query string or posted form data.
104 | */
105 | def parameterMap: Map[String, Seq[String]]
106 | }
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalatra/ssgi/Response.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | /**
4 | * An HTTP response. Based on the Rack specification and Python WSGI (PEP-333).
5 | *
6 | * @param status The HTTP status. Must be greater than or equal to 100.
7 | *
8 | * @param headers The header must not contain a Status key, contain keys with : or newlines in their name, contain
9 | * key names that end in - or _, but only contain keys that consist of letters, digits, _ or - and start with a letter.
10 | * The values of the header must consist of lines (for multiple header values, e.g. multiple Set-Cookie values)
11 | * separated by "\n". The lines must not contain characters below 037. There must be a Content-Type, except when the
12 | * Status is 1xx, 204 or 304, in which case there must be none given. There must not be a Content-Length header when
13 | * the Status is 1xx, 204 or 304.
14 | *
15 | * @param body The response body. Should be transformed to a Traversable[Byte] before returning to the web server.
16 | */
17 | case class Response[+A](status: Int = 200, headers: Map[String, String] = Map.empty, body: A)
18 | (implicit renderer: A => Renderable) {
19 | require(Response.STATUS_CODES.contains(status), "The response status code is not valid")
20 | /**
21 | * Returns a response by applying a function to this response's body. The new response has the same status and
22 | * headers, and its body is the result of the function.
23 | */
24 | def map[B <% Renderable](f: A => B): Response[B] = copy(body = f(body))
25 |
26 | /**
27 | * Returns a new response by applying a function to this response's body.
28 | */
29 | def flatMap[B <% Renderable](f: A => Response[B]): Response[B] = f(body)
30 |
31 | def renderableBody: Renderable = renderer(body)
32 | }
33 |
34 | object Response {
35 | /**
36 | * An empty response with a status of OK.
37 | */
38 | val Ok: Response[Array[Byte]] = Response(body = Array.empty)
39 |
40 | val STATUS_CODES = List(100, 101, (200 to 206), (300 to 307), (400 to 417), (500 to 505)).flatten
41 | }
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalatra/ssgi/ResponseBuilder.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | /**
4 | * Builds a Response.
5 | */
6 | trait ResponseBuilder {
7 | import ResponseBuilder._
8 |
9 | /**
10 | * Returns the response.
11 | */
12 | def apply(): Response[_] = response
13 |
14 | protected var response: Response[_] = Response.Ok
15 |
16 | /**
17 | * Gets the status of the response
18 | */
19 | def status = response.status
20 |
21 | /**
22 | * Sets the status of the response
23 | *
24 | * @param statusCode the status code for this response
25 | */
26 | def status_=(statusCode: Int) = {
27 | response = response.copy(statusCode)
28 | }
29 |
30 | /**
31 | * Sets a header to the response. This replaces an existing header if it was there previously
32 | *
33 | * @param name the name of the header
34 | * @param value the value of the header
35 | */
36 | def setHeader(name: String, value: String): Unit =
37 | response = response.copy(headers = response.headers + (name -> value))
38 |
39 | /**
40 | * Sets the status code of the response to the code provided.
41 | * This'd better be a 4xx or 5xx status!
42 | *
43 | * @param code The numeric status code of the error
44 | */
45 | def sendError(code: Int): Unit = {
46 | if (code < 400) throw new Exception("This needs to be a status code > 400")
47 | status = code
48 | }
49 |
50 | /**
51 | * Sets the status code of the response to the code provided.
52 | * This'd better be a 4xx or 5xx status!
53 | *
54 | * @param code The numeric status code of the error
55 | * @param message The body of the error response
56 | */
57 | def sendError(code: Int, message: String): Unit = {
58 | sendError(code)
59 | body = message
60 | }
61 |
62 | /**
63 | * Gets the content type that belongs to this response
64 | */
65 | def contentType: String = response.headers.get("Content-Type") getOrElse DefaultContentType
66 |
67 | /**
68 | * Sets the content type of this response
69 | *
70 | * @param contentType The content type to use for this response
71 | */
72 | def contentType_=(contentType: String): Unit = setHeader("Content-Type", contentType)
73 |
74 | /**
75 | * Get the character encoding that belongs to this response
76 | */
77 | def characterEncoding: String = {
78 | val ct = contentType
79 | val start = ct.indexOf("charset=")
80 | if(start > 0){
81 | val encStart = start + 8
82 | val end = ct.indexOf(" ", encStart)
83 | if(end > 0) ct.substring(encStart, end)
84 | else ct.substring(encStart)
85 | } else {
86 | DefaultEncoding
87 | }
88 | }
89 |
90 | /**
91 | * Sets the character encoding of the response
92 | *
93 | * @param encoding The encoding to use for this response
94 | */
95 | def characterEncoding_=(encoding: String): Unit = {
96 | //TODO: make this more sensible? There might be more content in there than just a charset
97 | val newType = contentType.split(";").head + "; charset=" + encoding
98 | response = response.copy(headers = response.headers + ("Content-Type" -> newType))
99 | setHeader("Content-Type", newType)
100 | }
101 |
102 | /**
103 | * Sets the status to 302 and adds the location header for the redirect
104 | *
105 | * @param url the URL to redirect to
106 | */
107 | def sendRedirect(url: String): Unit = {
108 | val redirectHeaders = response.headers + ("Location" -> encodeRedirectUrl(url))
109 | response = response.copy(302, redirectHeaders)
110 | }
111 |
112 | protected def encodeRedirectUrl(url: String): String
113 |
114 | def body: Any = response.body
115 |
116 | // TODO SsgiServletResponse currently assumes a Response[Array[Byte]]. Undo that assumption.
117 | def body_=[A <% Renderable](body: A): Unit = response = response.copy(body = body)
118 | }
119 |
120 | object ResponseBuilder {
121 | val DefaultContentType: String = "text/plain"
122 | val DefaultEncoding: String = "utf-8"
123 | }
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalatra/ssgi/package.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra
2 |
3 | package object ssgi {
4 | val SsgiVersion: (Int, Int) = (0, 1)
5 |
6 | }
--------------------------------------------------------------------------------
/core/src/main/scala/org/scalatra/ssgi/util/RicherString.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi.util
2 |
3 | class RicherString(orig: String) {
4 | def isBlank = orig == null || orig.trim.isEmpty
5 | def isNonBlank = orig != null && !orig.trim.isEmpty
6 | }
7 |
--------------------------------------------------------------------------------
/core/src/test/scala/org/scalatra/ssgi/CookieSpec.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | import org.scalatest.matchers.MustMatchers
4 | import org.scalatest.{WordSpec}
5 |
6 | class CookieSpec extends WordSpec with MustMatchers {
7 |
8 | "a Cookie" should {
9 | "render a simple name value pair" in {
10 | val cookie = Cookie("theName", "theValue")
11 | cookie.toCookieString must startWith("theName=theValue")
12 | }
13 |
14 | "render a simple name value pair with a version" in {
15 | val cookie = Cookie("theName", "theValue")
16 | cookie.toCookieString must startWith("theName=theValue; Version=1")
17 | }
18 |
19 | "have a dot in front of the domain when set" in {
20 | val cookie = Cookie("cookiename", "value1")( CookieOptions(domain="nowhere.com"))
21 | cookie.toCookieString must startWith("cookiename=value1; Domain=.nowhere.com")
22 | }
23 |
24 | "prefix a path with / if a path is set" in {
25 | val cookie = Cookie("cookiename", "value1")( CookieOptions(path="path/to/resource"))
26 | cookie.toCookieString must startWith("cookiename=value1; Path=/path/to/resource")
27 | }
28 |
29 | "have a maxAge when the value is >= 0" in {
30 | val cookie = Cookie("cookiename", "value1")(CookieOptions(maxAge=86700))
31 | cookie.toCookieString must startWith("cookiename=value1; Max-Age=86700")
32 | }
33 |
34 | "set the comment when a comment is given" in {
35 | val cookie = Cookie("cookiename", "value1")(CookieOptions(comment="This is the comment"))
36 | cookie.toCookieString must startWith("cookiename=value1; Comment=This is the comment")
37 | }
38 |
39 | "flag the cookie as secure if needed" in {
40 | val cookie = Cookie("cookiename", "value1")(CookieOptions(secure = true))
41 | cookie.toCookieString must startWith("cookiename=value1; Secure")
42 | }
43 |
44 | "flag the cookie as http only if needed" in {
45 | val cookie = Cookie("cookiename", "value1")( CookieOptions(httpOnly = true))
46 | cookie.toCookieString must startWith("cookiename=value1; HttpOnly")
47 | }
48 |
49 | "render a cookie with all options set" in {
50 | val cookie = Cookie("cookiename", "value3")(CookieOptions(
51 | domain="nowhere.com",
52 | path="path/to/page",
53 | comment="the cookie thingy comment",
54 | maxAge=15500,
55 | secure=true,
56 | httpOnly=true,
57 | version=654
58 | ))
59 | cookie.toCookieString must
60 | equal("cookiename=value3; Domain=.nowhere.com; Path=/path/to/page; Comment=the cookie thingy comment; " +
61 | "Max-Age=15500; Secure; HttpOnly; Version=654")
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/core/src/test/scala/org/scalatra/ssgi/HttpMethodSpec.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | import org.scalatest.Spec
4 | import org.scalatest.matchers.ShouldMatchers
5 |
6 | class HttpMethodSpec extends Spec with ShouldMatchers {
7 | describe("HttpMethod") {
8 | it("should resolve standard methods case insensitively") {
9 | HttpMethod("gET") should be (Get)
10 | }
11 |
12 | it("should return an extension method, uppercase, for non-standard methods methods") {
13 | HttpMethod("foo") should equal (ExtensionMethod("FOO"))
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/core/src/test/scala/org/scalatra/ssgi/MimeUtilSpec.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | import org.scalatest.matchers.MustMatchers
4 | import org.scalatest.WordSpec
5 |
6 | class MimeUtilSpec extends WordSpec with MustMatchers {
7 |
8 | "The mime util" when {
9 |
10 | "the file doesn't exist" must {
11 |
12 | "detect the correct mime type when an extension is present" in {
13 | val the_file = "helloworld.jpg"
14 | MimeUtil.mimeType(the_file) must equal("image/jpeg")
15 | }
16 |
17 | "fallback to the default value when no extension is present" in {
18 | val the_file = "helloworld"
19 | MimeUtil.mimeType(the_file) must equal(MimeUtil.DEFAULT_MIME)
20 | }
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/core/src/test/scala/org/scalatra/ssgi/RenderableSpec.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 |
3 | import org.scalatest.WordSpec
4 | import org.scalatest.matchers.ShouldMatchers
5 | import java.nio.charset.Charset
6 | import java.io._
7 |
8 | class RenderableSpec extends WordSpec with ShouldMatchers {
9 | private def renderViaStream(r: Renderable, cs: Charset = "utf-8") = {
10 | val out = new ByteArrayOutputStream
11 | r.writeTo(out, cs)
12 | out.toByteArray
13 | }
14 |
15 | private def renderViaWriter(r: CharRenderable) = {
16 | val w = new StringWriter
17 | r.writeTo(w)
18 | w.toString
19 | }
20 |
21 | private implicit def string2Charset(s: String): Charset = Charset.forName(s)
22 |
23 | "A traversable of bytes" should {
24 | "write itself to an output stream" in {
25 | val array: Array[Byte] = "traversable of bytes".getBytes
26 | renderViaStream(array.toIterable) should equal (array)
27 | }
28 | }
29 |
30 | "An array of bytes" should {
31 | "write itself to an output stream" in {
32 | val array: Array[Byte] = "array of bytes".getBytes
33 | renderViaStream(array) should equal (array)
34 | }
35 | }
36 |
37 | "A string of bytes" should {
38 | val s = "stríñg"
39 |
40 | "write itself to an output stream in the specified encoding" in {
41 | renderViaStream(s, "utf-8") should equal (s.getBytes("utf-8"))
42 | renderViaStream(s, "iso-8859-1") should equal (s.getBytes("iso-8859-1"))
43 | }
44 |
45 | "write itself to a writer" in {
46 | renderViaWriter(s) should equal (s)
47 | }
48 | }
49 |
50 | "A node sequence" should {
51 | val ns =
52 |
53 | "write itself to an output stream in the specified encoding" in {
54 | renderViaStream(ns, "utf-8") should equal (ns.toString.getBytes("utf-8"))
55 | renderViaStream(ns, "iso-8859-1") should equal (ns.toString.getBytes("iso-8859-1"))
56 | }
57 |
58 | "write itself to a writer" in {
59 | renderViaWriter(ns) should equal (ns.toString)
60 | }
61 | }
62 |
63 | "An input stream" should {
64 | "write itself to an output stream" in {
65 | val bytes = new Array[Byte](5000)
66 | for (i <- 0 until bytes.size) { bytes(i) = ((i % 127) + 1).toByte }
67 | val in = new ByteArrayInputStream(bytes)
68 | renderViaStream(in) should equal (bytes)
69 | }
70 |
71 | "be closed after rendering" in {
72 | val in = new ByteArrayInputStream(Array[Byte]()) {
73 | var closed = false
74 | override def close() = { super.close; closed = true }
75 | }
76 | renderViaStream(in)
77 | in.closed should be (true)
78 | }
79 | }
80 |
81 | "A file" should {
82 | def withTempFile(content: Array[Byte])(f: File => Unit) {
83 | val file = File.createTempFile("ssgitest", "tmp")
84 | try {
85 | val fos = new FileOutputStream(file)
86 | try {
87 | fos.write(content);
88 | fos.flush();
89 | }
90 | finally {
91 | fos.close();
92 | }
93 | f(file)
94 | }
95 | finally {
96 | file.delete();
97 | }
98 | }
99 |
100 | "write itself to an output stream" in {
101 | val bytes = "File".getBytes
102 | withTempFile(bytes) { file =>
103 | renderViaStream(file) should equal (bytes)
104 | }
105 | }
106 |
107 | "return itself as a file" in {
108 | val bytes = "File".getBytes
109 | withTempFile(bytes) { file =>
110 | val renderable: FileRenderable = file
111 | renderable.file should equal (file)
112 | }
113 | }
114 | }
115 |
116 | "Something that can write to a writer" should {
117 | val string = "foo!"
118 | class WritesToWriter {
119 | def writeTo(writer: Writer) = {
120 | writer.write(string)
121 | }
122 | }
123 |
124 | "implicitly render itself to an output stream" in {
125 | renderViaStream(new WritesToWriter) should equal (string.getBytes)
126 | }
127 |
128 | "implicitly render itself to a writer" in {
129 | renderViaWriter(new WritesToWriter) should equal (string)
130 | }
131 | }
132 |
133 | "Something that can write to an output stream" should {
134 | val bytes = "bytes".getBytes
135 | class WritesToOutputStream {
136 | def writeTo(out: OutputStream) = {
137 | out.write(bytes)
138 | }
139 | }
140 |
141 | "implicitly render itself to an output stream" in {
142 | renderViaStream(new WritesToOutputStream) should equal (bytes)
143 | }
144 | }
145 | }
--------------------------------------------------------------------------------
/examples/servlet/src/main/scala/org/scalatra/ssgi/examples/servlet/HelloWorldApp.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 | package examples.servlet
3 |
4 | import scala.xml.NodeSeq
5 |
6 | class HelloWorldApp extends Application {
7 | def apply(v1: Request) = Response(body = Hello, world!
)
8 | }
--------------------------------------------------------------------------------
/examples/servlet/src/main/webapp/WEB-INF/web.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | ssgi
8 | org.scalatra.ssgi.servlet.SsgiServlet
9 |
10 | org.scalatra.ssgi.Application
11 | org.scalatra.ssgi.examples.servlet.HelloWorldApp
12 |
13 |
14 |
15 | ssgi
16 | /*
17 |
18 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | #Project properties
2 | #Sat Oct 23 00:20:16 EDT 2010
3 | project.organization=org.scalatra.ssgi
4 | project.name=ssgi
5 | sbt.version=0.7.5
6 | project.version=0.1.0-SNAPSHOT
7 | build.scala.versions=2.8.1 2.8.0
8 | project.initialize=false
9 |
--------------------------------------------------------------------------------
/project/build/SsgiProject.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 |
3 | class SsgiProject(info: ProjectInfo) extends DefaultProject(info) {
4 | lazy val core = project("core", "ssgi-core", new CoreProject(_))
5 | class CoreProject(info: ProjectInfo) extends DefaultProject(info) with SsgiSubProject {
6 | val servletApi = "javax.servlet" % "servlet-api" % "2.5" % "provided"
7 | val commonsLang = "commons-lang" % "commons-lang" % "2.5"
8 | val mimeUtil = "eu.medsea.mimeutil" % "mime-util" % "2.1.3"
9 | val scalaArm = "com.github.jsuereth.scala-arm" % "scala-arm_2.8.0" % "0.2"
10 | }
11 |
12 | lazy val servlet = project("servlet", "ssgi-servlet", new ServletProject(_), core)
13 | class ServletProject(info: ProjectInfo) extends DefaultProject(info) with SsgiSubProject {
14 | val servletApi = "javax.servlet" % "servlet-api" % "2.5" % "provided" withSources
15 | }
16 |
17 | lazy val examples = project("examples", "ssgi-example", new Examples(_))
18 | class Examples(info: ProjectInfo) extends ParentProject(info) {
19 | val servletExample = project("servlet", "ssgi-servlet-example", new ServletExampleProject(_), servlet)
20 | class ServletExampleProject(info: ProjectInfo) extends DefaultWebProject(info) with SsgiSubProject {
21 | val jetty6 = "org.mortbay.jetty" % "jetty" % "6.1.25" % "test"
22 | }
23 | }
24 |
25 | trait SsgiSubProject {
26 | this: BasicScalaProject =>
27 | val scalatest = "org.scalatest" % "scalatest" % "1.2" % "test"
28 | val mockito = "org.mockito" % "mockito-core" % "1.8.5" % "test"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/servlet/.ensime:
--------------------------------------------------------------------------------
1 | ;; This config was generated using ensime-config-gen. Feel free to customize its contents manually.
2 |
3 | (
4 |
5 | :server-root "~/.emacs.d/ensime"
6 |
7 | :project-package "org.scalatra.ssgi.servlet"
8 |
9 | :use-sbt t
10 |
11 | :compile-jars ("./lib_managed/scala_2.8.0/compile/" "../project/boot/scala-2.8.0/lib/" "../core/lib_managed/scala_2.8.0/compile")
12 |
13 | :class-dirs ( "../core/target/scala_2.8.0/classes" )
14 |
15 | )
16 |
17 |
--------------------------------------------------------------------------------
/servlet/src/main/scala/org/scalatra/ssgi/servlet/ByteArrayServletOutputStream.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi.servlet
2 |
3 | import javax.servlet.ServletOutputStream
4 | import java.io.ByteArrayOutputStream
5 |
6 | class ByteArrayServletOutputStream(bufSize: Int) extends ServletOutputStream {
7 |
8 | def this() = this(32)
9 |
10 | val internal = new ByteArrayOutputStream
11 |
12 | def toByteArray = internal.toByteArray
13 |
14 | override def write( i: Int) { internal.write(i) }
15 |
16 | override def write( bytes: Array[Byte]) { internal.write(bytes) }
17 |
18 | override def write( bytes: Array[Byte], start: Int, end: Int) { internal.write(bytes, start, end) }
19 |
20 | override def flush { }
21 |
22 | override def close { }
23 |
24 | private[ssgi] def reallyClose { internal.close }
25 |
26 | def size = internal.size
27 |
28 | def reset(): Unit = internal.reset()
29 | }
--------------------------------------------------------------------------------
/servlet/src/main/scala/org/scalatra/ssgi/servlet/ServletRequest.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 | package servlet
3 |
4 | import javax.servlet.http.{HttpServletRequest, HttpServletRequestWrapper}
5 | import scala.collection.immutable.DefaultMap
6 | import scala.collection.JavaConversions._
7 | import java.io.InputStream
8 | import java.util.Enumeration
9 | import scala.collection.{Iterator, Map}
10 | import collection.mutable. {MapLike, Map => MMap}
11 |
12 | /**
13 | * An HttpServletRequestWrapper that also implements the SSGI request interface.
14 | */
15 | class ServletRequest(private val r: HttpServletRequest) extends HttpServletRequestWrapper(r) with Request {
16 | lazy val requestMethod: HttpMethod = HttpMethod(getMethod)
17 |
18 | lazy val scriptName: String = getContextPath + getServletPath
19 |
20 | lazy val pathInfo: String = Option(getPathInfo).getOrElse("")
21 |
22 | lazy val queryString: String = Option(getQueryString).getOrElse("")
23 |
24 | lazy val contentType: Option[String] = Option(getContentType)
25 |
26 | lazy val contentLength: Option[Int] = if (getContentLength >= 0) Some(getContentLength) else None
27 |
28 | def serverName: String = getServerName
29 |
30 | def serverPort: Int = getServerPort
31 |
32 | def serverProtocol: String = getProtocol
33 |
34 | lazy val headers: Map[String, Seq[String]] = new DefaultMap[String, Seq[String]] {
35 | def get(name: String): Option[Seq[String]] = getHeaders(name) match {
36 | case null => None
37 | case xs => Some(xs.asInstanceOf[Enumeration[String]].toSeq)
38 | }
39 |
40 | def iterator: Iterator[(String, Seq[String])] =
41 | getHeaderNames.asInstanceOf[Enumeration[String]] map { name => (name, apply(name)) }
42 | }
43 |
44 | def scheme: String = getScheme
45 |
46 | def inputStream: InputStream = getInputStream
47 |
48 | override lazy val attributes: MMap[String, Any] = new MMap[String, Any] {
49 | def get(name: String): Option[Any] = Option(getAttribute(name))
50 |
51 | def iterator: Iterator[(String, Any)] =
52 | getAttributeNames.asInstanceOf[Enumeration[String]] map { name => (name, getAttribute(name)) }
53 |
54 | def +=(kv: (String, Any)) = {
55 | setAttribute(kv._1, kv._2)
56 | this
57 | }
58 |
59 | def -=(name: _root_.scala.Predef.String) = {
60 | removeAttribute(name)
61 | this
62 | }
63 | }
64 |
65 | lazy val parameterMap = getParameterMap.asInstanceOf[java.util.Map[String,Array[String]]]
66 | .toMap.transform { (k, v) => v: Seq[String] }
67 | }
68 |
--------------------------------------------------------------------------------
/servlet/src/main/scala/org/scalatra/ssgi/servlet/SsgiServlet.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 | package servlet
3 |
4 | import javax.servlet.{ServletException, ServletConfig}
5 | import javax.servlet.http.{HttpServletResponse, HttpServletRequest, HttpServlet}
6 | import java.nio.charset.Charset
7 |
8 | class SsgiServlet extends HttpServlet {
9 | import SsgiServlet._
10 |
11 | private var application: Application = _
12 |
13 | override def init(config: ServletConfig) {
14 | config.getInitParameter(ApplicationClassName) match {
15 | case className: String => loadApplication(className)
16 | case _ => throw new ServletException(ApplicationClassName + " should be set to class name of application")
17 | }
18 | }
19 |
20 | protected def loadApplication(className: String) {
21 | val appClass = getClass.getClassLoader.loadClass(className)
22 | application = appClass.newInstance.asInstanceOf[Application]
23 | }
24 |
25 | override def service(req: HttpServletRequest, resp: HttpServletResponse) = {
26 | val ssgiResponse = application(new ServletRequest(req))
27 | resp.setStatus(ssgiResponse.status)
28 | ssgiResponse.headers foreach { case (key, value) => resp.addHeader(key, value) }
29 | ssgiResponse.renderableBody match {
30 | case cr: CharRenderable => cr.writeTo(resp.getWriter)
31 | case r: Renderable => r.writeTo(resp.getOutputStream, Charset.forName("utf-8"))
32 | }
33 | }
34 | }
35 |
36 | object SsgiServlet {
37 | /**
38 | * The name of the init-param used to load the application
39 | */
40 | val ApplicationClassName = "org.scalatra.ssgi.Application"
41 | }
--------------------------------------------------------------------------------
/servlet/src/main/scala/org/scalatra/ssgi/servlet/SsgiServletResponse.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 | package servlet
3 |
4 | import javax.servlet.http.{Cookie => ServletCookie, HttpServletResponse}
5 | import java.lang.String
6 | import java.util.{Calendar, TimeZone, Locale}
7 | import java.io.{PrintWriter}
8 | import org.apache.commons.lang.time.FastDateFormat
9 | import java.nio.charset.Charset
10 |
11 | object SsgiServletResponse {
12 | val DATE_FORMAT = FastDateFormat.getInstance("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z", TimeZone.getTimeZone("GMT"))
13 |
14 | }
15 |
16 | /**
17 | * A wrapper for a HttpServletResponse that builds up an SSGI Response.
18 | * This class makes the reset operations NOOP's
19 | */
20 | class SsgiServletResponse(private val r: HttpServletResponse) extends HttpServletResponse with ResponseBuilder {
21 |
22 | import SsgiServletResponse._
23 |
24 | private var _out = new ByteArrayServletOutputStream
25 | private var _pw = new PrintWriter(_out)
26 |
27 | /**
28 | * Returns the SSGI Response with the default headers added if necessary
29 | */
30 | override def apply() = {
31 | val hdrs = generateDefaultHeaders
32 | _pw.flush
33 | Response(status, hdrs, _out.toByteArray)
34 | }
35 |
36 | /**
37 | * Sets the status of the response
38 | *
39 | * @param statusCode the status code for this response
40 | * @param body the message for the body
41 | */
42 | def setStatus(statusCode: Int, body: String) {
43 | _pw.write(body)
44 | response = response.copy(statusCode)
45 | }
46 |
47 | /**
48 | * Sets the status of the response
49 | *
50 | * @param statusCode the status code for this response
51 | */
52 | def setStatus(statusCode: Int): Unit = status = statusCode
53 |
54 | /**
55 | * Adds an int header to the response
56 | *
57 | * @param name the name of the header
58 | * @param value the value of the header
59 | */
60 | def addIntHeader(name: String, value: Int) = {
61 | addHeader(name, value.toString)
62 | }
63 |
64 | /**
65 | * Sets an int header to the response. This replaces an existing header if it was there previously
66 | *
67 | * @param name the name of the header
68 | * @param value the value of the header
69 | */
70 | def setIntHeader(name: String, value: Int) = setHeader(name, value.toString)
71 |
72 | /**
73 | * Adds a header to the response
74 | *
75 | * @param name the name of the header
76 | * @param value the value of the header
77 | */
78 | def addHeader(name: String, value: String) = {
79 | addSsgiHeader(name, value)
80 | }
81 |
82 | /**
83 | * Adds a date header to the response.
84 | *
85 | * @param name the name of the header
86 | * @param value the value of the header
87 | */
88 | def addDateHeader(name: String, value: Long) = {
89 | addHeader(name, formatDate(value))
90 | }
91 |
92 | /**
93 | * Sets a date header to the response. This replaces an existing header if it was there previously
94 | *
95 | * @param name the name of the header
96 | * @param value the value of the header
97 | */
98 | def setDateHeader(name: String, value: Long) = {
99 | setHeader(name, formatDate(value))
100 | }
101 |
102 |
103 |
104 | /**
105 | * Encodes the redirect url
106 | *
107 | * @param url the url to encode
108 | */
109 | def encodeRedirectUrl(url: String) = encodeURL(url)
110 |
111 |
112 | /**
113 | * Encodes a url
114 | *
115 | * @param url the url to encode
116 | */
117 | def encodeUrl(url: String) = encodeURL(url)
118 |
119 | /**
120 | * Encodes the redirect url
121 | *
122 | * @param url the url to encode
123 | */
124 | def encodeRedirectURL(url: String) = encodeURL(url)
125 |
126 | /**
127 | * Encodes a url
128 | *
129 | * @param url the url to encode
130 | */
131 | def encodeURL(url: String) = {
132 | r.encodeURL(url)
133 | }
134 |
135 | /**
136 | * Predicate to check if the response already contains the header with the specified key
137 | *
138 | * @param key The key to check the header collection for
139 | */
140 | def containsHeader(key: String) = response.headers.contains(key)
141 |
142 | /**
143 | * Adds a cookie to the response
144 | */
145 | def addCookie(servletCookie: ServletCookie) = {
146 | addHeader("Set-Cookie", servletCookie2Cookie(servletCookie).toCookieString)
147 | }
148 |
149 | /**
150 | * Gets the currently configured locale for this response
151 | */
152 | def getLocale = response.headers.get("Content-Language") match {
153 | case Some(locLang) => {
154 | locLang.split("-").toList match {
155 | case lang :: Nil => new Locale(lang)
156 | case lang :: country :: Nil => new Locale(lang, country)
157 | case lang :: country :: variant :: whatever => new Locale(lang, country, variant)
158 | case _ => Locale.getDefault
159 | }
160 | }
161 | case _ => Locale.getDefault
162 | }
163 |
164 | /**
165 | * Sets the locale for this response
166 | */
167 | def setLocale(locale: Locale) = {
168 | setHeader("Content-Language", locale.toString.toLowerCase(locale).replace('_', '-'))
169 | }
170 |
171 | /**
172 | * Resets this response completely discarding the current content and headers and sets the status to 200
173 | */
174 | def reset = {
175 | response = Response(200, Map.empty, Array[Byte]())
176 | _out.close
177 | _out = new ByteArrayServletOutputStream
178 | _pw = new PrintWriter(_out)
179 | }
180 |
181 | /**
182 | * Always returns false in the context of SSGI. Committing responses is a big NO-NO for supporting middlewares
183 | */
184 | def isCommitted = false
185 |
186 | /**
187 | * Resets the content of this response discarding the current content
188 | */
189 | def resetBuffer = {
190 | body = Array[Byte]()
191 | _out.close
192 | _out = new ByteArrayServletOutputStream
193 | _pw = new PrintWriter(_out)
194 | }
195 |
196 | /**
197 | * This is a NOOP The interface is legacy. It's preferred to use the SSGI Repsonse class
198 | */
199 | def flushBuffer = {
200 |
201 | }
202 |
203 | /**
204 | * This is a NOOP The interface is legacy. It's preferred to use the SSGI Repsonse class
205 | */
206 | def getBufferSize = Int.MaxValue
207 |
208 | /**
209 | * This is a NOOP The interface is legacy. It's preferred to use the SSGI Repsonse class
210 | */
211 | def setBufferSize(size: Int) = {
212 |
213 | }
214 |
215 | /**
216 | * Sets the content type of this response
217 | *
218 | * @param contentType The content type to use for this response
219 | */
220 | def setContentType(contentType: String): Unit = this.contentType = contentType
221 |
222 | /**
223 | * Sets the content length of this response
224 | *
225 | * @param contentLength the content length of this response
226 | */
227 | def setContentLength(contentLength: Int) = setHeader("Content-Length", contentLength.toString)
228 |
229 | /**
230 | * Sets the character encoding of the response
231 | *
232 | * @param encoding The encoding to use for this response
233 | */
234 | def setCharacterEncoding(encoding: String): Unit = characterEncoding = encoding
235 |
236 | /**
237 | * Gets the printwriter for this response
238 | */
239 | def getWriter = _pw
240 |
241 | /**
242 | * Get the output stream for this response
243 | * This wraps a ByteArrayOutputStream so be careful with huge responses.
244 | *
245 | * Use the SSGI Response directly instead of this method if you need to send a big response
246 | */
247 | def getOutputStream = _out
248 |
249 | /**
250 | * Gets the content type that belongs to this response
251 | */
252 | def getContentType: String = contentType
253 |
254 | /**
255 | * Get the character encoding that belongs to this response
256 | */
257 | def getCharacterEncoding: String = characterEncoding
258 |
259 | private def generateDefaultHeaders = {
260 | var headers = response.headers
261 | if(!headers.contains("Content-Type")) headers += "Content-Type" -> "%s; charset=%s".format(getContentType, getCharacterEncoding)
262 | if(!headers.contains("Content-Length")) {
263 | headers += "Content-Length" -> body.asInstanceOf[Array[Byte]].length.toString
264 | }
265 | if(!headers.contains("Content-Language")) headers += "Content-Language" -> getLocale.toString.replace('_', '-')
266 | if(!headers.contains("Date")) headers += "Date" -> formatDate(Calendar.getInstance.getTimeInMillis)
267 | headers
268 | }
269 |
270 | private def addSsgiHeader(name: String, value: String) = {
271 | val headers = response.headers.get(name) match {
272 | case Some(hdrVal) => response.headers + (name -> "%s,%s".format(hdrVal, value))
273 | case _ => response.headers + (name -> value.toString)
274 | }
275 | response = response.copy(headers = headers)
276 | }
277 |
278 | private def formatDate(millis: Long) = {
279 | val cal = Calendar.getInstance
280 | cal.setTimeInMillis(millis)
281 | DATE_FORMAT.format(cal.getTime)
282 | }
283 |
284 | private def servletCookie2Cookie(sc: ServletCookie) = Cookie(sc.getName, sc.getValue)(CookieOptions(
285 | sc.getDomain,
286 | sc.getPath,
287 | sc.getMaxAge,
288 | sc.getSecure,
289 | sc.getComment,
290 | sc.getVersion))
291 |
292 | // TODO temporary hack around mismatch between streaming and in-memory bodies
293 | override def body: Any = _out.toByteArray
294 | override def body_=[A <% Renderable](body: A): Unit = {
295 | _out.reset()
296 | body.writeTo(_out, Charset.forName(characterEncoding))
297 | }
298 | }
--------------------------------------------------------------------------------
/servlet/src/main/scala/org/scalatra/ssgi/servlet/testing/MockRequestDispatcher.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi.servlet.testing
2 |
3 | import javax.servlet.{ServletRequest, ServletResponse, RequestDispatcher}
4 |
5 | class MockRequestDispatcher extends RequestDispatcher {
6 |
7 | private var _path: String = "/"
8 | private var _forwardedRequest: ServletRequest = null
9 | private var _forwardedResponse: ServletResponse = null
10 | private var _includedRequest: ServletRequest = null
11 | private var _includedResponse: ServletResponse = null
12 |
13 | def setPath(path: String) = {
14 | _path = path
15 | this
16 | }
17 |
18 | def getPath(path: String) = _path
19 |
20 | def forward(request: ServletRequest, response: ServletResponse) = {
21 | _forwardedRequest = request
22 | _forwardedResponse = response
23 | this
24 | }
25 |
26 | def include(request: ServletRequest, response: ServletResponse) = {
27 | _includedRequest = request
28 | _includedResponse = response
29 | this
30 | }
31 |
32 | def getForwardedRequest = _forwardedRequest
33 | def getForwardedResponse = _forwardedResponse
34 | def getIncludedRequest = _includedRequest
35 | def getIncludedResponse = _includedResponse
36 |
37 | }
--------------------------------------------------------------------------------
/servlet/src/main/scala/org/scalatra/ssgi/servlet/testing/MockServletContext.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi.servlet.testing
2 |
3 | import java.net.URL
4 | import org.scalatra.ssgi.MimeUtil
5 | import collection.mutable.HashMap
6 | import java.util.{ Vector => JVector }
7 | import collection.JavaConversions._
8 | import java.io.{BufferedInputStream, ByteArrayOutputStream, ByteArrayInputStream, InputStream}
9 | import javax.servlet.{RequestDispatcher, Servlet, ServletContext}
10 |
11 | class MockServletContext(initialContextPath: String) extends ServletContext {
12 |
13 | private var contextPath = initialContextPath
14 | private var minorVersion = 5
15 | private var servletContextName = "MockServletContext"
16 | private val contexts = new HashMap[String, ServletContext]()
17 | private val attributes = new HashMap[String, Any]()
18 | private val initParameters = new HashMap[String, String]()
19 | private val realPaths = new HashMap[String, String]()
20 | private val resources = new HashMap[String, URL]()
21 | private val resourcePaths = new HashMap[String, Set[String]]()
22 | private val resourceStreams = new HashMap[String, Array[Byte]]()
23 | private val requestDispatchers = new HashMap[String, RequestDispatcher]()
24 |
25 | def this() = this("")
26 |
27 |
28 | def reset = {
29 | contextPath = initialContextPath
30 | minorVersion = 5
31 | servletContextName = "MockServletContext"
32 | contexts.clear
33 | attributes.clear
34 | initParameters.clear
35 | realPaths.clear
36 | resources.clear
37 | resourcePaths.clear
38 | resourceStreams.clear
39 | requestDispatchers.clear
40 | }
41 |
42 | def setContextPath(ctxtPath: String) = {
43 | contextPath = if (ctxtPath != null ) ctxtPath else ""
44 | this
45 | }
46 |
47 | def getContextPath() = contextPath
48 |
49 | def registerContext(contextPath: String, context: ServletContext) = {
50 | contexts += contextPath -> context
51 | }
52 |
53 | def getContext(contextPath: String) = {
54 | if (this.contextPath == contextPath) {
55 | this
56 | } else {
57 | contexts(contextPath)
58 | }
59 | }
60 |
61 | def getMajorVersion() = 2
62 |
63 | def setMinorVersion(minorVersion: Int) = {
64 | this.minorVersion = minorVersion
65 | this
66 | }
67 |
68 | def getMinorVersion() = minorVersion
69 |
70 | def getMimeType(filePath: String) = MimeUtil.mimeType(filePath)
71 |
72 |
73 | def getServerInfo = "Scalatra SSGI Mock Servlet Context"
74 |
75 |
76 | def getServletContextName = servletContextName
77 | def setServletContextName(name: String) = {
78 | servletContextName = name
79 | this
80 | }
81 |
82 | def removeAttribute(key: String) { attributes -= key }
83 | def setAttribute(key: String, value: Any) = {
84 | attributes += key -> value
85 | this
86 | }
87 | def getAttributeNames = new JVector(attributes.keySet).elements
88 | def getAttribute(key: String) = attributes(key).asInstanceOf[AnyRef]
89 |
90 | def getInitParameterNames = new JVector(initParameters.keySet).elements
91 | def getInitParameter(key: String) = initParameters(key)
92 | def setInitParameter(key: String, param: String) = {
93 | initParameters += key -> param
94 | this
95 | }
96 |
97 | def getRealPath(path: String) = realPaths(path)
98 | def setRealPath(path: String, realPath: String) = {
99 | realPaths += path -> realPath
100 | this
101 | }
102 |
103 | def log(msg: String, th: Throwable) {}
104 | def log(msg: String) {}
105 | def log(exc: Exception, msg: String) {}
106 |
107 | def getServletNames = new JVector[String]().elements
108 | def getServlets = new JVector[String]().elements
109 | def getServlet(servletName: String) = null.asInstanceOf[Servlet]
110 |
111 | def getResource(path: String) = resources(path)
112 | def setResource(path: String, resource: URL) = {
113 | resources += path -> resource
114 | this
115 | }
116 |
117 | def getResourcePaths(path: String) = resourcePaths.get(path) getOrElse null.asInstanceOf[Set[String]]
118 |
119 | def addResourcePaths(path: String, resourcePaths: String*) = {
120 | val set = this.resourcePaths.get(path) getOrElse Set[String]()
121 | this.resourcePaths += path -> (resourcePaths.toSet ++ set)
122 | this
123 | }
124 |
125 | def getResourceAsStream(path: String) = {
126 | if(! resourceStreams.contains(path)) null.asInstanceOf[InputStream]
127 | else {
128 | val data = resourceStreams(path)
129 | new ByteArrayInputStream(data)
130 | }
131 | }
132 |
133 | private def streamToByteArray(is: InputStream): Array[Byte] = {
134 | val out = new ByteArrayOutputStream
135 | val data = new BufferedInputStream(is)
136 | var current = data.read
137 | while (current >= 0) {
138 | out.write(current)
139 | current = data.read
140 | }
141 | out.toByteArray
142 | }
143 |
144 | def setResourceAsStream(path: String, is: InputStream) = {
145 | resourceStreams += path -> streamToByteArray(is)
146 | this
147 | }
148 |
149 | def setResourceAsStream(path: String, is: Array[Byte]) = {
150 | resourceStreams += path -> is
151 | this
152 | }
153 |
154 |
155 | def getNamedDispatcher(name: String) = getRequestDispatcher(name)
156 | def getRequestDispatcher(path: String) = {
157 | requestDispatchers.get(path) match {
158 | case Some(disp) => disp
159 | case _ => {
160 | val disp = new MockRequestDispatcher()
161 | setRequestDispatcher(path, disp)
162 | disp
163 | }
164 | }
165 | }
166 |
167 | def setRequestDispatcher(path: String, dispatcher: RequestDispatcher) = {
168 | if(dispatcher.isInstanceOf[MockRequestDispatcher]) {
169 | dispatcher.asInstanceOf[MockRequestDispatcher].setPath(path)
170 | }
171 | requestDispatchers += path -> dispatcher
172 | this
173 | }
174 |
175 | }
--------------------------------------------------------------------------------
/servlet/src/test/scala/org/scalatra/ssgi/servlet/ServletRequestSpec.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 | package servlet
3 |
4 | import org.scalatest.{OneInstancePerTest, Spec}
5 | import org.scalatest.matchers.ShouldMatchers
6 | import org.scalatest.mock.MockitoSugar
7 | import org.mockito.Mockito._
8 | import javax.servlet.http.HttpServletRequest
9 | import scala.collection.JavaConversions._
10 | import java.util.{Enumeration, Map => JMap}
11 | import javax.servlet.ServletInputStream
12 |
13 | class ServletRequestSpec extends Spec with ShouldMatchers with MockitoSugar with OneInstancePerTest {
14 | val delegate = mock[HttpServletRequest]
15 | val req = new ServletRequest(delegate)
16 |
17 | describe("ServletRequest.requestMethod") {
18 | it("should derive from getMethod") {
19 | when(delegate.getMethod) thenReturn "GET"
20 | req.requestMethod should be (Get)
21 | }
22 | }
23 |
24 | describe("ServletRequest.scriptName") {
25 | it("should be the concatenation of contextPath and servletPath") {
26 | when(delegate.getContextPath) thenReturn "/contextPath"
27 | when(delegate.getServletPath) thenReturn "/servletPath"
28 | req.scriptName should equal ("/contextPath/servletPath")
29 | }
30 | }
31 |
32 | describe("ServletRequest.pathInfo") {
33 | describe("when getPathInfo is not null") {
34 | it("should equal getPathInfo") {
35 | when(delegate.getPathInfo) thenReturn "/pathInfo"
36 | req.pathInfo should equal ("/pathInfo")
37 | }
38 | }
39 |
40 | describe("when getPathInfo is null") {
41 | it("should be the empty string") {
42 | when(delegate.getPathInfo) thenReturn null
43 | req.pathInfo should equal ("")
44 | }
45 | }
46 | }
47 |
48 | describe("ServletRequest.queryString") {
49 | describe("when getQueryString is not null") {
50 | it("should equal getQueryString") {
51 | when(delegate.getQueryString) thenReturn "foo=bar"
52 | req.queryString should equal ("foo=bar")
53 | }
54 | }
55 |
56 | describe("when getQueryString is null") {
57 | it("should be the empty string") {
58 | when(delegate.getQueryString) thenReturn null
59 | req.queryString should equal ("")
60 | }
61 | }
62 | }
63 |
64 | describe("ServletRequest.contentType") {
65 | describe("when getContentType is not null") {
66 | it("should equal Some(getContentType)") {
67 | when(delegate.getContentType) thenReturn "application/octet-stream"
68 | req.contentType should equal (Some("application/octet-stream"))
69 | }
70 | }
71 |
72 | describe("when getContentType is null") {
73 | it("should be None") {
74 | when(delegate.getContentType) thenReturn null
75 | req.contentType should equal (None)
76 | }
77 | }
78 | }
79 |
80 | describe("ServletRequest.contentLength") {
81 | describe("when getContentLength is positive") {
82 | it("should equal Some(getContentLength)") {
83 | when(delegate.getContentLength) thenReturn 1024
84 | req.contentLength should equal (Some(1024))
85 | }
86 | }
87 |
88 | describe("when getContentLength is zero") {
89 | it("should equal Some(0)") {
90 | when(delegate.getContentLength) thenReturn 0
91 | req.contentLength should equal (Some(0))
92 | }
93 | }
94 |
95 | describe("when getContentType is negative") {
96 | it("should be None") {
97 | when(delegate.getContentLength) thenReturn -1
98 | req.contentLength should equal (None)
99 | }
100 | }
101 | }
102 |
103 | describe("ServletRequest.serverName") {
104 | it("should equal getServerName") {
105 | when(delegate.getServerName) thenReturn "www.scalatra.org"
106 | req.serverName should be ("www.scalatra.org")
107 | }
108 | }
109 |
110 | describe("ServletRequest.serverPort") {
111 | it("should equal getServerPort") {
112 | when(delegate.getServerPort) thenReturn 80
113 | req.serverPort should be (80)
114 | }
115 | }
116 |
117 | describe("ServletRequest.serverProtocol") {
118 | it("should equal getProtocol") {
119 | when(delegate.getProtocol) thenReturn "HTTP/1.1"
120 | req.serverProtocol should be ("HTTP/1.1")
121 | }
122 | }
123 |
124 | describe("ServletRequest.headers") {
125 | it("should iterate over all headers") {
126 | when(delegate.getHeaderNames.asInstanceOf[Enumeration[String]]) thenReturn Iterator("foo", "bar")
127 | when(delegate.getHeaders("foo").asInstanceOf[Enumeration[String]]) thenReturn Iterator("oof")
128 | when(delegate.getHeaders("bar").asInstanceOf[Enumeration[String]]) thenReturn Iterator("bar")
129 | req.headers.keys should equal (Set("foo", "bar"))
130 | }
131 |
132 | it("should return Some(Seq) for a known header") {
133 | when(delegate.getHeaders("Numbers").asInstanceOf[Enumeration[String]]) thenReturn Iterator("1", "2", "3")
134 | req.headers.get("Numbers") should equal (Some(List("1", "2", "3")))
135 | }
136 |
137 | it("should return None for an unknown header") {
138 | when(delegate.getHeaders("Unknown").asInstanceOf[Enumeration[String]]) thenReturn null
139 | req.headers.get("Unknown") should equal (None)
140 | }
141 | }
142 |
143 | describe("ServletRequest.scheme") {
144 | it("should equal getScheme") {
145 | when(delegate.getScheme) thenReturn "http"
146 | req.scheme should be ("http")
147 | }
148 | }
149 |
150 | describe("ServletRequest.inputStream") {
151 | it("should equal getInputStream") {
152 | val inputStream = mock[ServletInputStream]
153 | when(delegate.getInputStream) thenReturn inputStream
154 | req.inputStream should be (inputStream)
155 | }
156 | }
157 |
158 | describe("ServletRequest.attributes") {
159 | it("should iterate over all attributes") {
160 | when(delegate.getAttributeNames.asInstanceOf[Enumeration[String]]) thenReturn Iterator("foo", "bar")
161 | when(delegate.getAttribute("foo")).thenReturn(, Array[Object](): _*)
162 | when(delegate.getAttribute("bar")).thenReturn("rab", Array[Object](): _*)
163 | req.attributes should equal (Map("foo" -> , "bar" -> "rab"))
164 | }
165 |
166 | it("should return Some(val) for a known attribute") {
167 | when(delegate.getAttribute("foo")).thenReturn(, Array[Object](): _*)
168 | req.attributes.get("foo") should equal (Some())
169 | }
170 |
171 | it("should return None for an unknown header") {
172 | when(delegate.getHeaders("Unknown")) thenReturn null
173 | req.attributes.get("Unknown") should equal (None)
174 | }
175 |
176 | it("should setAttribute on update") {
177 | req.attributes("ssgi") = "rocks"
178 | verify(delegate).setAttribute("ssgi", "rocks")
179 | }
180 |
181 | it("should removeAttribute on remove") {
182 | req.attributes.remove("servlet dependency")
183 | verify(delegate).removeAttribute("servlet dependency")
184 | }
185 | }
186 |
187 | describe("ServletRequest.parameterMap") {
188 | it("should equal a Scalified version of request.getParameters") {
189 | val map = Map("1" -> Array("one"), "2" -> Array("two", "dos")).toMap
190 | when(delegate.getParameterMap.asInstanceOf[JMap[String, Array[String]]]).thenReturn(map, Array(): _*)
191 | req.parameterMap should equal (Map("1" -> Seq("one"), "2" -> Seq("two", "dos")))
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/servlet/src/test/scala/org/scalatra/ssgi/servlet/SsgiServletResponseSpec.scala:
--------------------------------------------------------------------------------
1 | package org.scalatra.ssgi
2 | package servlet
3 |
4 | import org.scalatest.matchers.MustMatchers
5 | import org.scalatest.mock.MockitoSugar
6 | import org.scalatest.{OneInstancePerTest, WordSpec}
7 | import org.mockito.Mockito._
8 | import org.mockito.Matchers._
9 | import java.util.Locale
10 | import javax.servlet.http.{Cookie, HttpServletResponse}
11 |
12 | class SsgiServletResponseSpec extends WordSpec with MustMatchers with MockitoSugar with OneInstancePerTest {
13 |
14 | val mockResponse = mock[HttpServletResponse]
15 | when(mockResponse.encodeURL(anyString)) thenReturn "encoded"
16 |
17 | val resp = new SsgiServletResponse(mockResponse)
18 | def actual = resp()
19 |
20 | "A SsgiServletResponse" when {
21 |
22 | "the response is empty" should {
23 |
24 | "have a status of 200" in {
25 | actual.status must be (200)
26 | }
27 |
28 | "contain the Content-Length header" in {
29 | actual.headers must not be ('empty)
30 | actual.headers.get("Content-Length") must be (Some("0"))
31 | }
32 |
33 | "contain the Content-Type header" in {
34 | actual.headers.get("Content-Type") must be (Some(ResponseBuilder.DefaultContentType + "; charset=" + ResponseBuilder.DefaultEncoding))
35 | }
36 |
37 | "contain the Content-Language header" in {
38 | actual.headers.get("Content-Language") must be ('defined)
39 | }
40 |
41 | "contain the Date header" in {
42 | actual.headers.get("Date") must be ('defined)
43 | }
44 |
45 | }
46 |
47 | "setting some properties" should {
48 |
49 | "the buffersize must be int.maxvalue" in {
50 | resp.getBufferSize must be (Int.MaxValue)
51 | }
52 |
53 | "setting the buffersize makes no difference" in {
54 | resp.setBufferSize(543)
55 | resp.getBufferSize must be(Int.MaxValue)
56 | }
57 |
58 | "getting the character encoding when none is set should return the default" in {
59 | resp.getCharacterEncoding must be (ResponseBuilder.DefaultEncoding)
60 | }
61 |
62 | "setting the character encoding should return the correct encoding" in {
63 | resp.setCharacterEncoding("NEW-ENCODING")
64 | resp.getCharacterEncoding must be ("NEW-ENCODING")
65 | }
66 |
67 | "set the content length" in {
68 | resp.setContentLength(12345)
69 | resp().headers.get("Content-Length") must be (Some("12345"))
70 | }
71 |
72 | "set the content type" in {
73 | resp.setContentType("text/html")
74 | resp.getContentType must be ("text/html")
75 | }
76 |
77 | "set the locale" in {
78 | resp.setLocale(Locale.US)
79 | resp().headers.get("Content-Language") must be (Some("en-us"))
80 | }
81 |
82 | "get the locale" in {
83 | resp.setLocale(Locale.US)
84 | resp.getLocale must be (Locale.US)
85 | }
86 |
87 | "isCommitted must always be false" in {
88 | resp.isCommitted must be (false)
89 | }
90 | }
91 |
92 | "resetting" should {
93 |
94 | "the buffer only should clear the body of content" in {
95 | resp.getWriter.println("Make body non-empty")
96 | resp.resetBuffer
97 | resp.getOutputStream.size must be (0)
98 | }
99 |
100 | "the entire response must reset the headers and the status to 200" in {
101 | resp.getWriter.println("Make body non-empty")
102 | resp.setStatus(400)
103 | resp.reset
104 | resp().status must be (200)
105 | resp.getOutputStream.size must be (0)
106 | }
107 |
108 | }
109 |
110 | "working with headers" should {
111 |
112 | "setting the status should change the status code" in {
113 | resp.setStatus(404)
114 | resp().status must be (404)
115 | }
116 |
117 | "setting an int header should replace an existing int header" in {
118 | resp.setIntHeader("theHeader", 4)
119 | resp().headers.get("theHeader") must be (Some("4"))
120 | resp.setIntHeader("theHeader", 7)
121 | resp().headers.get("theHeader") must be (Some("7"))
122 | }
123 |
124 | "adding an int header should not replace an existing int header" in {
125 | resp.addIntHeader("theHeader", 4)
126 | resp().headers.get("theHeader") must be (Some("4"))
127 | resp.addIntHeader("theHeader", 7)
128 | resp().headers.get("theHeader") must be (Some("4,7"))
129 | }
130 |
131 | "adding a cookie should serialize the servlet cookie to a header" in {
132 | resp.addCookie(new Cookie("theCookie", "the value"))
133 | resp().headers.get("Set-Cookie") must be (Some("theCookie=the+value"))
134 | }
135 | }
136 |
137 | "writing content" should {
138 |
139 | "to the output stream should produce a byte array" in {
140 | val bytes = "helloWorld".getBytes("UTF-8")
141 | resp.getOutputStream.write(bytes)
142 | resp().body.asInstanceOf[Array[Byte]] must be (bytes)
143 | }
144 |
145 | "to the print writer should produce a byte array" in {
146 | val bytes = "helloWorld".getBytes("UTF-8")
147 | resp.getWriter.print("helloWorld")
148 | resp().body.asInstanceOf[Array[Byte]] must be (bytes)
149 | }
150 | }
151 |
152 | "sending an error" should {
153 | "throw an exception if the error code < 400" in {
154 | evaluating { resp.sendError(200) } must produce [Exception]
155 | }
156 | "set the status correctly if the error code > 400" in {
157 | resp.sendError(404)
158 | resp().status must be (404)
159 | }
160 | }
161 |
162 | "redirecting" should {
163 |
164 | "encode the url" in {
165 | resp.sendRedirect("/go/somewhere")
166 | val ssgi = resp()
167 | ssgi.headers.get("Location") must be (Some("encoded"))
168 | ssgi.status must be (302)
169 | }
170 |
171 | }
172 | }
173 | }
--------------------------------------------------------------------------------