1 /*
2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 
24 /*
25  * @test
26  * @library /lib/testlibrary server
27  * @build jdk.testlibrary.SimpleSSLContext
28  * @modules java.base/sun.net.www.http
29  *          java.net.http/jdk.internal.net.http.common
30  *          java.net.http/jdk.internal.net.http.frame
31  *          java.net.http/jdk.internal.net.http.hpack
32  * @run testng/othervm
33  *       -Djdk.internal.httpclient.debug=true
34  *       -Djdk.httpclient.HttpClient.log=errors,requests,responses
35  *       ServerPushWithDiffTypes
36  */
37 
38 import java.io.*;
39 import java.net.*;
40 import java.nio.ByteBuffer;
41 import java.nio.file.*;
42 import java.net.http.*;
43 import java.net.http.HttpResponse.BodyHandler;
44 import java.net.http.HttpResponse.PushPromiseHandler;
45 import java.net.http.HttpResponse.BodySubscriber;
46 import java.net.http.HttpResponse.BodySubscribers;
47 import java.util.*;
48 import java.util.concurrent.*;
49 import java.util.function.BiPredicate;
50 
51 import org.testng.annotations.Test;
52 import static java.nio.charset.StandardCharsets.UTF_8;
53 import static org.testng.Assert.assertEquals;
54 
55 public class ServerPushWithDiffTypes {
56 
57     static Map<String,String> PUSH_PROMISES = Map.of(
58             "/x/y/z/1", "the first push promise body",
59             "/x/y/z/2", "the second push promise body",
60             "/x/y/z/3", "the third push promise body",
61             "/x/y/z/4", "the fourth push promise body",
62             "/x/y/z/5", "the fifth push promise body",
63             "/x/y/z/6", "the sixth push promise body",
64             "/x/y/z/7", "the seventh push promise body",
65             "/x/y/z/8", "the eighth push promise body",
66             "/x/y/z/9", "the ninth push promise body"
67     );
68 
69     @Test
test()70     public static void test() throws Exception {
71         Http2TestServer server = null;
72         try {
73             server = new Http2TestServer(false, 0);
74             Http2Handler handler =
75                     new ServerPushHandler("the main response body",
76                                           PUSH_PROMISES);
77             server.addHandler(handler, "/");
78             server.start();
79             int port = server.getAddress().getPort();
80             System.err.println("Server listening on port " + port);
81 
82             HttpClient client = HttpClient.newHttpClient();
83             // use multi-level path
84             URI uri = new URI("http://localhost:" + port + "/foo/a/b/c");
85             HttpRequest request = HttpRequest.newBuilder(uri).GET().build();
86 
87             ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<BodyAndType<?>>>>
88                     results = new ConcurrentHashMap<>();
89             PushPromiseHandler<BodyAndType<?>> bh = PushPromiseHandler.of(
90                     (pushRequest) -> new BodyAndTypeHandler(pushRequest), results);
91 
92             CompletableFuture<HttpResponse<BodyAndType<?>>> cf =
93                     client.sendAsync(request, new BodyAndTypeHandler(request), bh);
94             results.put(request, cf);
95             cf.join();
96 
97             assertEquals(results.size(), PUSH_PROMISES.size() + 1);
98 
99             for (HttpRequest r : results.keySet()) {
100                 URI u = r.uri();
101                 BodyAndType<?> body = results.get(r).get().body();
102                 String result;
103                 // convert all body types to String for easier comparison
104                 if (body.type() == String.class) {
105                     result = (String)body.getBody();
106                 } else if (body.type() == byte[].class) {
107                     byte[] bytes = (byte[])body.getBody();
108                     result = new String(bytes, UTF_8);
109                 } else if (Path.class.isAssignableFrom(body.type())) {
110                     Path path = (Path)body.getBody();
111                     result = new String(Files.readAllBytes(path), UTF_8);
112                 } else {
113                     throw new AssertionError("Unknown:" + body.type());
114                 }
115 
116                 System.err.printf("%s -> %s\n", u.toString(), result.toString());
117                 String expected = PUSH_PROMISES.get(r.uri().getPath());
118                 if (expected == null)
119                     expected = "the main response body";
120                 assertEquals(result, expected);
121             }
122         } finally {
123             server.stop();
124         }
125     }
126 
127     interface BodyAndType<T> {
type()128         Class<T> type();
getBody()129         T getBody();
130     }
131 
132     static final Path WORK_DIR = Paths.get(".");
133 
134     static class BodyAndTypeHandler implements BodyHandler<BodyAndType<?>> {
135         int count;
136         final HttpRequest request;
137 
BodyAndTypeHandler(HttpRequest request)138         BodyAndTypeHandler(HttpRequest request) {
139             this.request = request;
140         }
141 
142         @Override
apply(HttpResponse.ResponseInfo info)143         public HttpResponse.BodySubscriber<BodyAndType<?>> apply(HttpResponse.ResponseInfo info) {
144             int whichType = count++ % 3;  // real world may base this on the request metadata
145             switch (whichType) {
146                 case 0: // String
147                     return new BodyAndTypeSubscriber(BodySubscribers.ofString(UTF_8));
148                 case 1: // byte[]
149                     return new BodyAndTypeSubscriber(BodySubscribers.ofByteArray());
150                 case 2: // Path
151                     URI u = request.uri();
152                     Path path = Paths.get(WORK_DIR.toString(), u.getPath());
153                     try {
154                         Files.createDirectories(path.getParent());
155                     } catch (IOException ee) {
156                         throw new UncheckedIOException(ee);
157                     }
158                     return new BodyAndTypeSubscriber(BodySubscribers.ofFile(path));
159                 default:
160                     throw new AssertionError("Unexpected " + whichType);
161             }
162         }
163     }
164 
165     static class BodyAndTypeSubscriber<T>
166         implements HttpResponse.BodySubscriber<BodyAndType<T>>
167     {
168         private static class BodyAndTypeImpl<T> implements BodyAndType<T> {
169             private final Class<T> type;
170             private final T body;
BodyAndTypeImpl(Class<T> type, T body)171             public BodyAndTypeImpl(Class<T> type, T body) { this.type = type; this.body = body; }
type()172             @Override public Class<T> type() { return type; }
getBody()173             @Override public T getBody() { return body; }
174         }
175 
176         private final BodySubscriber<?> bodySubscriber;
177         private final CompletableFuture<BodyAndType<T>> cf;
178 
BodyAndTypeSubscriber(BodySubscriber bodySubscriber)179         BodyAndTypeSubscriber(BodySubscriber bodySubscriber) {
180             this.bodySubscriber = bodySubscriber;
181             cf = new CompletableFuture<>();
182             bodySubscriber.getBody().whenComplete(
183                     (r,t) -> cf.complete(new BodyAndTypeImpl(r.getClass(), r)));
184         }
185 
186         @Override
onSubscribe(Flow.Subscription subscription)187         public void onSubscribe(Flow.Subscription subscription) {
188             bodySubscriber.onSubscribe(subscription);
189         }
190 
191         @Override
onNext(List<ByteBuffer> item)192         public void onNext(List<ByteBuffer> item) {
193             bodySubscriber.onNext(item);
194         }
195 
196         @Override
onError(Throwable throwable)197         public void onError(Throwable throwable) {
198             bodySubscriber.onError(throwable);
199             cf.completeExceptionally(throwable);
200         }
201 
202         @Override
onComplete()203         public void onComplete() {
204             bodySubscriber.onComplete();
205         }
206 
207         @Override
getBody()208         public CompletionStage<BodyAndType<T>> getBody() {
209             return cf;
210         }
211     }
212 
213     // --- server push handler ---
214     static class ServerPushHandler implements Http2Handler {
215 
216         private final String mainResponseBody;
217         private final Map<String,String> promises;
218 
ServerPushHandler(String mainResponseBody, Map<String,String> promises)219         public ServerPushHandler(String mainResponseBody,
220                                  Map<String,String> promises)
221             throws Exception
222         {
223             Objects.requireNonNull(promises);
224             this.mainResponseBody = mainResponseBody;
225             this.promises = promises;
226         }
227 
handle(Http2TestExchange exchange)228         public void handle(Http2TestExchange exchange) throws IOException {
229             System.err.println("Server: handle " + exchange);
230             try (InputStream is = exchange.getRequestBody()) {
231                 is.readAllBytes();
232             }
233 
234             if (exchange.serverPushAllowed()) {
235                 pushPromises(exchange);
236             }
237 
238             // response data for the main response
239             try (OutputStream os = exchange.getResponseBody()) {
240                 byte[] bytes = mainResponseBody.getBytes(UTF_8);
241                 exchange.sendResponseHeaders(200, bytes.length);
242                 os.write(bytes);
243             }
244         }
245 
246         static final BiPredicate<String,String> ACCEPT_ALL = (x, y) -> true;
247 
pushPromises(Http2TestExchange exchange)248         private void pushPromises(Http2TestExchange exchange) throws IOException {
249             URI requestURI = exchange.getRequestURI();
250             for (Map.Entry<String,String> promise : promises.entrySet()) {
251                 URI uri = requestURI.resolve(promise.getKey());
252                 InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8));
253                 Map<String,List<String>> map = Map.of("X-Promise", List.of(promise.getKey()));
254                 HttpHeaders headers = HttpHeaders.of(map, ACCEPT_ALL);
255                 // TODO: add some check on headers, maybe
256                 exchange.serverPush(uri, headers, is);
257             }
258             System.err.println("Server: All pushes sent");
259         }
260     }
261 }
262