From 2941a55401b8d6f76dcac2072963698244ef9574 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 4 Nov 2014 15:54:10 +0100 Subject: [PATCH] !htp #15656 split out scala-xml support into its own subproject --- .../http/testkit/MarshallingTestUtils.scala | 31 ++++++++++++++++ .../scala/akka/http/testkit/RouteTest.scala | 5 +-- .../akka/http/testkit/ScalatestUtils.scala | 32 ++++++++++++++++ .../marshallers/xml/ScalaXmlSupportSpec.scala | 37 +++++++++++++++++++ .../http/marshalling/MarshallingSpec.scala | 14 ++----- .../scala/akka/http/server/TestServer.scala | 6 ++- .../directives/RouteDirectivesSpec.scala | 1 + .../unmarshalling/UnmarshallingSpec.scala | 10 +---- .../akka/http/marshalling/Marshallers.scala | 11 +----- .../PredefinedToEntityMarshallers.scala | 4 -- .../PredefinedFromEntityUnmarshallers.scala | 19 ---------- .../http/unmarshalling/Unmarshaller.scala | 13 ++++++- 12 files changed, 126 insertions(+), 57 deletions(-) create mode 100644 akka-http-testkit/src/main/scala/akka/http/testkit/MarshallingTestUtils.scala create mode 100644 akka-http-testkit/src/main/scala/akka/http/testkit/ScalatestUtils.scala create mode 100644 akka-http-tests/src/test/scala/akka/http/marshallers/xml/ScalaXmlSupportSpec.scala diff --git a/akka-http-testkit/src/main/scala/akka/http/testkit/MarshallingTestUtils.scala b/akka-http-testkit/src/main/scala/akka/http/testkit/MarshallingTestUtils.scala new file mode 100644 index 0000000000..e2c7589d1d --- /dev/null +++ b/akka-http-testkit/src/main/scala/akka/http/testkit/MarshallingTestUtils.scala @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.testkit + +import akka.http.unmarshalling.{ Unmarshal, FromEntityUnmarshaller } + +import scala.concurrent.duration._ +import scala.concurrent.{ ExecutionContext, Await } + +import akka.http.marshalling._ +import akka.http.model.HttpEntity +import akka.stream.FlowMaterializer + +import scala.util.Try + +trait MarshallingTestUtils { + def marshal[T: ToEntityMarshallers](value: T)(implicit ec: ExecutionContext, mat: FlowMaterializer): HttpEntity.Strict = + Await.result(Marshal(value).to[HttpEntity].flatMap(_.toStrict(1.second)), 1.second) + + def unmarshalValue[T: FromEntityUnmarshaller](entity: HttpEntity)(implicit ec: ExecutionContext, mat: FlowMaterializer): T = + unmarshal(entity).get + + def unmarshal[T: FromEntityUnmarshaller](entity: HttpEntity)(implicit ec: ExecutionContext, mat: FlowMaterializer): Try[T] = { + val fut = Unmarshal(entity).to[T] + Await.ready(fut, 1.second) + fut.value.get + } +} + diff --git a/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTest.scala b/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTest.scala index 3edf4a4def..8a2fe0f385 100644 --- a/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTest.scala +++ b/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTest.scala @@ -10,7 +10,6 @@ import scala.concurrent.{ Await, Future } import scala.concurrent.duration._ import scala.util.DynamicVariable import scala.reflect.ClassTag -import org.scalatest.Suite import akka.actor.ActorSystem import akka.stream.FlowMaterializer import akka.http.client.RequestBuilding @@ -21,7 +20,7 @@ import akka.http.model._ import headers.Host import FastFuture._ -trait RouteTest extends RequestBuilding with RouteTestResultComponent { +trait RouteTest extends RequestBuilding with RouteTestResultComponent with MarshallingTestUtils { this: TestFrameworkInterface ⇒ /** Override to supply a custom ActorSystem */ @@ -142,6 +141,4 @@ trait RouteTest extends RequestBuilding with RouteTestResultComponent { } } -trait ScalatestRouteTest extends RouteTest with TestFrameworkInterface.Scalatest { this: Suite ⇒ } - //FIXME: trait Specs2RouteTest extends RouteTest with Specs2Interface \ No newline at end of file diff --git a/akka-http-testkit/src/main/scala/akka/http/testkit/ScalatestUtils.scala b/akka-http-testkit/src/main/scala/akka/http/testkit/ScalatestUtils.scala new file mode 100644 index 0000000000..b0ee033e10 --- /dev/null +++ b/akka-http-testkit/src/main/scala/akka/http/testkit/ScalatestUtils.scala @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.testkit + +import akka.http.model.HttpEntity +import akka.http.unmarshalling.FromEntityUnmarshaller +import akka.stream.FlowMaterializer +import org.scalatest.Suite +import org.scalatest.matchers.Matcher + +import scala.concurrent.duration._ +import scala.concurrent.{ ExecutionContext, Future, Await } +import scala.util.Try + +trait ScalatestUtils extends MarshallingTestUtils { + import org.scalatest.Matchers._ + def evaluateTo[T](value: T): Matcher[Future[T]] = + equal(value).matcher[T] compose (x ⇒ Await.result(x, 1.second)) + + def haveFailedWith(t: Throwable): Matcher[Future[_]] = + equal(t).matcher[Throwable] compose (x ⇒ Await.result(x.failed, 1.second)) + + def unmarshalToValue[T: FromEntityUnmarshaller](value: T)(implicit ec: ExecutionContext, mat: FlowMaterializer): Matcher[HttpEntity] = + equal(value).matcher[T] compose (unmarshalValue(_)) + + def unmarshalTo[T: FromEntityUnmarshaller](value: Try[T])(implicit ec: ExecutionContext, mat: FlowMaterializer): Matcher[HttpEntity] = + equal(value).matcher[Try[T]] compose (unmarshal(_)) +} + +trait ScalatestRouteTest extends RouteTest with TestFrameworkInterface.Scalatest with ScalatestUtils { this: Suite ⇒ } \ No newline at end of file diff --git a/akka-http-tests/src/test/scala/akka/http/marshallers/xml/ScalaXmlSupportSpec.scala b/akka-http-tests/src/test/scala/akka/http/marshallers/xml/ScalaXmlSupportSpec.scala new file mode 100644 index 0000000000..9d60849d47 --- /dev/null +++ b/akka-http-tests/src/test/scala/akka/http/marshallers/xml/ScalaXmlSupportSpec.scala @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.marshallers.xml + +import akka.http.unmarshalling.Unmarshal + +import akka.http.testkit.ScalatestRouteTest +import akka.http.unmarshalling.UnmarshallingError.UnsupportedContentType +import org.scalatest.{ Matchers, WordSpec } + +import akka.http.model.HttpCharsets._ +import akka.http.model.MediaTypes._ +import akka.http.model.{ ContentTypeRange, ContentType, HttpEntity } + +import akka.http.marshalling.{ ToEntityMarshallers, Marshal } + +import scala.xml.NodeSeq + +class ScalaXmlSupportSpec extends WordSpec with Matchers with ScalatestRouteTest { + import ScalaXmlSupport._ + + "ScalaXmlSupport" should { + "NodeSeqMarshaller should marshal xml snippets to `text/xml` content in UTF-8" in { + marshal(Ha“llo) shouldEqual + HttpEntity(ContentType(`text/xml`, `UTF-8`), "Ha“llo") + } + "nodeSeqUnmarshaller should unmarshal `text/xml` content in UTF-8 to NodeSeqs" in { + Unmarshal(HttpEntity(`text/xml`, "Hällö")).to[NodeSeq].map(_.text) should evaluateTo("Hällö") + } + "nodeSeqUnmarshaller should reject `application/octet-stream`" in { + Unmarshal(HttpEntity(`application/octet-stream`, "Hällö")).to[NodeSeq].map(_.text) should + haveFailedWith(UnsupportedContentType(ScalaXmlSupport.nodeSeqMediaTypes map (ContentTypeRange(_)))) + } + } +} diff --git a/akka-http-tests/src/test/scala/akka/http/marshalling/MarshallingSpec.scala b/akka-http-tests/src/test/scala/akka/http/marshalling/MarshallingSpec.scala index 6f75f59271..b43ba8047d 100644 --- a/akka-http-tests/src/test/scala/akka/http/marshalling/MarshallingSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/marshalling/MarshallingSpec.scala @@ -4,9 +4,10 @@ package akka.http.marshalling +import akka.http.testkit.MarshallingTestUtils +import akka.http.marshallers.xml.ScalaXmlSupport._ + import scala.collection.immutable.ListMap -import scala.concurrent.Await -import scala.concurrent.duration._ import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers } import akka.actor.ActorSystem import akka.stream.FlowMaterializer @@ -17,7 +18,7 @@ import headers._ import HttpCharsets._ import MediaTypes._ -class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with MultipartMarshallers { +class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with MultipartMarshallers with MarshallingTestUtils { implicit val system = ActorSystem(getClass.getSimpleName) implicit val materializer = FlowMaterializer() import system.dispatcher @@ -29,10 +30,6 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with "CharArrayMarshaller should marshal char arrays to `text/plain` content in UTF-8" in { marshal("Ha“llo".toCharArray) shouldEqual HttpEntity("Ha“llo") } - "NodeSeqMarshaller should marshal xml snippets to `text/xml` content in UTF-8" in { - marshal(Ha“llo) shouldEqual - HttpEntity(ContentType(`text/xml`, `UTF-8`), "Ha“llo") - } "FormDataMarshaller should marshal FormData instances to application/x-www-form-urlencoded content" in { marshal(FormData(Map("name" -> "Bob", "pass" -> "hällo", "admin" -> ""))) shouldEqual HttpEntity(ContentType(`application/x-www-form-urlencoded`, `UTF-8`), "name=Bob&pass=h%C3%A4llo&admin=") @@ -145,9 +142,6 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with override def afterAll() = system.shutdown() - def marshal[T: ToEntityMarshallers](value: T): HttpEntity.Strict = - Await.result(Await.result(Marshal(value).to[HttpEntity], 1.second).toStrict(1.second), 1.second) - protected class FixedRandom extends java.util.Random { override def nextBytes(array: Array[Byte]): Unit = "my-stable-boundary".getBytes("UTF-8").copyToArray(array) } diff --git a/akka-http-tests/src/test/scala/akka/http/server/TestServer.scala b/akka-http-tests/src/test/scala/akka/http/server/TestServer.scala index e966ef07f6..c3ec4475fa 100644 --- a/akka-http-tests/src/test/scala/akka/http/server/TestServer.scala +++ b/akka-http-tests/src/test/scala/akka/http/server/TestServer.scala @@ -4,7 +4,7 @@ package akka.http.server -import akka.http.marshalling.Marshaller +import akka.http.marshallers.xml.ScalaXmlSupport import akka.http.server.directives.AuthenticationDirectives._ import com.typesafe.config.{ ConfigFactory, Config } import scala.concurrent.duration._ @@ -36,7 +36,9 @@ object TestServer extends App { case _ ⇒ false } - implicit val html = Marshaller.nodeSeqMarshaller(MediaTypes.`text/html`) + // FIXME: a simple `import ScalaXmlSupport._` should suffice but currently doesn't because + // of #16190 + implicit val html = ScalaXmlSupport.nodeSeqMarshaller(MediaTypes.`text/html`) handleConnections(bindingFuture) withRoute { get { diff --git a/akka-http-tests/src/test/scala/akka/http/server/directives/RouteDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/server/directives/RouteDirectivesSpec.scala index ed754e197f..bd5d2670de 100644 --- a/akka-http-tests/src/test/scala/akka/http/server/directives/RouteDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/server/directives/RouteDirectivesSpec.scala @@ -7,6 +7,7 @@ package akka.http.server.directives import org.scalatest.FreeSpec import scala.concurrent.Promise +import akka.http.marshallers.xml.ScalaXmlSupport._ import akka.http.marshalling._ import akka.http.server._ import akka.http.model._ diff --git a/akka-http-tests/src/test/scala/akka/http/unmarshalling/UnmarshallingSpec.scala b/akka-http-tests/src/test/scala/akka/http/unmarshalling/UnmarshallingSpec.scala index 190f95580f..e2547d4a05 100644 --- a/akka-http-tests/src/test/scala/akka/http/unmarshalling/UnmarshallingSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/unmarshalling/UnmarshallingSpec.scala @@ -4,9 +4,9 @@ package akka.http.unmarshalling +import akka.http.testkit.ScalatestUtils import akka.util.ByteString -import scala.xml.NodeSeq import scala.concurrent.duration._ import scala.concurrent.{ Future, Await } import org.scalatest.matchers.Matcher @@ -20,7 +20,7 @@ import headers._ import MediaTypes._ import FastFuture._ -class UnmarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll { +class UnmarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with ScalatestUtils { implicit val system = ActorSystem(getClass.getSimpleName) implicit val materializer = FlowMaterializer() import system.dispatcher @@ -32,9 +32,6 @@ class UnmarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll { "charArrayUnmarshaller should unmarshal `text/plain` content in UTF-8 to char arrays" in { Unmarshal(HttpEntity("árvíztűrő ütvefúrógép")).to[Array[Char]] should evaluateTo("árvíztűrő ütvefúrógép".toCharArray) } - "nodeSeqUnmarshaller should unmarshal `text/xml` content in UTF-8 to NodeSeqs" in { - Unmarshal(HttpEntity(`text/xml`, "Hällö")).to[NodeSeq].fast.map(_.text) should evaluateTo("Hällö") - } } "The MultipartUnmarshallers." - { @@ -213,9 +210,6 @@ class UnmarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll { override def afterAll() = system.shutdown() - def evaluateTo[T](value: T): Matcher[Future[T]] = - equal(value).matcher[T] compose (x ⇒ Await.result(x, 1.second)) - def haveParts[T <: Multipart](parts: Multipart.BodyPart*): Matcher[Future[T]] = equal(parts).matcher[Seq[Multipart.BodyPart]] compose { x ⇒ Await.result(x diff --git a/akka-http/src/main/scala/akka/http/marshalling/Marshallers.scala b/akka-http/src/main/scala/akka/http/marshalling/Marshallers.scala index 36c7361178..9b1445e151 100644 --- a/akka-http/src/main/scala/akka/http/marshalling/Marshallers.scala +++ b/akka-http/src/main/scala/akka/http/marshalling/Marshallers.scala @@ -7,11 +7,9 @@ package akka.http.marshalling import scala.collection.immutable import scala.concurrent.{ Future, ExecutionContext } import scala.util.control.NonFatal -import scala.xml.NodeSeq import akka.http.util.FastFuture import akka.http.model._ import FastFuture._ -import MediaTypes._ case class Marshallers[-A, +B](marshallers: immutable.Seq[Marshaller[A, B]]) { require(marshallers.nonEmpty, "marshallers must be non-empty") @@ -22,13 +20,8 @@ case class Marshallers[-A, +B](marshallers: immutable.Seq[Marshaller[A, B]]) { object Marshallers extends SingleMarshallerMarshallers { def apply[A, B](m: Marshaller[A, B]): Marshallers[A, B] = apply(m :: Nil) def apply[A, B](first: Marshaller[A, B], more: Marshaller[A, B]*): Marshallers[A, B] = apply(first +: more.toVector) - def apply[A, B](first: MediaType, more: MediaType*)(f: MediaType ⇒ Marshaller[A, B]): Marshallers[A, B] = { - val vector: Vector[Marshaller[A, B]] = more.map(f)(collection.breakOut) - Marshallers(f(first) +: vector) - } - - implicit def nodeSeqMarshallers(implicit ec: ExecutionContext): ToEntityMarshallers[NodeSeq] = - Marshallers(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`)(PredefinedToEntityMarshallers.nodeSeqMarshaller) + def apply[A, B](mediaTypes: MediaType*)(f: MediaType ⇒ Marshaller[A, B]): Marshallers[A, B] = + Marshallers(mediaTypes.map(f)(collection.breakOut)) implicit def entity2response[T](implicit m: Marshallers[T, ResponseEntity], ec: ExecutionContext): Marshallers[T, HttpResponse] = m map (entity ⇒ HttpResponse(entity = entity)) diff --git a/akka-http/src/main/scala/akka/http/marshalling/PredefinedToEntityMarshallers.scala b/akka-http/src/main/scala/akka/http/marshalling/PredefinedToEntityMarshallers.scala index 7b6849336d..9e3145472d 100644 --- a/akka-http/src/main/scala/akka/http/marshalling/PredefinedToEntityMarshallers.scala +++ b/akka-http/src/main/scala/akka/http/marshalling/PredefinedToEntityMarshallers.scala @@ -6,7 +6,6 @@ package akka.http.marshalling import java.nio.CharBuffer import scala.concurrent.ExecutionContext -import scala.xml.NodeSeq import akka.http.model.parser.CharacterClasses import akka.http.model.MediaTypes._ import akka.http.model._ @@ -53,9 +52,6 @@ trait PredefinedToEntityMarshallers extends MultipartMarshallers { def stringMarshaller(mediaType: MediaType): ToEntityMarshaller[String] = Marshaller.withOpenCharset(mediaType) { (s, cs) ⇒ HttpEntity(ContentType(mediaType, cs), s) } - implicit def nodeSeqMarshaller(mediaType: MediaType)(implicit ec: ExecutionContext): ToEntityMarshaller[NodeSeq] = - StringMarshaller.wrap(mediaType)(_.toString()) - implicit val FormDataMarshaller: ToEntityMarshaller[FormData] = Marshaller.withOpenCharset(`application/x-www-form-urlencoded`) { (formData, charset) ⇒ val query = Uri.Query(formData.fields: _*) diff --git a/akka-http/src/main/scala/akka/http/unmarshalling/PredefinedFromEntityUnmarshallers.scala b/akka-http/src/main/scala/akka/http/unmarshalling/PredefinedFromEntityUnmarshallers.scala index c7502c24a4..9d1775e8d0 100644 --- a/akka-http/src/main/scala/akka/http/unmarshalling/PredefinedFromEntityUnmarshallers.scala +++ b/akka-http/src/main/scala/akka/http/unmarshalling/PredefinedFromEntityUnmarshallers.scala @@ -4,15 +4,11 @@ package akka.http.unmarshalling -import java.io.{ ByteArrayInputStream, InputStreamReader } import scala.concurrent.ExecutionContext -import scala.xml.{ XML, NodeSeq } import akka.stream.FlowMaterializer -import akka.stream.scaladsl._ import akka.util.ByteString import akka.http.util.FastFuture import akka.http.model._ -import MediaTypes._ trait PredefinedFromEntityUnmarshallers extends MultipartUnmarshallers { @@ -42,21 +38,6 @@ trait PredefinedFromEntityUnmarshallers extends MultipartUnmarshallers { bytes.decodeString(entity.contentType.charset.nioCharset.name) // ouch!!! } - private val nodeSeqMediaTypes = List(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`) - implicit def nodeSeqUnmarshaller(implicit fm: FlowMaterializer, - ec: ExecutionContext): FromEntityUnmarshaller[NodeSeq] = - byteArrayUnmarshaller flatMapWithInput { (entity, bytes) ⇒ - if (nodeSeqMediaTypes contains entity.contentType.mediaType) { - val parser = XML.parser - try parser.setProperty("http://apache.org/xml/properties/locale", java.util.Locale.ROOT) - catch { - case e: org.xml.sax.SAXNotRecognizedException ⇒ // property is not needed - } - val reader = new InputStreamReader(new ByteArrayInputStream(bytes), entity.contentType.charset.nioCharset) - FastFuture.successful(XML.withSAXParser(parser).load(reader)) // blocking call! Ideally we'd have a `loadToFuture` - } else FastFuture.failed(UnmarshallingError.UnsupportedContentType(nodeSeqMediaTypes map (ContentTypeRange(_)))) - } - implicit def urlEncodedFormDataUnmarshaller(implicit fm: FlowMaterializer, ec: ExecutionContext): FromEntityUnmarshaller[FormData] = stringUnmarshaller mapWithInput { (entity, string) ⇒ diff --git a/akka-http/src/main/scala/akka/http/unmarshalling/Unmarshaller.scala b/akka-http/src/main/scala/akka/http/unmarshalling/Unmarshaller.scala index c8e44fa293..4011a90686 100644 --- a/akka-http/src/main/scala/akka/http/unmarshalling/Unmarshaller.scala +++ b/akka-http/src/main/scala/akka/http/unmarshalling/Unmarshaller.scala @@ -8,7 +8,7 @@ import scala.util.control.NonFatal import scala.collection.immutable import scala.concurrent.{ Future, ExecutionContext } import akka.http.util._ -import akka.http.model.ContentTypeRange +import akka.http.model.{ HttpCharset, MediaType, ContentTypeRange } import FastFuture._ trait Unmarshaller[-A, B] extends (A ⇒ Future[B]) { @@ -53,6 +53,17 @@ object Unmarshaller def flatMapWithInput[C](f: (A, B) ⇒ Future[C])(implicit ec: ExecutionContext): Unmarshaller[A, C] = Unmarshaller(a ⇒ um(a).fast.flatMap(f(a, _))) } + + implicit class EnhancedToEntityUnmarshaller[T](val um: FromEntityUnmarshaller[T]) extends AnyVal { + def mapWithCharset[U](f: (T, HttpCharset) ⇒ U)(implicit ec: ExecutionContext): FromEntityUnmarshaller[U] = + um.mapWithInput { (entity, data) ⇒ f(data, entity.contentType.charset) } + + def filterMediaType(allowed: MediaType*)(implicit ec: ExecutionContext): FromEntityUnmarshaller[T] = + um.flatMapWithInput { (entity, data) ⇒ + if (allowed contains entity.contentType.mediaType) Future.successful(data) + else FastFuture.failed(UnmarshallingError.UnsupportedContentType(allowed map (ContentTypeRange(_)) toList)) + } + } } sealed abstract class UnmarshallingError(msg: String, cause: Throwable = null) extends RuntimeException(msg, cause)