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