From c74e42e928492293a2efab2f6943b81515dd1a27 Mon Sep 17 00:00:00 2001 From: Simon Stewart Date: Wed, 30 Aug 2017 22:30:08 +0100 Subject: [PATCH] Provide a pure JRE implementation of HttpClient So we can avoid having to take a dependency on Apache HttpClient. --- .../remote/internal/JreHttpClient.java | 114 ++++++++++++++++++ .../remote/internal/JreHttpClientTest.java | 105 ++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 java/client/src/org/openqa/selenium/remote/internal/JreHttpClient.java create mode 100644 java/client/test/org/openqa/selenium/remote/internal/JreHttpClientTest.java diff --git a/java/client/src/org/openqa/selenium/remote/internal/JreHttpClient.java b/java/client/src/org/openqa/selenium/remote/internal/JreHttpClient.java new file mode 100644 index 0000000000000..806d663e1437a --- /dev/null +++ b/java/client/src/org/openqa/selenium/remote/internal/JreHttpClient.java @@ -0,0 +1,114 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.remote.internal; + +import org.openqa.selenium.remote.http.HttpClient; +import org.openqa.selenium.remote.http.HttpMethod; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class JreHttpClient implements HttpClient { + + private final URL url; + + private JreHttpClient(URL url) { + if (!url.getProtocol().toLowerCase().startsWith("http")) { + throw new IllegalArgumentException("Base URL must be an http URL: " + url); + } + this.url = url; + } + + @Override + public HttpResponse execute(HttpRequest request, boolean followRedirects) throws IOException { + URL url = new URL(this.url.toString() + request.getUri()); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + try { + connection.setInstanceFollowRedirects(followRedirects); + for (String name : request.getHeaderNames()) { + for (String value : request.getHeaders(name)) { + connection.addRequestProperty(name, value); + } + } + connection.setRequestProperty("Content-length", String.valueOf(request.getContent().length)); + connection.setRequestMethod(request.getMethod().toString()); + connection.setDoInput(true); + + if (request.getMethod() == HttpMethod.POST) { + connection.setDoOutput(true); + try (OutputStream os = connection.getOutputStream()) { + os.write(request.getContent()); + } + } + + HttpResponse response = new HttpResponse(); + response.setStatus(connection.getResponseCode()); + + for (Map.Entry> entry : connection.getHeaderFields().entrySet()) { + // HttpURLConnection stores headers in a HashMap. The order they come back is basically + // random. + for (String value : entry.getValue()) { + response.addHeader(entry.getKey(), value); + } + } + + InputStream is = connection.getErrorStream(); + if (is == null) { + is = connection.getInputStream(); + } + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + int count; + byte[] data = new byte[1024]; + while ((count = is.read(data, 0, data.length)) != -1) { + bos.write(data, 0, count); + } + bos.flush(); + response.setContent(bos.toByteArray()); + } finally { + is.close(); + } + + return response; + } finally { + connection.disconnect(); + } + } + + @Override + public void close() throws IOException { + + } + + public static class Factory implements HttpClient.Factory { + + @Override + public HttpClient createClient(URL url) { + return new JreHttpClient(Objects.requireNonNull(url, "Base URL must be set")); + } + } +} diff --git a/java/client/test/org/openqa/selenium/remote/internal/JreHttpClientTest.java b/java/client/test/org/openqa/selenium/remote/internal/JreHttpClientTest.java new file mode 100644 index 0000000000000..7b27d0c9c42f0 --- /dev/null +++ b/java/client/test/org/openqa/selenium/remote/internal/JreHttpClientTest.java @@ -0,0 +1,105 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.remote.internal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; + +import org.junit.Test; +import org.openqa.selenium.net.PortProber; +import org.openqa.selenium.remote.http.HttpClient; +import org.openqa.selenium.remote.http.HttpMethod; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; +import org.seleniumhq.jetty9.server.Server; +import org.seleniumhq.jetty9.servlet.ServletContextHandler; +import org.seleniumhq.jetty9.servlet.ServletHolder; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class JreHttpClientTest { + + @Test + public void responseShouldCaptureASingleHeader() throws Exception { + HashMultimap headers = HashMultimap.create(); + headers.put("Cake", "Delicious"); + + HttpResponse response = getResponseWithHeaders(headers); + + String value = response.getHeader("Cake"); + assertEquals("Delicious", value); + } + + /** + * The HTTP Spec that it should be + * safe to combine them + * , but things like the cookie spec make + * this hard (notably when a legal value may contain a comma). + */ + @Test + public void responseShouldKeepMultipleHeadersSeparate() throws Exception { + HashMultimap headers = HashMultimap.create(); + headers.put("Cheese", "Cheddar"); + headers.put("Cheese", "Brie, Gouda"); + + HttpResponse response = getResponseWithHeaders(headers); + + ImmutableList values = ImmutableList.copyOf(response.getHeaders("Cheese")); + + assertTrue(values.contains("Cheddar")); + assertTrue(values.contains("Brie, Gouda")); + } + + private HttpResponse getResponseWithHeaders(final Multimap headers) + throws Exception { + Server server = new Server(PortProber.findFreePort()); + ServletContextHandler handler = new ServletContextHandler(); + handler.setContextPath(""); + + class Headers extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + headers.forEach(resp::addHeader); + resp.setContentLengthLong(0); + } + } + ServletHolder holder = new ServletHolder(new Headers()); + handler.addServlet(holder, "/*"); + + server.setHandler(handler); + + server.start(); + try { + HttpClient client = new JreHttpClient.Factory().createClient(server.getURI().toURL()); + HttpRequest request = new HttpRequest(HttpMethod.GET, "/foo"); + return client.execute(request, true); + } finally { + server.stop(); + } + } +}