From c401d8a2fe61823418896340615500b04e5ffe41 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 14 Jul 2015 12:21:54 +0200 Subject: [PATCH] +htp #16439 allow creation of custom marshallers on the java side --- .../http/javadsl/server/MarshallerTest.java | 208 ++++++++++++++++++ .../scala/akka/http/impl/server/Util.scala | 14 +- .../http/javadsl/server/Marshallers.scala | 52 ++++- 3 files changed, 269 insertions(+), 5 deletions(-) create mode 100644 akka-http-tests/src/test/java/akka/http/javadsl/server/MarshallerTest.java diff --git a/akka-http-tests/src/test/java/akka/http/javadsl/server/MarshallerTest.java b/akka-http-tests/src/test/java/akka/http/javadsl/server/MarshallerTest.java new file mode 100644 index 0000000000..45b09cd6e4 --- /dev/null +++ b/akka-http-tests/src/test/java/akka/http/javadsl/server/MarshallerTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2009-2015 Typesafe Inc. + */ + +package akka.http.javadsl.server; + +import akka.http.javadsl.model.*; +import akka.http.javadsl.model.headers.Accept; +import akka.http.javadsl.model.headers.AcceptCharset; +import akka.http.javadsl.server.values.Parameters; +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.testkit.TestRoute; +import akka.japi.function.Function; +import akka.util.ByteString; +import org.junit.Test; + +public class MarshallerTest extends JUnitRouteTest { + RequestVal n = Parameters.intValue("n"); + + @Test + public void testCustomToStringMarshaller() { + final Marshaller numberAsNameMarshaller = + Marshallers.toEntityString(MediaTypes.TEXT_X_SPEECH, new Function() { + @Override + public String apply(Integer param) throws Exception { + switch(param) { + case 0: return "null"; + case 1: return "eins"; + case 2: return "zwei"; + case 3: return "drei"; + case 4: return "vier"; + case 5: return "fünf"; + default: return "wat?"; + } + } + }); + + Handler1 nummerHandler = new Handler1() { + @Override + public RouteResult handle(RequestContext ctx, Integer integer) { + return ctx.completeAs(numberAsNameMarshaller, integer); + } + }; + + TestRoute route = + testRoute( + get( + path("nummer").route( + handleWith(n, nummerHandler) + ) + ) + ); + + route.run(HttpRequest.GET("/nummer?n=1")) + .assertStatusCode(200) + .assertMediaType(MediaTypes.TEXT_X_SPEECH) + .assertEntity("eins"); + + route.run(HttpRequest.GET("/nummer?n=6")) + .assertStatusCode(200) + .assertMediaType(MediaTypes.TEXT_X_SPEECH) + .assertEntity("wat?"); + + route.run(HttpRequest.GET("/nummer?n=5")) + .assertStatusCode(200) + .assertEntityBytes(ByteString.fromString("fünf", "utf8")); + + route.run( + HttpRequest.GET("/nummer?n=5") + .addHeader(AcceptCharset.create(HttpCharsets.ISO_8859_1.toRange()))) + .assertStatusCode(200) + .assertEntityBytes(ByteString.fromString("fünf", "ISO-8859-1")); + } + + @Test + public void testCustomToByteStringMarshaller() { + final Marshaller numberAsJsonListMarshaller = + Marshallers.toEntityByteString(MediaTypes.APPLICATION_JSON.toContentType(), new Function() { + @Override + public ByteString apply(Integer param) throws Exception { + switch(param) { + case 1: return ByteString.fromString("[1]"); + case 5: return ByteString.fromString("[1,2,3,4,5]"); + default: return ByteString.fromString("[]"); + } + } + }); + + Handler1 nummerHandler = new Handler1() { + @Override + public RouteResult handle(RequestContext ctx, Integer integer) { + return ctx.completeAs(numberAsJsonListMarshaller, integer); + } + }; + + TestRoute route = + testRoute( + get( + path("nummer").route( + handleWith(n, nummerHandler) + ) + ) + ); + + route.run(HttpRequest.GET("/nummer?n=1")) + .assertStatusCode(200) + .assertMediaType(MediaTypes.APPLICATION_JSON) + .assertEntity("[1]"); + + route.run(HttpRequest.GET("/nummer?n=5")) + .assertStatusCode(200) + .assertEntity("[1,2,3,4,5]"); + + route.run( + HttpRequest.GET("/nummer?n=5").addHeader(Accept.create(MediaTypes.TEXT_PLAIN.toRange()))) + .assertStatusCode(406); + } + + @Test + public void testCustomToEntityMarshaller() { + final Marshaller numberAsJsonListMarshaller = + Marshallers.toEntity(MediaTypes.APPLICATION_JSON.toContentType(), new Function() { + @Override + public ResponseEntity apply(Integer param) throws Exception { + switch(param) { + case 1: return HttpEntities.create(MediaTypes.APPLICATION_JSON.toContentType(), "[1]"); + case 5: return HttpEntities.create(MediaTypes.APPLICATION_JSON.toContentType(), "[1,2,3,4,5]"); + default: return HttpEntities.create(MediaTypes.APPLICATION_JSON.toContentType(), "[]"); + } + } + }); + + Handler1 nummerHandler = new Handler1() { + @Override + public RouteResult handle(RequestContext ctx, Integer integer) { + return ctx.completeAs(numberAsJsonListMarshaller, integer); + } + }; + + TestRoute route = + testRoute( + get( + path("nummer").route( + handleWith(n, nummerHandler) + ) + ) + ); + + route.run(HttpRequest.GET("/nummer?n=1")) + .assertStatusCode(200) + .assertMediaType(MediaTypes.APPLICATION_JSON) + .assertEntity("[1]"); + + route.run(HttpRequest.GET("/nummer?n=5")) + .assertStatusCode(200) + .assertEntity("[1,2,3,4,5]"); + + route.run( + HttpRequest.GET("/nummer?n=5").addHeader(Accept.create(MediaTypes.TEXT_PLAIN.toRange()))) + .assertStatusCode(406); + } + + @Test + public void testCustomToResponseMarshaller() { + final Marshaller numberAsJsonListMarshaller = + Marshallers.toResponse(MediaTypes.APPLICATION_JSON.toContentType(), new Function() { + @Override + public HttpResponse apply(Integer param) throws Exception { + switch(param) { + case 1: return HttpResponse.create().withEntity(MediaTypes.APPLICATION_JSON.toContentType(), "[1]"); + case 5: return HttpResponse.create().withEntity(MediaTypes.APPLICATION_JSON.toContentType(), "[1,2,3,4,5]"); + default: return HttpResponse.create().withStatus(404); + } + } + }); + + Handler1 nummerHandler = new Handler1() { + @Override + public RouteResult handle(RequestContext ctx, Integer integer) { + return ctx.completeAs(numberAsJsonListMarshaller, integer); + } + }; + + TestRoute route = + testRoute( + get( + path("nummer").route( + handleWith(n, nummerHandler) + ) + ) + ); + + route.run(HttpRequest.GET("/nummer?n=1")) + .assertStatusCode(200) + .assertMediaType(MediaTypes.APPLICATION_JSON) + .assertEntity("[1]"); + + route.run(HttpRequest.GET("/nummer?n=5")) + .assertStatusCode(200) + .assertEntity("[1,2,3,4,5]"); + + route.run(HttpRequest.GET("/nummer?n=6")) + .assertStatusCode(404); + + route.run(HttpRequest.GET("/nummer?n=5").addHeader(Accept.create(MediaTypes.TEXT_PLAIN.toRange()))) + .assertStatusCode(406); + } +} diff --git a/akka-http/src/main/scala/akka/http/impl/server/Util.scala b/akka-http/src/main/scala/akka/http/impl/server/Util.scala index 9c8ab9615f..96c4390756 100644 --- a/akka-http/src/main/scala/akka/http/impl/server/Util.scala +++ b/akka-http/src/main/scala/akka/http/impl/server/Util.scala @@ -6,16 +6,24 @@ package akka.http.impl.server import akka.http.scaladsl.unmarshalling.{ Unmarshaller, FromStringUnmarshaller } import akka.http.scaladsl.util.FastFuture -import akka.japi.function.Function +import akka.japi.function import scala.concurrent.{ Future, ExecutionContext } import scala.util.Try object Util { - def fromStringUnmarshallerFromFunction[T](convert: Function[String, T]): FromStringUnmarshaller[T] = + def fromStringUnmarshallerFromFunction[T](convert: function.Function[String, T]): FromStringUnmarshaller[T] = scalaUnmarshallerFromFunction(convert) - def scalaUnmarshallerFromFunction[T, U](convert: Function[T, U]): Unmarshaller[T, U] = + + def scalaUnmarshallerFromFunction[T, U](convert: function.Function[T, U]): Unmarshaller[T, U] = new Unmarshaller[T, U] { def apply(value: T)(implicit ec: ExecutionContext): Future[U] = FastFuture(Try(convert(value))) } + + implicit class JApiFunctionAndThen[T, U](f1: function.Function[T, U]) { + def andThen[V](f2: U ⇒ V): function.Function[T, V] = + new function.Function[T, V] { + def apply(param: T): V = f2(f1(param)) + } + } } diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/Marshallers.scala b/akka-http/src/main/scala/akka/http/javadsl/server/Marshallers.scala index 11c3930ecd..c9f04f4f43 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/Marshallers.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/Marshallers.scala @@ -4,12 +4,60 @@ package akka.http.javadsl.server -import akka.http.scaladsl.marshalling.ToResponseMarshaller +import akka.http.javadsl.model._ +import akka.http.scaladsl.marshalling.{ ToResponseMarshaller, Marshaller ⇒ ScalaMarshaller } import akka.http.impl.server.MarshallerImpl +import akka.http.scaladsl +import akka.japi.function +import akka.util.ByteString /** * A collection of predefined marshallers. */ object Marshallers { - def STRING: Marshaller[String] = MarshallerImpl(implicit ctx ⇒ implicitly[ToResponseMarshaller[String]]) + /** + * A marshaller that marshals a String to a ``text/plain`` using a charset as negotiated with the + * peer. + */ + def String: Marshaller[String] = MarshallerImpl(implicit ctx ⇒ implicitly[ToResponseMarshaller[String]]) + + import akka.http.impl.util.JavaMapping.Implicits._ + import akka.http.impl.server.Util._ + + /** + * Creates a marshaller by specifying a media type and conversion function from ``T`` to String. + * The charset for encoding the response will be negotiated with the client. + */ + def toEntityString[T](mediaType: MediaType, convert: function.Function[T, String]): Marshaller[T] = + MarshallerImpl(_ ⇒ ScalaMarshaller.stringMarshaller(mediaType.asScala).compose[T](convert(_))) + + /** + * Creates a marshaller from a ContentType and a conversion function from ``T`` to a ``Array[Byte]``. + */ + def toEntityBytes[T](contentType: ContentType, convert: function.Function[T, Array[Byte]]): Marshaller[T] = + toEntity(contentType, convert.andThen(scaladsl.model.HttpEntity(contentType.asScala, _))) + + /** + * Creates a marshaller from a ContentType and a conversion function from ``T`` to a ``ByteString``. + */ + def toEntityByteString[T](contentType: ContentType, convert: function.Function[T, ByteString]): Marshaller[T] = + toEntity(contentType, convert.andThen(scaladsl.model.HttpEntity(contentType.asScala, _))) + + /** + * Creates a marshaller from a ContentType and a conversion function from ``T`` to a ``ResponseEntity``. + */ + def toEntity[T](contentType: ContentType, convert: function.Function[T, ResponseEntity]): Marshaller[T] = + MarshallerImpl { _ ⇒ + ScalaMarshaller.withFixedCharset(contentType.mediaType().asScala, contentType.charset().asScala)(t ⇒ + HttpResponse.create().withStatus(200).withEntity(convert(t)).asScala) + } + + /** + * Creates a marshaller from a ContentType and a conversion function from ``T`` to an ``HttpResponse``. + */ + def toResponse[T](contentType: ContentType, convert: function.Function[T, HttpResponse]): Marshaller[T] = + MarshallerImpl { _ ⇒ + ScalaMarshaller.withFixedCharset(contentType.mediaType().asScala, contentType.charset().asScala)(t ⇒ + convert(t).asScala) + } }