|
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 * jdk.incubator.httpclient/jdk.incubator.http.internal.common |
|
30 * jdk.incubator.httpclient/jdk.incubator.http.internal.frame |
|
31 * jdk.incubator.httpclient/jdk.incubator.http.internal.hpack |
|
32 * @run testng/othervm -Djdk.internal.httpclient.debug=true -Djdk.httpclient.HttpClient.log=errors,requests,responses ServerPushWithDiffTypes |
|
33 */ |
|
34 |
|
35 import java.io.*; |
|
36 import java.net.*; |
|
37 import java.nio.ByteBuffer; |
|
38 import java.nio.charset.StandardCharsets; |
|
39 import java.nio.file.*; |
|
40 import jdk.incubator.http.*; |
|
41 import jdk.incubator.http.HttpResponse.BodyHandler; |
|
42 import jdk.incubator.http.HttpResponse.PushPromiseHandler; |
|
43 import jdk.incubator.http.HttpResponse.BodySubscriber; |
|
44 import java.util.*; |
|
45 import java.util.concurrent.*; |
|
46 import jdk.incubator.http.internal.common.HttpHeadersImpl; |
|
47 import org.testng.annotations.Test; |
|
48 import static java.nio.charset.StandardCharsets.UTF_8; |
|
49 |
|
50 public class ServerPushWithDiffTypes { |
|
51 |
|
52 static Map<String,String> PUSH_PROMISES = Map.of( |
|
53 "/x/y/z/1", "the first push promise body", |
|
54 "/x/y/z/2", "the second push promise body", |
|
55 "/x/y/z/3", "the third push promise body", |
|
56 "/x/y/z/4", "the fourth push promise body", |
|
57 "/x/y/z/5", "the fifth push promise body", |
|
58 "/x/y/z/6", "the sixth push promise body", |
|
59 "/x/y/z/7", "the seventh push promise body", |
|
60 "/x/y/z/8", "the eight push promise body", |
|
61 "/x/y/z/9", "the ninth push promise body" |
|
62 ); |
|
63 |
|
64 @Test |
|
65 public static void test() throws Exception { |
|
66 Http2TestServer server = null; |
|
67 try { |
|
68 server = new Http2TestServer(false, 0); |
|
69 Http2Handler handler = new ServerPushHandler("the main response body", |
|
70 PUSH_PROMISES); |
|
71 server.addHandler(handler, "/"); |
|
72 server.start(); |
|
73 int port = server.getAddress().getPort(); |
|
74 System.err.println("Server listening on port " + port); |
|
75 |
|
76 HttpClient client = HttpClient.newHttpClient(); |
|
77 // use multi-level path |
|
78 URI uri = new URI("http://127.0.0.1:" + port + "/foo/a/b/c"); |
|
79 HttpRequest request = HttpRequest.newBuilder(uri).GET().build(); |
|
80 |
|
81 ConcurrentMap<HttpRequest,CompletableFuture<HttpResponse<BodyAndType<?>>>> results = new ConcurrentHashMap<>(); |
|
82 PushPromiseHandler<BodyAndType<?>> bh = PushPromiseHandler.withPushPromises( |
|
83 (pushRequest) -> new BodyAndTypeHandler(pushRequest), results); |
|
84 |
|
85 CompletableFuture<HttpResponse<BodyAndType<?>>> cf = client.sendAsync(request, new BodyAndTypeHandler(request), bh); |
|
86 results.put(request, cf); |
|
87 cf.join(); |
|
88 System.err.println("CHEGAR: results.size: " + results.size()); |
|
89 |
|
90 if (results.size() != PUSH_PROMISES.size() + 1) |
|
91 throw new RuntimeException("Some results missing, expected:" |
|
92 + (PUSH_PROMISES.size() + 1) + ", got:" + results.size()); |
|
93 |
|
94 for (HttpRequest r : results.keySet()) { |
|
95 URI u = r.uri(); |
|
96 BodyAndType<?> body = results.get(r).get().body(); |
|
97 String result; |
|
98 // convert all body types to String for easier comparison |
|
99 if (body.type() == String.class) { |
|
100 result = (String)body.getBody(); |
|
101 } else if (body.type() == byte[].class) { |
|
102 byte[] bytes = (byte[])body.getBody(); |
|
103 result = new String(bytes, UTF_8); |
|
104 } else if (Path.class.isAssignableFrom(body.type())) { |
|
105 Path path = (Path)body.getBody(); |
|
106 result = new String(Files.readAllBytes(path), UTF_8); |
|
107 } else { |
|
108 throw new AssertionError("Unknown:" + body.type()); |
|
109 } |
|
110 |
|
111 System.err.printf("%s -> %s\n", u.toString(), result.toString()); |
|
112 String expected = PUSH_PROMISES.get(r.uri().getPath()); |
|
113 if (expected == null) |
|
114 expected = "the main response body"; |
|
115 System.err.println("For " + r + ", got [" + result + "], expected [" + expected +"]"); |
|
116 if (!result.equals(expected)) { |
|
117 throw new RuntimeException("For " + r + ", got [" + result + "], expected [" + expected +"]"); |
|
118 } |
|
119 } |
|
120 } finally { |
|
121 server.stop(); |
|
122 } |
|
123 } |
|
124 |
|
125 static interface BodyAndType<T> { |
|
126 Class<T> type(); |
|
127 T getBody(); |
|
128 } |
|
129 |
|
130 static final Path WORK_DIR = Paths.get("."); |
|
131 |
|
132 static class BodyAndTypeHandler implements BodyHandler<BodyAndType<?>> { |
|
133 int count; |
|
134 final HttpRequest request; |
|
135 |
|
136 BodyAndTypeHandler(HttpRequest request) { |
|
137 this.request = request; |
|
138 } |
|
139 |
|
140 @Override |
|
141 public HttpResponse.BodySubscriber<BodyAndType<?>> apply(int statusCode, |
|
142 HttpHeaders responseHeaders) { |
|
143 int whichType = count++ % 3; // real world may base this on the request metadata |
|
144 switch (whichType) { |
|
145 case 0: // String |
|
146 return new BodyAndTypeSubscriber(BodySubscriber.asString(StandardCharsets.UTF_8)); |
|
147 case 1: // byte[] |
|
148 return new BodyAndTypeSubscriber(BodySubscriber.asByteArray()); |
|
149 case 2: // Path |
|
150 URI u = request.uri(); |
|
151 Path path = Paths.get(WORK_DIR.toString(), u.getPath()); |
|
152 try { |
|
153 Files.createDirectories(path.getParent()); |
|
154 } catch (IOException ee) { |
|
155 throw new UncheckedIOException(ee); |
|
156 } |
|
157 return new BodyAndTypeSubscriber(BodySubscriber.asFile(path)); |
|
158 default: |
|
159 throw new AssertionError("Unexpected " + whichType); |
|
160 } |
|
161 } |
|
162 } |
|
163 |
|
164 static class BodyAndTypeSubscriber<T> implements HttpResponse.BodySubscriber<BodyAndType<T>> { |
|
165 |
|
166 private static class BodyAndTypeImpl<T> implements BodyAndType<T> { |
|
167 private final Class<T> type; |
|
168 private final T body; |
|
169 public BodyAndTypeImpl(Class<T> type, T body) { this.type = type; this.body = body; } |
|
170 @Override public Class<T> type() { return type; } |
|
171 @Override public T getBody() { return body; } |
|
172 } |
|
173 |
|
174 private final BodySubscriber<?> bodySubscriber; |
|
175 private final CompletableFuture<BodyAndType<T>> cf; |
|
176 |
|
177 BodyAndTypeSubscriber(BodySubscriber bodySubscriber) { |
|
178 this.bodySubscriber = bodySubscriber; |
|
179 cf = new CompletableFuture<>(); |
|
180 bodySubscriber.getBody().whenComplete((r,t) -> cf.complete(new BodyAndTypeImpl(r.getClass(), r))); |
|
181 } |
|
182 |
|
183 @Override |
|
184 public void onSubscribe(Flow.Subscription subscription) { |
|
185 bodySubscriber.onSubscribe(subscription); |
|
186 } |
|
187 |
|
188 @Override |
|
189 public void onNext(List<ByteBuffer> item) { |
|
190 bodySubscriber.onNext(item); |
|
191 } |
|
192 |
|
193 @Override |
|
194 public void onError(Throwable throwable) { |
|
195 bodySubscriber.onError(throwable); |
|
196 cf.completeExceptionally(throwable); |
|
197 } |
|
198 |
|
199 @Override |
|
200 public void onComplete() { |
|
201 bodySubscriber.onComplete(); |
|
202 } |
|
203 |
|
204 @Override |
|
205 public CompletionStage<BodyAndType<T>> getBody() { |
|
206 return cf; |
|
207 } |
|
208 } |
|
209 |
|
210 // --- server push handler --- |
|
211 static class ServerPushHandler implements Http2Handler { |
|
212 |
|
213 private final String mainResponseBody; |
|
214 private final Map<String,String> promises; |
|
215 |
|
216 public ServerPushHandler(String mainResponseBody, Map<String,String> promises) throws Exception { |
|
217 Objects.requireNonNull(promises); |
|
218 this.mainResponseBody = mainResponseBody; |
|
219 this.promises = promises; |
|
220 } |
|
221 |
|
222 public void handle(Http2TestExchange exchange) throws IOException { |
|
223 System.err.println("Server: handle " + exchange); |
|
224 try (InputStream is = exchange.getRequestBody()) { |
|
225 is.readAllBytes(); |
|
226 } |
|
227 |
|
228 if (exchange.serverPushAllowed()) { |
|
229 pushPromises(exchange); |
|
230 } |
|
231 |
|
232 // response data for the main response |
|
233 try (OutputStream os = exchange.getResponseBody()) { |
|
234 byte[] bytes = mainResponseBody.getBytes(UTF_8); |
|
235 exchange.sendResponseHeaders(200, bytes.length); |
|
236 os.write(bytes); |
|
237 } |
|
238 } |
|
239 |
|
240 private void pushPromises(Http2TestExchange exchange) throws IOException { |
|
241 URI requestURI = exchange.getRequestURI(); |
|
242 for (Map.Entry<String,String> promise : promises.entrySet()) { |
|
243 URI uri = requestURI.resolve(promise.getKey()); |
|
244 InputStream is = new ByteArrayInputStream(promise.getValue().getBytes(UTF_8)); |
|
245 HttpHeadersImpl headers = new HttpHeadersImpl(); |
|
246 headers.addHeader("X-Promise-"+promise.getKey(), promise.getKey()); // todo: add some check on headers, maybe |
|
247 exchange.serverPush(uri, headers, is); |
|
248 } |
|
249 System.err.println("Server: All pushes sent"); |
|
250 } |
|
251 } |
|
252 } |