Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
improvements to multipart file handling
- Loading branch information
1 parent
8c77e9a
commit 082bcd5
Showing
8 changed files
with
173 additions
and
63 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
core/src/main/scala/io/fintrospect/parameters/FormFieldExtractor.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package io.fintrospect.parameters | ||
|
||
import io.fintrospect.util.{Extracted, Extraction, ExtractionFailed, Extractor} | ||
|
||
sealed trait FormFieldExtractor { | ||
def apply(fields: Seq[Extractor[Form, _]], f: Form): Extraction[Form] | ||
} | ||
|
||
object WebFormFieldExtractor extends FormFieldExtractor { | ||
override def apply(fields: Seq[Extractor[Form, _]], t: Form): Extraction[Form] = Extracted(Some(t)) | ||
} | ||
|
||
object StrictFormFieldExtractor extends FormFieldExtractor { | ||
override def apply(fields: Seq[Extractor[Form, _]], form: Form): Extraction[Form] = Extraction.combine(fields.map(_.extract(form))) match { | ||
case failed@ExtractionFailed(_) => failed | ||
case _ => Extracted(Some(form)) | ||
} | ||
} | ||
|
35 changes: 35 additions & 0 deletions
35
core/src/main/scala/io/fintrospect/parameters/FormValidator.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package io.fintrospect.parameters | ||
|
||
import io.fintrospect.util._ | ||
|
||
/** | ||
* A strategy for validating the fields in a form | ||
*/ | ||
sealed trait FormValidator { | ||
def apply(fields: Seq[Extractor[Form, _]], form: Form): Form | ||
} | ||
|
||
/** | ||
* Web-forms are a less harsh version of forms, which report both a collection of received fields and a set of invalid fields. | ||
* This form-type is to be used for web forms (where feedback is desirable and the user can be redirected back to the form page). | ||
* As such, extracting an invalid webform from a request will not fail unless the body encoding itself is invalid. | ||
*/ | ||
class WebFormValidator(messages: Map[Parameter, String]) extends FormValidator { | ||
override def apply(fields: Seq[Extractor[Form, _]], rawForm: Form): Form = { | ||
new Form(rawForm.fields, rawForm.files, fields.flatMap { | ||
_ <--? rawForm match { | ||
case ExtractionFailed(e) => e.map(er => ExtractionError(er.param, messages.getOrElse(er.param, er.reason))) | ||
case _ => Nil | ||
} | ||
}) | ||
} | ||
} | ||
|
||
/** | ||
* Strict Forms fail when failing even a single field fails. | ||
* This form is used for non-web forms (where the posted form is merely an url-encoded set of form parameters) and | ||
* will auto-reject requests with a BadRequest. | ||
*/ | ||
object StrictFormValidator extends FormValidator { | ||
override def apply(fields: Seq[Extractor[Form, _]], rawForm: Form): Form = rawForm | ||
} |
49 changes: 49 additions & 0 deletions
49
core/src/main/scala/io/fintrospect/parameters/MultiPartFormBody.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package io.fintrospect.parameters | ||
|
||
import com.twitter.finagle.http._ | ||
import com.twitter.finagle.http.exp.Multipart.{FileUpload, InMemoryFileUpload, OnDiskFileUpload} | ||
import com.twitter.io.Buf | ||
import io.fintrospect.ContentTypes.MULTIPART_FORM | ||
import io.fintrospect.util.{Extraction, ExtractionError, ExtractionFailed, Extractor} | ||
|
||
import scala.util.{Failure, Success, Try} | ||
|
||
class MultiPartFormBody(fields: Seq[FormField[_] with Extractor[Form, _]], | ||
validator: FormValidator, extractor: FormFieldExtractor) | ||
extends Body[Form] { | ||
|
||
override val contentType = MULTIPART_FORM | ||
|
||
override def iterator = fields.iterator | ||
|
||
def -->(value: Form): Seq[RequestBinding] = | ||
Seq(new RequestBinding(null, req => { | ||
val fields = value.fields.flatMap(f => f._2.map(g => SimpleElement(f._1, g))).toSeq | ||
val files = value.files.flatMap(f => f._2.map(g => FileElement(f._1, g.content, g.contentType, g.filename))).toSeq | ||
|
||
val next = RequestBuilder() | ||
.url("http://notreallyaserver") | ||
.addHeaders(Map(req.headerMap.toSeq: _*)) | ||
.add(fields ++ files) | ||
.buildFormPost(multipart = true) | ||
next.uri = req.uri | ||
next | ||
})) | ||
|
||
override def <--?(message: Message): Extraction[Form] = message.asInstanceOf[Request].multipart | ||
.map(m => { | ||
val multipart = message.asInstanceOf[Request].multipart.get | ||
Try(validator(fields, Form(multipart.attributes.mapValues(_.toSet), multipart.files.mapValues(_.map(toMultipartFile).toSet)))) match { | ||
case Success(form) => extractor(fields, form) | ||
case Failure(e) => ExtractionFailed(fields.filter(_.required).map(param => ExtractionError(param, "Could not parse"))) | ||
} | ||
}).getOrElse(ExtractionFailed(fields.filter(_.required).map(param => ExtractionError(param, "Could not parse")))) | ||
|
||
private def toMultipartFile(f: FileUpload) = { | ||
f match { | ||
case InMemoryFileUpload(content, fileType, name, _) => MultiPartFile(content, Option(fileType), Option(name)) | ||
// FIXME - OnDiskUploads! | ||
case OnDiskFileUpload(_, fileType, name, _) => MultiPartFile(Buf.Empty, Option(fileType), Option(name)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 26 additions & 34 deletions
60
core/src/main/scala/io/fintrospect/parameters/UrlEncodedFormBody.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,60 +1,52 @@ | ||
package io.fintrospect.parameters | ||
|
||
import java.net.{URLDecoder, URLEncoder} | ||
|
||
import com.twitter.finagle.http._ | ||
import com.twitter.io.Buf | ||
import io.fintrospect.ContentTypes.{APPLICATION_FORM_URLENCODED, MULTIPART_FORM} | ||
import io.fintrospect.ContentTypes.APPLICATION_FORM_URLENCODED | ||
import io.fintrospect.util.{Extraction, ExtractionError, ExtractionFailed, Extractor} | ||
import org.jboss.netty.handler.codec.http.HttpHeaders.Names | ||
|
||
import scala.util.{Failure, Success, Try} | ||
|
||
class UrlEncodedFormBody(val fields: Seq[FormField[_] with Extractor[Form, _]], encodeDecode: FormCodec) | ||
class UrlEncodedFormBody(fields: Seq[FormField[_] with Extractor[Form, _]], | ||
validator: FormValidator, extractor: FormFieldExtractor) | ||
extends Body[Form] { | ||
|
||
override val contentType = APPLICATION_FORM_URLENCODED | ||
|
||
override def iterator = fields.iterator | ||
|
||
private def decodeFields(content: String): Map[String, Set[String]] = { | ||
content | ||
.split("&") | ||
.filter(_.contains("=")) | ||
.map(nvp => { | ||
val parts = nvp.split("=") | ||
(URLDecoder.decode(parts(0), "UTF-8"), if (parts.length > 1) URLDecoder.decode(parts(1), "UTF-8") else "") | ||
}) | ||
.groupBy(_._1) | ||
.mapValues(_.map(_._2)) | ||
.mapValues(_.toSet) | ||
} | ||
|
||
private def encode(form: Form): String = form.fields.flatMap { | ||
case (name, values) => values.map(value => URLEncoder.encode(name, "UTF-8") + "=" + URLEncoder.encode(value, "UTF-8")) | ||
}.mkString("&") | ||
|
||
|
||
def -->(value: Form): Seq[RequestBinding] = | ||
Seq(new RequestBinding(null, req => { | ||
val contentString = encodeDecode.encode(value) | ||
val contentString = encode(value) | ||
req.headerMap.add(Names.CONTENT_TYPE, contentType.value) | ||
req.headerMap.add(Names.CONTENT_LENGTH, contentString.length.toString) | ||
req.contentString = contentString | ||
req | ||
})) ++ fields.map(f => new FormFieldBinding(f, "")) | ||
|
||
override def <--?(message: Message): Extraction[Form] = | ||
Try(encodeDecode.decode(fields, message)) match { | ||
case Success(form) => encodeDecode.extract(fields, form) | ||
Try(validator(fields, new Form(decodeFields(message.contentString), Map.empty, Nil))) match { | ||
case Success(form) => extractor(fields, form) | ||
case Failure(e) => ExtractionFailed(fields.filter(_.required).map(param => ExtractionError(param, "Could not parse"))) | ||
} | ||
} | ||
|
||
class MultiPartFormBody(val fields: Seq[FormField[_] with Extractor[Form, _]], encodeDecode: FormCodec) | ||
extends Body[Form] { | ||
|
||
override val contentType = MULTIPART_FORM | ||
|
||
override def iterator = fields.iterator | ||
|
||
def -->(value: Form): Seq[RequestBinding] = | ||
Seq(new RequestBinding(null, req => { | ||
val fields = value.fields.flatMap(f => f._2.map(g => SimpleElement(f._1, g))).toSeq | ||
val files = value.files.flatMap(f => f._2.map(g => FileElement(f._1, null, null, null))).toSeq | ||
val headers = Map(req.headerMap.toSeq: _*) | ||
RequestBuilder().url(req.uri).addHeaders(headers).add(fields).add(files).buildFormPost(multipart = true) | ||
})) | ||
|
||
override def <--?(message: Message): Extraction[Form] = message.asInstanceOf[Request].multipart | ||
.map(m => { | ||
val multipart = message.asInstanceOf[Request].multipart.get | ||
Try(Form( | ||
multipart.attributes.mapValues(_.toSet), | ||
multipart.files.mapValues(_.map(f => MultiPartFile(Buf.Empty, Option(f.contentType), Option(f.fileName))).toSet) | ||
)) match { | ||
case Success(form) => encodeDecode.extract(fields, form) | ||
case Failure(e) => ExtractionFailed(fields.filter(_.required).map(param => ExtractionError(param, "Could not parse"))) | ||
} | ||
}).getOrElse(ExtractionFailed(fields.filter(_.required).map(param => ExtractionError(param, "Could not parse")))) | ||
} |
Oops, something went wrong.