Permalink
Browse files

Basic FileUploadSupport with Servlet 3.0 API

  • Loading branch information...
1 parent 7b71bcc commit c47937513a8807396af28c19f88556d774978494 @mnylen committed Apr 8, 2012
@@ -0,0 +1,12 @@
+package org.scalatra.fileupload
+
+import javax.servlet.http.Part
+
+case class FileItemServlet3(name: String, part: Part) {
+ val size = part.getSize
+ val fieldName = part.getName
+ val contentType: Option[String] = Option(part.getHeader("content-type"))
+
+ def bytes: Array[Byte] = org.scalatra.util.io.readBytes(getInputStream)
+ def getInputStream = part.getInputStream
+}
@@ -0,0 +1,26 @@
+package org.scalatra.fileupload
+
+class FileMultiParamsServlet3(wrapped: Map[String, Seq[FileItemServlet3]] = Map.empty) extends Map[String, Seq[FileItemServlet3]] {
+
+ def get(key: String): Option[Seq[FileItemServlet3]] = {
+ (wrapped.get(key) orElse wrapped.get(key + "[]"))
+ }
+
+ def get(key: Symbol): Option[Seq[FileItemServlet3]] = get(key.name)
+
+ def +[B1 >: Seq[FileItemServlet3]](kv: (String, B1)) =
+ new FileMultiParamsServlet3(wrapped + kv.asInstanceOf[(String, Seq[FileItemServlet3])])
+
+ def -(key: String) = new FileMultiParamsServlet3(wrapped - key)
+
+ def iterator = wrapped.iterator
+
+ override def default(a: String): Seq[FileItemServlet3] = wrapped.default(a)
+}
+
+object FileMultiParamsServlet3 {
+ def apply() = new FileMultiParamsServlet3
+
+ def apply[SeqType <: Seq[FileItemServlet3]](wrapped: Map[String, Seq[FileItemServlet3]]) =
+ new FileMultiParamsServlet3(wrapped)
+}
@@ -0,0 +1,125 @@
+package org.scalatra.fileupload
+
+import scala.collection.JavaConversions._
+import javax.servlet.annotation.MultipartConfig
+import javax.servlet.http.{HttpServletRequest, Part}
+import org.scalatra.servlet.{ServletRequest, ServletBase}
+import java.util.{HashMap => JHashMap, Map => JMap}
+
+@MultipartConfig
+trait FileUploadSupportServlet3 extends ServletBase {
+ import FileUploadSupportServlet3._
+
+ override def handle(req: RequestT, res: ResponseT) {
+ val req2 = try {
+ if (isMultipartRequest(req)) {
+ val bodyParams = extractMultipartParams(req)
+ val mergedFormParams = mergeFormParamsWithQueryString(req, bodyParams)
+
+ wrapRequest(req, mergedFormParams)
+ } else req
+ } catch {
+ case e: Exception => req
+ }
+
+ super.handle(req2, res)
+ }
+
+ private def isMultipartRequest(req: RequestT): Boolean = {
+ val isPostOrPut = Set("POST", "PUT").contains(req.getMethod)
+
+ isPostOrPut && (req.contentType match {
+ case Some(contentType) => contentType.startsWith("multipart/")
+ case _ => false
+ })
+ }
+
+ private def extractMultipartParams(req: RequestT): BodyParams = {
+ req.get(BodyParamsKey).asInstanceOf[Option[BodyParams]] match {
+ case Some(bodyParams) =>
+ bodyParams
+
+ case None => {
+ req.getParts.foldRight(BodyParams(FileMultiParamsServlet3(), Map.empty)) { (part, params) =>
+ val fname = fileName(part)
+
+ if (fname.isDefined) {
+ BodyParams(params.fileParams + ((
+ part.getName, FileItemServlet3(fname.get, part) +: params.fileParams.getOrElse(part.getName, List[FileItemServlet3]())
+ )), params.formParams)
+ } else {
+ BodyParams(params.fileParams, params.formParams + (
+ (part.getName, partToString(part) ::
+ params.formParams.getOrElse(part.getName, List[String]())
+ )
+ ))
+ }
+ }
+ }
+ }
+ }
+
+ private def partToString(part: Part): String = {
+ import org.scalatra.util.io.readBytes
+
+ new String(readBytes(part.getInputStream))
+ }
+
+ private def fileName(part: Part): Option[String] = {
+ val partHeaderOrNone = Option(part.getHeader("content-disposition"))
+
+ partHeaderOrNone match {
+ case Some(partHeader) => {
+ val fileNameAttrOrNone = partHeader.split(";").find(_.trim().startsWith("filename"))
+
+ fileNameAttrOrNone match {
+ case Some(fileName) => Some(fileName.substring(fileName.indexOf('=') + 1).trim().replace("\"", ""))
+ case _ => None
+ }
+ }
+
+ case _ => None
+ }
+ }
+
+ private def mergeFormParamsWithQueryString(req: RequestT, bodyParams: BodyParams): Map[String, List[String]] = {
+ var mergedParams = bodyParams.formParams
+ req.getParameterMap.asInstanceOf[JMap[String, Array[String]]] foreach {
+ case (name, values) =>
+ val formValues = mergedParams.getOrElse(name, List.empty)
+ mergedParams += name -> (values.toList ++ formValues)
+ }
+
+ mergedParams
+ }
+
+ private def wrapRequest(req: HttpServletRequest, formMap: Map[String, Seq[String]]) = {
+ val wrapped = new ServletRequest(req) {
+ override def getParameter(name: String) = formMap.get(name) map { _.head } getOrElse null
+ override def getParameterNames = formMap.keysIterator
+ override def getParameterValues(name: String) = formMap.get(name) map { _.toArray } getOrElse null
+ override def getParameterMap = new JHashMap[String, Array[String]] ++ (formMap transform { (k, v) => v.toArray })
+ }
+ wrapped
+ }
+
+ protected def fileMultiParams: FileMultiParamsServlet3 = extractMultipartParams(request).fileParams
+
+ protected val _fileParams = new collection.Map[String, FileItemServlet3] {
+ def get(key: String) = fileMultiParams.get(key) flatMap { _.headOption }
+ override def size = fileMultiParams.size
+ override def iterator = (fileMultiParams map {
+ case (k, v) => (k, v.head)
+ }).iterator
+ override def -(key: String) = Map() ++ this - key
+ override def +[B1 >: FileItemServlet3](kv: (String, B1)) = Map() ++ this + kv
+ }
+
+ /** @return a Map, keyed on the names of multipart file upload parameters, of all multipart files submitted with the request */
+ def fileParams = _fileParams
+}
+
+object FileUploadSupportServlet3 {
+ private val BodyParamsKey = "org.scalatra.fileupload.bodyParams"
+ case class BodyParams(fileParams: FileMultiParamsServlet3, formParams: Map[String, List[String]])
+}
@@ -0,0 +1,86 @@
+package org.scalatra.fileupload
+
+import scala.collection.JavaConversions._
+import org.scalatra.test.specs2.MutableScalatraSpec
+import org.scalatra.ScalatraServlet
+import java.io.File
+
+class FileUploadSupportServlet3TestServlet extends ScalatraServlet with FileUploadSupportServlet3 {
+ post("/upload") {
+ params.foreach(param =>
+ response.setHeader(param._1, param._2)
+ )
+
+ request.getHeaderNames.filter(_.startsWith("X")).foreach(header =>
+ response.setHeader(header, request.getHeader(header))
+ )
+
+ fileParams.foreach(fileParam => {
+ response.setHeader("File-" + fileParam._1 + "-Name", fileParam._2.name)
+ response.setHeader("File-" + fileParam._1 + "-Size", fileParam._2.size.toString)
+ response.setHeader("File-" + fileParam._1 + "-SHA", DigestUtils.shaHex(fileParam._2.bytes))
+ })
+
+ "post(/upload)"
+ }
+}
+
+class FileUploadSupportServlet3Test extends MutableScalatraSpec {
+ addServlet(classOf[FileUploadSupportServlet3TestServlet], "/*")
+ def postExample[A](f: => A): A = {
+ val params = Map("param1" -> "one", "param2" -> "two")
+ val files = Map(
+ "text" -> new File("fileupload/src/test/resources/org/scalatra/fileupload/lorem_ipsum.txt"),
+ "binary" -> new File("fileupload/src/test/resources/org/scalatra/fileupload/smiley.png")
+ )
+
+ val headers = Map(
+ "X-Header" -> "I'm a header",
+ "X-Header2" -> "I'm another header"
+ )
+
+ post("/upload?qsparam1=three&qsparam2=four", params, files, headers) { f }
+ }
+
+ "POST with multipart/form-data" should {
+ "routes correctly to action" in {
+ postExample {
+ status must_== 200
+ body must_== "post(/upload)"
+ }
+ }
+
+ "makes multipart form params available from params" in {
+ postExample {
+ header("param1") must_== "one"
+ header("param2") must_== "two"
+ }
+ }
+
+ "makes querystring params available from params" in {
+ postExample {
+ header("qsparam1") must_== "three"
+ header("qsparam2") must_== "four"
+ }
+ }
+
+ "does not twiddle with the headers" in {
+ postExample {
+ header("X-Header") must_== "I'm a header"
+ header("X-Header2") must_== "I'm another header"
+ }
+ }
+
+ "makes files available through fileParams" in {
+ postExample {
+ header("File-text-Name") must_== "lorem_ipsum.txt"
+ header("File-text-Size") must_== "651"
+ header("File-text-SHA") must_== "b3572a890c5005aed6409cf81d13fd19f6d004f0"
+
+ header("File-binary-Name") must_== "smiley.png"
+ header("File-binary-Size") must_== "3432"
+ header("File-binary-SHA") must_== "0e777b71581c631d056ee810b4550c5dcd9eb856"
+ }
+ }
+ }
+}

0 comments on commit c479375

Please sign in to comment.