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