diff --git a/build.sbt b/build.sbt index 8d6967221b4..0dbcc575944 100644 --- a/build.sbt +++ b/build.sbt @@ -167,6 +167,7 @@ lazy val docs = project("docs") case akka.Doc.BinVer(_) => "" case _ => "cross CrossVersion.full" }), + "jackson.version" -> Dependencies.jacksonVersion, "extref.akka-docs.base_url" -> s"http://doc.akka.io/docs/akka/${Dependencies.akkaVersion.value}/%s", "javadoc.akka.http.base_url" -> { val v = if (isSnapshot.value) "current" else version.value diff --git a/docs/src/main/paradox/java/http/common/index.md b/docs/src/main/paradox/java/http/common/index.md deleted file mode 100644 index fc582418710..00000000000 --- a/docs/src/main/paradox/java/http/common/index.md +++ /dev/null @@ -1,21 +0,0 @@ -# Common Abstractions (Client- and Server-Side) - -HTTP and related specifications define a great number of concepts and functionality that is not specific to either -HTTP's client- or server-side since they are meaningful on both end of an HTTP connection. -The documentation for their counterparts in Akka HTTP lives in this section rather than in the ones for the -@ref[Client-Side API](../client-side/index.md), @ref[Low-Level Server-Side API](../server-side/low-level-api.md) or @ref[High-level Server-Side API](../routing-dsl/index.md), -which are specific to one side only. - -@@toc { depth=3 } - -@@@ index - -* [http-model](http-model.md) -* [uri-model](uri-model.md) -* [marshalling](marshalling.md) -* [unmarshalling](unmarshalling.md) -* [encoding](encoding.md) -* [json-support](json-support.md) -* [timeouts](timeouts.md) - -@@@ diff --git a/docs/src/main/paradox/java/http/common/index.md b/docs/src/main/paradox/java/http/common/index.md new file mode 120000 index 00000000000..b9bdb5092a3 --- /dev/null +++ b/docs/src/main/paradox/java/http/common/index.md @@ -0,0 +1 @@ +../../../scala/http/common/index.md \ No newline at end of file diff --git a/docs/src/main/paradox/java/http/common/marshalling.md b/docs/src/main/paradox/java/http/common/marshalling.md index 85003f7f15c..51c40c250ee 100644 --- a/docs/src/main/paradox/java/http/common/marshalling.md +++ b/docs/src/main/paradox/java/http/common/marshalling.md @@ -82,6 +82,7 @@ The implicits for most of the predefined marshallers in Akka HTTP are provided t `Marshaller` trait. This means that they are always available and never need to be explicitly imported. Additionally, you can simply "override" them by bringing your own custom version into local scope. + ## Custom Marshallers Akka HTTP gives you a few convenience tools for constructing marshallers for your own types. diff --git a/docs/src/main/paradox/java/http/common/xml-support.md b/docs/src/main/paradox/java/http/common/xml-support.md deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/docs/src/main/paradox/java/http/common/xml-support.md b/docs/src/main/paradox/java/http/common/xml-support.md new file mode 120000 index 00000000000..fb7d656110d --- /dev/null +++ b/docs/src/main/paradox/java/http/common/xml-support.md @@ -0,0 +1 @@ +../../../scala/http/common/xml-support.md \ No newline at end of file diff --git a/docs/src/main/paradox/scala/http/common/xml-support.md b/docs/src/main/paradox/scala/http/common/xml-support.md index 7bd1d52606d..cb72671643a 100644 --- a/docs/src/main/paradox/scala/http/common/xml-support.md +++ b/docs/src/main/paradox/scala/http/common/xml-support.md @@ -4,7 +4,46 @@ Akka HTTP's @ref[marshalling](marshalling.md) and @ref[unmarshalling](unmarshall infrastructure makes it rather easy to seamlessly support specific wire representations of your data objects, like JSON, XML or even binary encodings. -For XML Akka HTTP currently provides support for [Scala XML](https://github.com/scala/scala-xml) right out of the box through it's +@@@ div { .group-java } + +Akka HTTP does not currently provide a Java API for XML support. If you need to +produce and consume XML, you can write a @ref[custom marshaller](marshalling.md#custom-marshallers) +using [Jackson], which is also the library used for providing @ref[JSON support](json-support.md#json-jackson-support-java). + +@@ snip [#jackson-xml-support] (../../../../../test/java/docs/http/javadsl/JacksonXmlSupport.java) { #jackson-xml-support } + +The custom XML (un)marshalling code shown above requires that you depend on the `jackson-dataformat-xml` library. + +sbt +: @@@vars + ``` + "com.fasterxml.jackson.dataformat" % "jackson-dataformat-xml" % "$jackson.version$" + ``` + @@@ + +Gradle +: @@@vars + ``` + compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '$jackson.version$' + ``` + @@@ + +Maven +: @@@vars + ```xml + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + $jackson.version$ + + ``` + @@@ + +@@@ + +@@@ div { .group-scala } + +For XML Akka HTTP currently provides support for [Scala XML][scala-xml] right out of the box through it's `akka-http-xml` module. ## Scala XML Support @@ -12,15 +51,39 @@ For XML Akka HTTP currently provides support for [Scala XML](https://github.com/ The @scaladoc[ScalaXmlSupport](akka.http.scaladsl.marshallers.xml.ScalaXmlSupport) trait provides a `FromEntityUnmarshaller[NodeSeq]` and `ToEntityMarshaller[NodeSeq]` that you can use directly or build upon. -In order to enable support for (un)marshalling from and to XML with [Scala XML](https://github.com/scala/scala-xml) `NodeSeq` you must add +In order to enable support for (un)marshalling from and to XML with [Scala XML][scala-xml] `NodeSeq` you must add the following dependency: -@@@vars -```sbt -"com.typesafe.akka" %% "akka-http-xml" % "$project.version$" -``` -@@@ +sbt +: @@@vars + ``` + "com.typesafe.akka" %% "akka-http-xml" % "$project.version$" $crossString$ + ``` + @@@ + +Gradle +: @@@vars + ``` + compile group: 'com.typesafe.akka', name: 'akka-http-xml_$scala.binary_version$', version: '$project.version$' + ``` + @@@ + +Maven +: @@@vars + ``` + + com.typesafe.akka + akka-http-xml_$scala.binary_version$ + $project.version$ + + ``` + @@@ Once you have done this (un)marshalling between XML and `NodeSeq` instances should work nicely and transparently, by either using `import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._` or mixing in the `akka.http.scaladsl.marshallers.xml.ScalaXmlSupport` trait. + +@@@ + + [scala-xml]: https://github.com/scala/scala-xml + [jackson]: https://github.com/FasterXML/jackson diff --git a/docs/src/test/java/docs/http/javadsl/JacksonXmlExampleTest.java b/docs/src/test/java/docs/http/javadsl/JacksonXmlExampleTest.java new file mode 100644 index 00000000000..5f7fd93e17d --- /dev/null +++ b/docs/src/test/java/docs/http/javadsl/JacksonXmlExampleTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ + +package docs.http.javadsl; + +import org.junit.Test; +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import com.fasterxml.jackson.annotation.JsonRootName; + +import akka.http.javadsl.testkit.JUnitRouteTest; +import akka.http.javadsl.model.*; +import akka.http.javadsl.server.Route; +import akka.http.javadsl.unmarshalling.Unmarshaller; + +public class JacksonXmlExampleTest extends JUnitRouteTest { + + final String xml = "34"; + final Point point = new Point() { + { + setX(3); + setY(4); + } + }; + + @Test + public void marshalXml() throws Exception { + final Route route = route( + completeOK(point, JacksonXmlSupport.marshaller()) + ); + + testRoute(route) + .run(HttpRequest.GET("/")) + .assertStatusCode(StatusCodes.OK) + .assertEntity(xml); + } + + @Test + public void unmarshalXml() throws Exception { + final Unmarshaller unmarshaller = JacksonXmlSupport.unmarshaller(Point.class); + + final Route route = route( + entity(unmarshaller, p -> { + assertEquals(p, point); + return complete(p.toString()); + }) + ); + + final RequestEntity entity = HttpEntities.create(ContentTypes.TEXT_XML_UTF8, xml); + + testRoute(route) + .run(HttpRequest.POST("/").withEntity(entity)) + .assertStatusCode(StatusCodes.OK) + .assertEntity("Point(x=3, y=4)"); + } + + @Test + public void unmarshalXmlDirect() throws Exception { + { + CompletionStage resultStage = + JacksonXmlSupport.unmarshaller(Point.class).unmarshal( + HttpEntities.create(ContentTypes.TEXT_XML_UTF8, xml), + system().dispatcher(), + materializer()); + + assertEquals(point, resultStage.toCompletableFuture().get(3, TimeUnit.SECONDS)); + } + + { + CompletionStage resultStage = + JacksonXmlSupport.unmarshaller(Point.class).unmarshal( + HttpEntities.create(ContentTypes.create(MediaTypes.APPLICATION_XML, HttpCharsets.UTF_8), xml), + system().dispatcher(), + materializer()); + + assertEquals(point, resultStage.toCompletableFuture().get(3, TimeUnit.SECONDS)); + } + } + + @JsonRootName("point") + public static class Point { + private int x, y; + public void setX(int x) { this.x = x; } + public int getX() { return this.x; } + public void setY(int y) { this.y = y; } + public int getY() { return this.y; } + + public String toString() { + return "Point(x=" + x + ", y=" + y + ")"; + } + + public boolean equals(Object other) { + Point that = other instanceof Point ? (Point) other : null; + return that != null + && that.x == this.x + && that.y == this.y; + } + } +} diff --git a/docs/src/test/java/docs/http/javadsl/JacksonXmlSupport.java b/docs/src/test/java/docs/http/javadsl/JacksonXmlSupport.java new file mode 100644 index 00000000000..91ea9a97f60 --- /dev/null +++ b/docs/src/test/java/docs/http/javadsl/JacksonXmlSupport.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2009-2017 Lightbend Inc. + */ + +package docs.http.javadsl; + +//#jackson-xml-support +import java.io.IOException; +import java.util.List; +import java.util.Arrays; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import akka.http.javadsl.model.*; +import akka.http.javadsl.marshalling.Marshaller; +import akka.http.javadsl.unmarshalling.Unmarshaller; + +public class JacksonXmlSupport { + private static final ObjectMapper DEFAULT_XML_MAPPER = + new XmlMapper().enable(SerializationFeature.WRAP_ROOT_VALUE); + private static final List XML_MEDIA_TYPES = Arrays.asList(MediaTypes.APPLICATION_XML, MediaTypes.TEXT_XML); + + public static Marshaller marshaller() { + return Marshaller.wrapEntity( + u -> toXML(DEFAULT_XML_MAPPER, u), + Marshaller.stringToEntity(), + MediaTypes.APPLICATION_XML + ); + } + + public static Unmarshaller unmarshaller(Class expectedType) { + return Unmarshaller.forMediaTypes(XML_MEDIA_TYPES, Unmarshaller.entityToString()) + .thenApply(xml -> fromXML(DEFAULT_XML_MAPPER, xml, expectedType)); + } + + private static String toXML(ObjectMapper mapper, T object) { + try { + return mapper.writeValueAsString(object); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot marshal to XML: " + object, e); + } + } + + private static T fromXML(ObjectMapper mapper, String xml, Class expectedType) { + try { + return mapper.readerFor(expectedType).readValue(xml); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot unmarshal XML as " + expectedType.getSimpleName(), e); + } + } +} +//#jackson-xml-support diff --git a/docs/src/test/java/docs/http/javadsl/server/directives/MarshallingDirectivesExamplesTest.java b/docs/src/test/java/docs/http/javadsl/server/directives/MarshallingDirectivesExamplesTest.java index bfe84a23b9d..64b0999db57 100644 --- a/docs/src/test/java/docs/http/javadsl/server/directives/MarshallingDirectivesExamplesTest.java +++ b/docs/src/test/java/docs/http/javadsl/server/directives/MarshallingDirectivesExamplesTest.java @@ -47,9 +47,9 @@ public int getFavoriteNumber() { @Test public void testEntity() { //#example-entity-with-json - final Unmarshaller unmarshallar = Jackson.unmarshaller(Person.class); + final Unmarshaller unmarshaller = Jackson.unmarshaller(Person.class); - final Route route = entity(unmarshallar, person -> + final Route route = entity(unmarshaller, person -> complete( "Person:" + person.getName() + " - favoriteNumber:" + person.getFavoriteNumber() ) ); diff --git a/project/Dependencies.scala b/project/Dependencies.scala index ac372d2177c..4b54b4b72bc 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -7,6 +7,7 @@ import scala.language.implicitConversions object Dependencies { import DependencyHelpers._ + val jacksonVersion = "2.8.8" val junitVersion = "4.12" val h2specVersion = "1.5.0" val h2specName = s"h2spec_${DependencyHelpers.osName}_amd64" @@ -42,7 +43,7 @@ object Dependencies { val sprayJson = "io.spray" %% "spray-json" % "1.3.3" // ApacheV2 // For akka-http-jackson support - val jackson = "com.fasterxml.jackson.core" % "jackson-databind" % "2.8.8" // ApacheV2 + val jackson = "com.fasterxml.jackson.core" % "jackson-databind" % jacksonVersion // ApacheV2 // For akka-http-testkit-java val junit = "junit" % "junit" % junitVersion // Common Public License 1.0 @@ -53,7 +54,8 @@ object Dependencies { object Docs { val sprayJson = Compile.sprayJson % "test" - val gson = "com.google.code.gson" % "gson" % "2.3.1" % "test" + val gson = "com.google.code.gson" % "gson" % "2.3.1" % "test" + val jacksonXml = "com.fasterxml.jackson.dataformat" % "jackson-dataformat-xml" % jacksonVersion % "test" // ApacheV2 } object Test { @@ -108,7 +110,7 @@ object Dependencies { lazy val httpJackson = l ++= Seq(jackson) - lazy val docs = l ++= Seq(Docs.sprayJson, Docs.gson) + lazy val docs = l ++= Seq(Docs.sprayJson, Docs.gson, Docs.jacksonXml) }