Skip to content
This repository has been archived by the owner on Apr 10, 2019. It is now read-only.

Commit

Permalink
#150, refactor: Sound dependencies for play 2.5, WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
slavaschmidt committed Jun 16, 2016
1 parent 7ee4b96 commit a2ce73b
Show file tree
Hide file tree
Showing 30 changed files with 280 additions and 304 deletions.
3 changes: 1 addition & 2 deletions api-first-core/src/main/scala/de/zalando/apifirst/ast.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import de.zalando.apifirst.ParameterPlace.ParameterPlace
import de.zalando.apifirst.naming.{Path, Reference}

import scala.language.{implicitConversions, postfixOps}
import scala.util.parsing.input.Positional

sealed trait Expr

Expand Down Expand Up @@ -342,7 +341,7 @@ object Application {
default: Option[String],
constraint: String,
encode: Boolean,
place: ParameterPlace.Value) extends Expr with Positional
place: ParameterPlace.Value) extends Expr

case class HandlerCall(
packageName: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ object naming {
Reference(parts)
}
private def unescape(str: String) = str.replace("~1", "/").replace("~0", "~")
def deref(jstr: String) = Reference.fromUrl(unescape(jstr.reverse.takeWhile(_ != '#').reverse))
def deref(jstr: String): Reference = Reference.fromUrl(unescape(jstr.reverse.takeWhile(_ != '#').reverse))
}

case class Path(private val reference: Reference) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ object ScalaString {
case constraint: Security.OAuth2Constraint => oauth2Constraint(pad, constraint)
case constraint: Security.Constraint => securityConstraint(pad, constraint)
case definition: Security.ApiKey => apiKey(pad, definition)
case definition: Security.OAuth2Definition => oauthDef(pad, definition)
case definition: Security.OAuth2Definition => oauthDef(definition)
case definition: Security.Definition => securityDef(pad, definition)

case t: EnumTrait => enumTrait(pad, t)
Expand Down Expand Up @@ -150,7 +150,7 @@ object ScalaString {
val errorStr = if (call.errorMapping.isEmpty)
"Map.empty[String, Seq[Class[Exception]]]"
else call.errorMapping.map { case (k, v) =>
"\"" + k + "\" -> Seq(" + v.map(_.getCanonicalName).map("classOf[" + _ + "]").mkString(", ") + ")"
"\"" + k + "\" -> Seq(" + v.map("classOf[" + _.getCanonicalName + "]").mkString(", ") + ")"
}.mkString("Map(", ", ", ")")

val mimeInStr = set(call.mimeIn)
Expand All @@ -173,7 +173,7 @@ object ScalaString {
private def apiKey(pad: String, k: Security.ApiKey): String =
s"""ApiKey(${toScalaString("")(k.description)}, "${k.name}", ParameterPlace.withName("${k.in}"))"""

private def oauthDef(pad: String, d: Security.OAuth2Definition): String = {
private def oauthDef(d: Security.OAuth2Definition): String = {
val scopesStr = d.scopes.map{case (k,v) => s""" "$k" -> "${v.replace('\n', ' ')}" """}.mkString(", ")
s"""OAuth2Definition(${toScalaString("")(d.description)}, ${d.validationURL.map("new URL(\"" + _ + "\")")}, Map[String, String]($scopesStr))""".stripMargin
}
Expand Down Expand Up @@ -239,7 +239,7 @@ object ScalaString {
s"""Parameter("${p.name}", ${toScalaString("")(p.typeName)}, ${toScalaString("")(p.fixed)}, ${toScalaString("")(p.default)}, "${p.constraint}", encode = ${p.encode}, ParameterPlace.withName("${p.place}"))"""

private def handlerCall(pad: String, c: HandlerCall): String =
s"""HandlerCall(\n$pad\t"${c.packageName}",\n$pad\t"${c.controller}",\n$pad\tinstantiate = ${c.instantiate},\n$pad\t"${c.method}",parameters = ${c.parameters.map(toScalaString(pad + "\t\t")).mkString(s"\n$pad\tSeq(\n", s",\n", s"\n$pad\t\t)\n")}$pad\t)"""
s"""HandlerCall(\n$pad\t"${c.packageName}",\n$pad\t"${c.controller}",\n$pad\tinstantiate = ${c.instantiate},\n$pad\t"${c.method}",parameters = ${c.parameters.map(toScalaString(pad + "\t\t")).mkString(s"\n$pad\tSeq(\n", ",\n", s"\n$pad\t\t)\n")}$pad\t)"""

private def stateResponseInfo(pad: String, t: StateResponseInfo): String = {
val resStr =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package de.zalando.play.controllers

import java.nio.file.{Files, Paths}

import akka.util.ByteString
import play.api.http.{HeaderNames, Writeable}
import play.api.libs.Files.TemporaryFile
import play.api.mvc.MultipartFormData.FilePart
Expand All @@ -14,21 +15,21 @@ import play.api.mvc.{Codec, MultipartFormData}
* taken from <a href="http://tech.fongmun.com/post/125479939452/test-multipartformdata-in-play">here</a>
*/
object MultipartFormDataWritable {
import scala.concurrent.ExecutionContext.Implicits.global

val boundary = "--------ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"

def formatDataParts(data: Map[String, Seq[String]]): Array[Byte] = {
def formatDataParts(data: Map[String, Seq[String]]): ByteString = {
val dataParts = data.flatMap { case (key, values) =>
values.map { value =>
val name = s""""$key""""
s"--$boundary\r\n${HeaderNames.CONTENT_DISPOSITION}: form-data; name=$name\r\n\r\n$value\r\n"
}
}.mkString("")
Codec.utf_8.encode(dataParts)
val bytes: ByteString = Codec.utf_8.encode(dataParts)
bytes
}

def filePartHeader(file: FilePart[TemporaryFile]): Array[Byte] = {
def filePartHeader(file: FilePart[TemporaryFile]): ByteString = {
val name = s""""${file.key}""""
val filename = s""""${file.filename}""""
val contentType = file.contentType.map { ct =>
Expand Down
189 changes: 62 additions & 127 deletions api/src/main/scala/de/zalando/play/controllers/PlayBodyParsing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,142 +2,133 @@ package de.zalando.play.controllers

import java.io.File

import com.fasterxml.jackson.core.JsonFactory
import akka.util.ByteString
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.zalando.play.controllers.WrappedBodyParsers.Parser
import play.api.Logger
import play.api.http.Status._
import play.api.http._
import play.api.libs.Files.TemporaryFile
import play.api.libs.iteratee._
import play.api.mvc.{BodyParser, BodyParsers, QueryStringBindable, RequestHeader}
import play.api.mvc.MultipartFormData.FilePart
import play.api.mvc.Results.Status
import play.api.mvc._

import scala.concurrent.Future
import scala.language.implicitConversions
import scala.reflect.ClassTag
import scala.util.{Either, Left, Right}
import scala.util.control.NonFatal
import scala.util._

/**
* @since 02.09.2015
*/
object WriterFactories {
private val jsonFactory = new JsonFactory()

/**
* Contains proper Jackson Factories for different mime types
* JsonFactory is a default
*/
val factories: Map[String, JsonFactory] = Map(
"application/json" -> jsonFactory,
"text/x-yaml" -> new YAMLFactory() // TODO implement workaround for bug in yaml parser
).withDefaultValue(jsonFactory)
}
* @since 02.09.2015
*/
object PlayBodyParsing extends PlayBodyParsing {

import play.api.libs.iteratee.Execution.Implicits.trampoline

/**
* Returns proper jackson mapper for given mime type
* Returns proper jackson mapper for given mime type
*
* @param mimeType the mimeType of the required mapper
* @return
*/
* @param mimeType the mimeType of the required mapper
* @return
*/
def jacksonMapper(mimeType: String): ObjectMapper = {
//noinspection ScalaStyle
assert(mimeType != null)
val factory = WriterFactories.factories(mimeType)
val mapper = new ObjectMapper(factory)
mapper.registerModule(DefaultScalaModule)
mapper
}

import play.api.libs.iteratee.Execution.Implicits.trampoline
/**
* Parser factory for optional bodies
*
* @param mimeType name of the parser
* @param errorMsg error message to return if an input cannot be parsed
* @param mimeType name of the parser
* @param errorMsg error message to return if an input cannot be parsed
* @param maxLength the maximal length of the content
* @param tag the ClassTag to use at runtime
* @param tag the ClassTag to use at runtime
* @tparam T the type of the input the parser should be created for
* @return BodyParser for the type Option[T]
*/
def optionParser[T](mimeType: Option[MediaType] => String,
customParsers: Seq[(String, Parser[Option[T]])],
errorMsg: String, maxLength: Int = parse.DefaultMaxTextLength)
(implicit oTag: ClassTag[Option[T]], tag: ClassTag[T]): BodyParser[Option[T]] =
tolerantBodyParser[Option[T]](maxLength.toLong, errorMsg) { (requestHeader, bytes) =>
if (bytes.nonEmpty) {
parserCore(mimeType, customParsers, requestHeader, bytes)
} else
None
errorMsg: String,
maxLength: Long = parse.DefaultMaxTextLength.toLong)
(requestHeader: RequestHeader)
(implicit oTag: ClassTag[Option[T]], tag: ClassTag[T]): BodyParser[Option[T]] =
parse.raw(maxLength = maxLength).map {
_.asBytes(maxLength).flatMap { byteString =>
if (byteString.nonEmpty) {
parserCore(mimeType, customParsers, byteString, requestHeader.mediaType)
} else
None

}
}

/**
* Parser factory for any type
*
* @param mimeType name of the parser
* @param errorMsg error message to return if an input cannot be parsed
* @param maxLength the maximal length of the content
* @param tag the ClassTag to use at runtime
* @tparam T the type of the input the parser should be created for
* @return BodyParser for the type T
*/
* Parser factory for any type
*
* @param mimeType name of the parser
* @param errorMsg error message to return if an input cannot be parsed
* @param maxLength the maximal length of the content
* @param tag the ClassTag to use at runtime
* @tparam T the type of the input the parser should be created for
* @return BodyParser for the type T
*/
def anyParser[T](mimeType: Option[MediaType] => String,
customParsers: Seq[(String, Parser[T])],
errorMsg: String, maxLength: Int = parse.DefaultMaxTextLength)
errorMsg: String,
maxLength: Long = parse.DefaultMaxTextLength.toLong)
(requestHeader: RequestHeader)
(implicit tag: ClassTag[T]): BodyParser[T] =
tolerantBodyParser[T](maxLength.toLong, errorMsg) { (requestHeader, bytes) =>
parserCore(mimeType, customParsers, requestHeader, bytes)
parse.raw(maxLength = maxLength).map { rawBuffer =>
parserCore(mimeType, customParsers, rawBuffer.asBytes(maxLength).getOrElse(ByteString.empty), requestHeader.mediaType)
}

private def parserCore[T](mimeType: (Option[MediaType]) => String,
customParsers: Seq[(String, Parser[T])],
requestHeader: RequestHeader,
bytes: Array[Byte])(implicit tag: ClassTag[T]): T = {
val mimeTypeName = mimeType(requestHeader.mediaType)
val jacksonParser: (RequestHeader, Array[Byte]) => T =
(_, bytes) => jacksonMapper(mimeTypeName).readValue(bytes, tag.runtimeClass.asInstanceOf[Class[T]])
bytes: ByteString, mediaType: Option[MediaType])
(implicit tag: ClassTag[T]): T = {
val mimeTypeName = mimeType(mediaType)
val jacksonParser: Parser[T] =
byteString => jacksonMapper(mimeTypeName).readValue(bytes.toArray, tag.runtimeClass.asInstanceOf[Class[T]])
// TODO default play parsers could be used here as well
val parser = customParsers.find(_._1 == mimeTypeName).map(_._2).getOrElse {
jacksonParser
}
parser(requestHeader, bytes)
parser(bytes)
}

/**
* Converts parsing errors to Writeable
*/
* Converts parsing errors to Writeable
*/
def parsingErrors2Writable(mimeType: String): Writeable[Seq[ParsingError]] =
Writeable(parsingErrors2Bytes(mimeType), Some(mimeType))


def anyToWritable[T <: Any]: String => Writeable[T] = mimeType =>
Writeable(jacksonMapper(mimeType).writeValueAsBytes, Some(mimeType))
Writeable(w => ByteString(jacksonMapper(mimeType).writeValueAsBytes(w)), Some(mimeType))

/**
* Converts anything of type Either[Throwable, T] to Writeable
*/
* Converts anything of type Either[Throwable, T] to Writeable
*/
def eitherToWritable[T](mimeType: String): Writeable[Either[Throwable, T]] =
Writeable(eitherToT(mimeType), Some(mimeType))

private def eitherToT[T](mimeType: String): (Either[Throwable, T]) => Array[Byte] =
private def eitherToT[T](mimeType: String): (Either[Throwable, T]) => ByteString =
(t: Either[Throwable, T]) => {
val result = t match {
case Right(rt) => rt
case Left(throwable) => throwable.getLocalizedMessage
}
jacksonMapper(mimeType).writeValueAsBytes(result)
ByteString(jacksonMapper(mimeType).writeValueAsBytes(result))
}

private def parsingErrors2Bytes(mimeType: String): Seq[ParsingError] => Array[Byte] = errors =>
jacksonMapper(mimeType).writeValueAsBytes(errors)
private def parsingErrors2Bytes(mimeType: String): Seq[ParsingError] => ByteString = errors =>
ByteString(jacksonMapper(mimeType).writeValueAsBytes(errors))

implicit def writers[T]: String => Option[Writeable[T]] =
mimeType => util.Try(Some(PlayBodyParsing.anyToWritable[T](mimeType))).recover {
mimeType => Try(Some(PlayBodyParsing.anyToWritable[T](mimeType))).recover {
case _: java.util.NoSuchElementException => None
}.get
}
Expand All @@ -153,7 +144,7 @@ trait PlayBodyParsing extends BodyParsers {
val onlySecond = m2.filterKeys(!m1.keySet.contains(_))
val both = m1.filterKeys(m2.keySet.contains)
val merged = both map { case (code, f) =>
code -> f.orElse(m2(code))
code -> f.orElse(m2(code))
}
onlyFirst ++ onlySecond ++ merged
}
Expand All @@ -172,77 +163,21 @@ trait PlayBodyParsing extends BodyParsers {
/**
* Helper method to parse parameters sent as Headers
*/
def fromParameters[T](place: String)(key: String, headers: Map[String, Seq[String]], default: Option[T] = None)(implicit binder: QueryStringBindable[T]): Either[String,T] =
def fromParameters[T](place: String)(key: String, headers: Map[String, Seq[String]], default: Option[T] = None)
(implicit binder: QueryStringBindable[T]): Either[String, T] =
binder.bind(key, headers).getOrElse {
default.map(d => Right(d)).getOrElse(Left(s"Missing $place parameter(s) for '$key'"))
}

/**
* Helper methods to parse files
*/
def fromFileOptional[T <: Option[File]](name: String, file: Option[FilePart[TemporaryFile]]) = Right(file.map(_.ref.file))
def fromFileOptional[T <: Option[File]](name: String, file: Option[FilePart[TemporaryFile]]): Either[Nothing, Option[File]] =
Right(file.map(_.ref.file))

def fromFileRequired[T <: File](name: String, file: Option[FilePart[TemporaryFile]]) = file match {
def fromFileRequired[T <: File](name: String, file: Option[FilePart[TemporaryFile]]): Either[String, File] = file match {
case Some(filePart) => Right(filePart.ref.file)
case None => Left(s"Missing file parameter for '$name'")
}

/**
* This is private in play codebase. Copy-pasted it.
*/
def tolerantBodyParser[A](maxLength: Long, errorMessage: String)(parser: (RequestHeader, Array[Byte]) => A): BodyParser[A] =
BodyParser(errorMessage + ", maxLength=" + maxLength) { request =>
import play.api.libs.iteratee.Execution.Implicits.trampoline

import scala.util.control.Exception._

val bodyParser: Iteratee[Array[Byte], Either[Result, Either[Future[Result], A]]] =
Traversable.takeUpTo[Array[Byte]](maxLength).transform(
Iteratee.consume[Array[Byte]]().map { bytes =>
allCatch[A].either {
parser(request, bytes)
}.left.map {
case NonFatal(e) =>
// logger.debug(errorMessage, e)
createBadResult(errorMessage + ": " + e.getMessage)(request)
case t => throw t
}
}
).flatMap(checkForEof(request))

bodyParser.mapM {
case Left(tooLarge) => Future.successful(Left(tooLarge))
case Right(Left(badResult)) => badResult.map(Left.apply)
case Right(Right(body)) => Future.successful(Right(body))
}
}

/**
*
* This is private in play codebase. Copy-pasted it.
*
* Check that the input is finished. If it is finished, the iteratee returns `eofValue`.
* If the input is not finished then it returns a REQUEST_ENTITY_TOO_LARGE result.
*/
private def checkForEof[A](request: RequestHeader): A => Iteratee[Array[Byte], Either[Result, A]] = { eofValue: A =>
import play.api.libs.iteratee.Execution.Implicits.trampoline
def cont: Iteratee[Array[Byte], Either[Result, A]] = Cont {
case in@Input.El(e) =>
val badResult: Future[Result] = createBadResult("Request Entity Too Large", REQUEST_ENTITY_TOO_LARGE)(request)
Iteratee.flatten(badResult.map(r => Done(Left(r), in)))
case in@Input.EOF =>
Done(Right(eofValue), in)
case Input.Empty =>
cont
}
cont
}

/**
* This is private in play codebase. Copy-pasted it.
*/
private def createBadResult(msg: String, statusCode: Int = BAD_REQUEST): RequestHeader => Future[Result] = { request =>
LazyHttpErrorHandler.onClientError(request, statusCode, msg)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ object PlayPathBindables {

def tempFileFromString(s: String): File = {
val prefix = "tmp_" + s.hashCode
val f = File.createTempFile(prefix.toString, "")
val f = File.createTempFile(prefix, "")
f.deleteOnExit()
import java.nio.file.{Paths, Files}
import java.nio.charset.StandardCharsets
Expand Down
Loading

0 comments on commit a2ce73b

Please sign in to comment.