Skip to content

Commit

Permalink
Update documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
wsargent committed Jun 15, 2017
1 parent 76c6bd1 commit 452c608
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 92 deletions.
2 changes: 1 addition & 1 deletion documentation/manual/working/javaGuide/main/index.toc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ xml:Working with XML
upload:Handling file upload
sql:Accessing an SQL database
cache:Using the Cache
ws:Calling WebServices
ws:Calling REST APIs with Play WS
akka:Integrating with Akka
i18n:Internationalization with Messages
dependencyinjection:Dependency Injection
Expand Down
25 changes: 15 additions & 10 deletions documentation/manual/working/javaGuide/main/ws/JavaWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!--- Copyright (C) 2009-2017 Lightbend Inc. <https://www.lightbend.com> -->
# The Play WS API
# Calling REST APIs with Play WS

Sometimes we would like to call other HTTP services from within a Play application. Play supports this via its [WS library](api/java/play/libs/ws/package-summary.html), which provides a way to make asynchronous HTTP calls.

Expand Down Expand Up @@ -32,7 +32,7 @@ Using an HTTP cache means savings on repeated requests to backend REST services,

Now any controller or component that wants to use WS will have to add the following imports and then declare a dependency on the [`WSClient`](api/java/play/libs/ws/WSClient.html) type to use dependency injection:

@[ws-controller](code/javaguide/ws/Application.java)
@[ws-controller](code/javaguide/ws/MyClient.java)

> If you are calling out to an [unreliable network](https://queue.acm.org/detail.cfm?id=2655736) or doing any blocking work, including any kind of DNS work such as calling [`java.util.URL.equals()`](https://docs.oracle.com/javase/8/docs/api/java/net/URL.html#equals-java.lang.Object-), then you should use a custom execution context as described in [[ThreadPools]]. You should size the pool to leave a safety margin large enough to account for futures, and consider using [`play.libs.concurrent.Futures.timeout`](api/java/play/libs/concurrent/Futures.html) and a [Failsafe Circuit Breaker](https://github.com/jhalterman/failsafe#circuit-breakers).
Expand Down Expand Up @@ -78,9 +78,15 @@ For example, if you are sending plain text in a particular format, you may want

@[ws-header-content-type](code/javaguide/ws/JavaWS.java)

### Request with cookie

You can specify cookies for a request.

@[ws-cookie](code/javaguide/ws/JavaWS.java)

### Request with timeout

If you wish to specify a request timeout, you can use `setRequestTimeout` to set a value in milliseconds. A value of `-1` can be used to set an infinite timeout.
If you wish to specify a request timeout, you can use `setRequestTimeout` to set a value in milliseconds. A value of `Duration.ofMillis(Long.MAX_VALUE)` can be used to set an infinite timeout.

@[ws-timeout](code/javaguide/ws/JavaWS.java)

Expand All @@ -92,21 +98,21 @@ To post url-form-encoded data you can set the proper header and formatted data.

### Submitting JSON data

The easiest way to post JSON data is to use the [[JSON library|JavaJsonActions]].
The easiest way to post JSON data is to use the [[JSON library|JavaJsonActions]] with the WSBodyWritables `body(JsonNode)` method.

@[json-imports](code/javaguide/ws/JavaWS.java)

@[ws-post-json](code/javaguide/ws/JavaWS.java)

### Submitting multipart/form data

The easiest way to post multipart/form data is to use a `Source<Http.MultipartFormData.Part<Source<ByteString>, ?>, ?>`
The easiest way to post multipart/form data is to use a `Source<Http.MultipartFormData.Part<Source<ByteString>, ?>, ?>` using `multipartBody()`

@[multipart-imports](code/javaguide/ws/JavaWS.java)

@[ws-post-multipart](code/javaguide/ws/JavaWS.java)

To Upload a File you need to pass a `Http.MultipartFormData.FilePart<Source<ByteString>, ?>` to the `Source`:
To upload a File you need to pass a `Http.MultipartFormData.FilePart<Source<ByteString>, ?>` to the `Source`:

@[ws-post-multipart2](code/javaguide/ws/JavaWS.java)

Expand All @@ -132,22 +138,21 @@ Working with the [`WSResponse`](api/java/play/libs/ws/WSResponse.html) is done b

### Processing a response as JSON

You can process the response as a `JsonNode` by calling `response.asJson()`.
You can process the response as a `JsonNode` by calling `response.getBody(json())`, with the `json()` method provided by `WSBodyReadables`.

@[ws-response-json](code/javaguide/ws/JavaWS.java)


### Processing a response as XML

Similarly, you can process the response as XML by calling `response.asXml()`.
Similarly, you can process the response as XML by calling `response.getBody(xml())` with the `xml()` method provided by `WSBodyReadables`.

@[ws-response-xml](code/javaguide/ws/JavaWS.java)

### Processing large responses

Calling `get()`, `post()` or `execute()` will cause the body of the response to be loaded into memory before the response is made available. When you are downloading a large, multi-gigabyte file, this may result in unwelcomed garbage collection or even out of memory errors.

`WS` lets you consume the response's body incrementally by using an Akka Streams `Sink`. The `stream()` method on `WSRequest` returns a `CompletionStage<StreamedResponse>`. A `StreamedResponse` is a simple container holding together the response's headers and body.
You can consume the response's body incrementally by using an [Akka Streams](http://doc.akka.io/docs/akka/current/scala/stream/stream-flows-and-basics.html) `Sink`. The `stream()` method on `WSRequest` returns a `CompletionStage<WSResponse>`, where the `WSResponse` contains a `bodyAsSource` method that provides a source of `ByteStream`.

Any controller or component that wants to leverage the WS streaming functionality will have to add the following imports and dependencies:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import play.libs.concurrent.Futures;
import play.libs.ws.*;

import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.concurrent.*;
// #ws-imports
Expand Down Expand Up @@ -48,7 +50,7 @@
public class JavaWS {
private static final String feedUrl = "http://localhost:3333/feed";

public static class Controller0 extends MockJavaAction {
public static class Controller0 extends MockJavaAction implements WSBodyReadables, WSBodyWritables {

private final WSClient ws;
private final Materializer materializer;
Expand All @@ -66,9 +68,9 @@ public void requestExamples() {
// #ws-holder

// #ws-complex-holder
WSRequest complexRequest = request.setHeader("headerKey", "headerValue")
.setRequestTimeout(1000)
.setQueryParameter("paramKey", "paramValue");
WSRequest complexRequest = request.addHeader("headerKey", "headerValue")
.setRequestTimeout(Duration.of(1000, ChronoUnit.MILLIS))
.addQueryParameter("paramKey", "paramValue");
// #ws-complex-holder

// #ws-get
Expand All @@ -85,47 +87,51 @@ public void requestExamples() {
// #ws-follow-redirects

// #ws-query-parameter
ws.url(url).setQueryParameter("paramKey", "paramValue");
ws.url(url).addQueryParameter("paramKey", "paramValue");
// #ws-query-parameter

// #ws-header
ws.url(url).setHeader("headerKey", "headerValue").get();
ws.url(url).addHeader("headerKey", "headerValue").get();
// #ws-header

// #ws-cookie
ws.url(url).addCookies(new WSCookieBuilder().setName("headerKey").setValue("headerValue").build()).get();
// #ws-cookie

String jsonString = "{\"key1\":\"value1\"}";
// #ws-header-content-type
ws.url(url).setHeader("Content-Type", "application/json").post(jsonString);
ws.url(url).addHeader("Content-Type", "application/json").post(body(jsonString));
// OR
ws.url(url).setContentType("application/json").post(jsonString);
ws.url(url).setContentType("application/json").post(body(jsonString));
// #ws-header-content-type

// #ws-timeout
ws.url(url).setRequestTimeout(1000).get();
ws.url(url).setRequestTimeout(Duration.of(1000, ChronoUnit.MILLIS)).get();
// #ws-timeout

// #ws-post-form-data
ws.url(url).setContentType("application/x-www-form-urlencoded")
.post("key1=value1&key2=value2");
.post(body("key1=value1&key2=value2"));
// #ws-post-form-data

// #ws-post-json
JsonNode json = Json.newObject()
.put("key1", "value1")
.put("key2", "value2");

ws.url(url).post(json);
ws.url(url).post(body(json));
// #ws-post-json

// #ws-post-multipart
ws.url(url).post(Source.single(new DataPart("hello", "world")));
ws.url(url).post(multipartBody(Source.single(new DataPart("hello", "world"))));
// #ws-post-multipart

// #ws-post-multipart2
Source<ByteString, ?> file = FileIO.fromFile(new File("hello.txt"));
FilePart<Source<ByteString, ?>> fp = new FilePart<>("hello", "hello.txt", "text/plain", file);
DataPart dp = new DataPart("key", "value");

ws.url(url).post(Source.from(Arrays.asList(fp, dp)));
ws.url(url).post(multipartBody(Source.from(Arrays.asList(fp, dp))));
// #ws-post-multipart2

String value = IntStream.range(0,100).boxed().
Expand All @@ -134,7 +140,7 @@ public void requestExamples() {
Stream<ByteString> largeSource = IntStream.range(0,10).boxed().map(i -> seedValue);
Source<ByteString, ?> largeImage = Source.from(largeSource.collect(Collectors.toList()));
// #ws-stream-request
CompletionStage<? extends WSResponse> wsResponse = ws.url(url).setBody(largeImage).execute("PUT");
CompletionStage<WSResponse> wsResponse = ws.url(url).setBody(body(largeImage)).execute("PUT");
// #ws-stream-request
}

Expand All @@ -143,25 +149,27 @@ public void responseExamples() {
String url = "http://example.com";

// #ws-response-json
// implements WSBodyReadables or use WSBodyReadables.instance.json()
CompletionStage<JsonNode> jsonPromise = ws.url(url).get()
.thenApply(WSResponse::asJson);
.thenApply(r -> r.getBody(json()));
// #ws-response-json

// #ws-response-xml
// implements WSBodyReadables or use WSBodyReadables.instance.xml()
CompletionStage<Document> documentPromise = ws.url(url).get()
.thenApply(WSResponse::asXml);
.thenApply(r -> r.getBody(xml()));
// #ws-response-xml
}

public void streamSimpleRequest() {
String url = "http://example.com";
// #stream-count-bytes
// Make the request
CompletionStage<? extends StreamedResponse> futureResponse =
CompletionStage<WSResponse> futureResponse =
ws.url(url).setMethod("GET").stream();

CompletionStage<Long> bytesReturned = futureResponse.thenCompose(res -> {
Source<ByteString, ?> responseBody = res.getBody();
Source<ByteString, ?> responseBody = res.getBodyAsSource();

// Count the number of bytes returned
Sink<ByteString, CompletionStage<Long>> bytesSum =
Expand All @@ -179,11 +187,11 @@ public void streamFile() throws IOException, FileNotFoundException, InterruptedE
OutputStream outputStream = java.nio.file.Files.newOutputStream(file.toPath());

// Make the request
CompletionStage<? extends StreamedResponse> futureResponse =
CompletionStage<WSResponse> futureResponse =
ws.url(url).setMethod("GET").stream();

CompletionStage<File> downloadedFile = futureResponse.thenCompose(res -> {
Source<ByteString, ?> responseBody = res.getBody();
Source<ByteString, ?> responseBody = res.getBodyAsSource();

// The sink that writes to the output stream
Sink<ByteString, CompletionStage<akka.Done>> outputWriter =
Expand All @@ -208,21 +216,20 @@ public void streamResponse() {
String url = "http://example.com";
//#stream-to-result
// Make the request
CompletionStage<? extends StreamedResponse> futureResponse = ws.url(url).setMethod("GET").stream();
CompletionStage<WSResponse> futureResponse = ws.url(url).setMethod("GET").stream();

CompletionStage<Result> result = futureResponse.thenApply(response -> {
WSResponseHeaders responseHeaders = response.getHeaders();
Source<ByteString, ?> body = response.getBody();
Source<ByteString, ?> body = response.getBodyAsSource();
// Check that the response was successful
if (responseHeaders.getStatus() == 200) {
if (response.getStatus() == 200) {
// Get the content type
String contentType =
Optional.ofNullable(responseHeaders.getHeaders().get("Content-Type"))
Optional.ofNullable(response.getHeaders().get("Content-Type"))
.map(contentTypes -> contentTypes.get(0))
.orElse("application/octet-stream");

// If there's a content length, send that, otherwise return the body chunked
Optional<String> contentLength = Optional.ofNullable(responseHeaders.getHeaders()
Optional<String> contentLength = Optional.ofNullable(response.getHeaders()
.get("Content-Length"))
.map(contentLengths -> contentLengths.get(0));
if (contentLength.isPresent()) {
Expand All @@ -244,21 +251,21 @@ public void streamResponse() {
public void streamPut() {
String url = "http://example.com";
//#stream-put
CompletionStage<? extends StreamedResponse> futureResponse =
ws.url(url).setMethod("PUT").setBody("some body").stream();
CompletionStage<WSResponse> futureResponse =
ws.url(url).setMethod("PUT").setBody(body("some body")).stream();
//#stream-put
}

public void patternExamples() {
String urlOne = "http://localhost:3333/one";
// #ws-composition
final CompletionStage<? extends WSResponse> responseThreePromise = ws.url(urlOne).get()
final CompletionStage<WSResponse> responseThreePromise = ws.url(urlOne).get()
.thenCompose(responseOne -> ws.url(responseOne.getBody()).get())
.thenCompose(responseTwo -> ws.url(responseTwo.getBody()).get());
// #ws-composition

// #ws-recover
CompletionStage<? extends WSResponse> responsePromise = ws.url("http://example.com").get();
CompletionStage<WSResponse> responsePromise = ws.url("http://example.com").get();
responsePromise.handle((result, error) -> {
if (error != null) {
return ws.url("http://backup.example.com").get();
Expand Down Expand Up @@ -316,13 +323,13 @@ public Controller1(JavaHandlerComponents javaHandlerComponents, WSClient client)
// #ws-action
public CompletionStage<Result> index() {
return ws.url(feedUrl).get().thenApply(response ->
ok("Feed title: " + response.asJson().findPath("title").asText())
ok("Feed title: " + response.getBody(WSBodyReadables.instance.json()).findPath("title").asText())
);
}
// #ws-action
}

public static class Controller2 extends MockJavaAction {
public static class Controller2 extends MockJavaAction implements WSBodyWritables, WSBodyReadables {

private final WSClient ws;

Expand All @@ -335,13 +342,13 @@ public Controller2(JavaHandlerComponents javaHandlerComponents, WSClient ws) {
// #composed-call
public CompletionStage<Result> index() {
return ws.url(feedUrl).get()
.thenCompose(response -> ws.url(response.asJson().findPath("commentsUrl").asText()).get())
.thenApply(response -> ok("Number of comments: " + response.asJson().findPath("count").asInt()));
.thenCompose(response -> ws.url(response.getBody(json()).findPath("commentsUrl").asText()).get())
.thenApply(response -> ok("Number of comments: " + response.getBody(json()).findPath("count").asInt()));
}
// #composed-call
}

public static class Controller3 extends MockJavaAction {
public static class Controller3 extends MockJavaAction implements WSBodyWritables, WSBodyReadables {

private final WSClient ws;
private Logger logger;
Expand All @@ -364,9 +371,13 @@ public CompletionStage<Result> index() {
return executor.apply(request);
};

return ws.url(feedUrl).setRequestFilter(filter).get().thenApply(response ->
ok("Feed title: " + response.asJson().findPath("title").asText())
);
return ws.url(feedUrl)
.setRequestFilter(filter)
.get()
.thenApply((WSResponse r) -> {
String title = r.getBody(json()).findPath("title").asText();
return ok("Feed title: " + title);
});
}
// #ws-request-filter
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
import play.libs.ws.*;
import java.util.concurrent.CompletionStage;

public class Application extends Controller {

@Inject WSClient ws;
public class MyClient implements WSBodyReadables, WSBodyWritables {
private final WSClient ws;

@Inject
public MyClient(WSClient ws) {
this.ws = ws;
}
// ...
}
// #ws-controller

0 comments on commit 452c608

Please sign in to comment.