From: Simon Brandhof Date: Wed, 23 Aug 2017 15:18:08 +0000 (+0200) Subject: SONAR-9740 support HTTP headers in API Request and sonar-ws X-Git-Tag: 6.6-RC1~348 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=9d5fc95ed97f0dc0e1a797065b5a3870449a9ded;p=sonarqube.git SONAR-9740 support HTTP headers in API Request and sonar-ws --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java b/server/sonar-server/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java index 0f13fae0161..64ce9d05baa 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java @@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Optional; import org.sonar.api.server.ws.LocalConnector; import org.sonar.api.server.ws.internal.ValidatingRequest; @@ -77,4 +78,9 @@ public class LocalRequestAdapter extends ValidatingRequest { public String getMediaType() { return localRequest.getMediaType(); } + + @Override + public Optional header(String name) { + return localRequest.getHeader(name); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/ws/ServletRequest.java b/server/sonar-server/src/main/java/org/sonar/server/ws/ServletRequest.java index 5c0afa73a14..b22822bc1fa 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/ws/ServletRequest.java +++ b/server/sonar-server/src/main/java/org/sonar/server/ws/ServletRequest.java @@ -25,6 +25,7 @@ import com.google.common.net.HttpHeaders; import java.io.InputStream; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.CheckForNull; import javax.servlet.http.HttpServletRequest; import org.sonar.api.server.ws.internal.PartImpl; @@ -136,4 +137,8 @@ public class ServletRequest extends ValidatingRequest { return source.getRequestURI().replaceFirst(source.getContextPath(), ""); } + @Override + public Optional header(String name) { + return Optional.ofNullable(source.getHeader(name)); + } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/ws/ServletRequestTest.java b/server/sonar-server/src/test/java/org/sonar/server/ws/ServletRequestTest.java index 4fc4a5c8c08..2108ba4f030 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/ws/ServletRequestTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/ws/ServletRequestTest.java @@ -41,9 +41,9 @@ public class ServletRequestTest { @Rule public ExpectedException expectedException = ExpectedException.none(); - HttpServletRequest source = mock(HttpServletRequest.class); + private HttpServletRequest source = mock(HttpServletRequest.class); - ServletRequest underTest = new ServletRequest(source); + private ServletRequest underTest = new ServletRequest(source); @Test public void call_method() { @@ -184,4 +184,23 @@ public class ServletRequestTest { assertThat(underTest.toString()).isEqualTo("http:localhost:9000/api/issues?components=sonar"); } + + @Test + public void header_returns_the_value_of_http_header() { + when(source.getHeader("Accept")).thenReturn("text/plain"); + assertThat(underTest.header("Accept")).hasValue("text/plain"); + } + + @Test + public void header_is_empty_if_absent_from_request() { + when(source.getHeader("Accept")).thenReturn(null); + assertThat(underTest.header("Accept")).isEmpty(); + } + + @Test + public void header_has_empty_value_if_present_in_request_without_value() { + when(source.getHeader("Accept")).thenReturn(""); + assertThat(underTest.header("Accept")).hasValue(""); + } + } diff --git a/server/sonar-server/src/test/java/org/sonar/server/ws/TestRequest.java b/server/sonar-server/src/test/java/org/sonar/server/ws/TestRequest.java index 5d173a9071c..2e83af69bce 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/ws/TestRequest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/ws/TestRequest.java @@ -28,6 +28,7 @@ import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import org.apache.commons.io.IOUtils; import org.sonar.api.server.ws.internal.PartImpl; import org.sonar.api.server.ws.internal.ValidatingRequest; @@ -40,6 +41,7 @@ public class TestRequest extends ValidatingRequest { private final ListMultimap multiParams = ArrayListMultimap.create(); private final Map params = new HashMap<>(); + private final Map headers = new HashMap<>(); private final Map parts = Maps.newHashMap(); private String method = "GET"; private String mimeType = "application/octet-stream"; @@ -127,6 +129,16 @@ public class TestRequest extends ValidatingRequest { return this; } + @Override + public Optional header(String name) { + return Optional.ofNullable(headers.get(name)); + } + + public TestRequest setHeader(String name, String value) { + this.headers.put(requireNonNull(name), requireNonNull(value)); + return this; + } + public TestResponse execute() { try { DumbResponse response = new DumbResponse(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/ws/WsTester.java b/server/sonar-server/src/test/java/org/sonar/server/ws/WsTester.java index 043fb1fa297..c2aac40d96a 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/ws/WsTester.java +++ b/server/sonar-server/src/test/java/org/sonar/server/ws/WsTester.java @@ -20,18 +20,17 @@ package org.sonar.server.ws; import com.google.common.collect.Maps; -import com.google.protobuf.GeneratedMessage; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.lang.reflect.Method; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.io.IOUtils; @@ -47,6 +46,7 @@ import org.sonarqube.ws.MediaTypes; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.server.ws.RequestVerifier.verifyRequest; @@ -64,6 +64,7 @@ public class WsTester { private String mediaType = MediaTypes.JSON; private Map params = Maps.newHashMap(); + private Map headers = Maps.newHashMap(); private final Map parts = Maps.newHashMap(); private TestRequest(String method) { @@ -90,6 +91,16 @@ public class WsTester { return params.keySet().contains(key); } + @Override + public Optional header(String name) { + return Optional.ofNullable(headers.get(name)); + } + + public TestRequest setHeader(String name, String value) { + this.headers.put(requireNonNull(name), requireNonNull(value)); + return this; + } + @Override public String getPath() { return path; diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/LocalConnector.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/LocalConnector.java index a6cdc79c098..7e12c0fd141 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/LocalConnector.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/LocalConnector.java @@ -21,6 +21,7 @@ package org.sonar.api.server.ws; import java.util.Collection; import java.util.List; +import java.util.Optional; import javax.annotation.CheckForNull; /** @@ -92,6 +93,12 @@ public interface LocalConnector { * @see Request#multiParam(String) */ List getMultiParam(String key); + + /** + * @see Request#header(String) + * @since 6.6 + */ + Optional getHeader(String name); } interface LocalResponse { diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java index 2428fc441f5..2a210e519e2 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Supplier; @@ -301,6 +302,14 @@ public abstract class Request { return AbsentStringParam.absent(); } + /** + * Optional value of the HTTP header with specified name. + * If present, the result can have an empty string value ({@code ""}). + * + * @since 6.6 + */ + public abstract Optional header(String name); + /** * Allows a web service to call another web service. * @see LocalConnector diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/internal/SimpleGetRequest.java b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/internal/SimpleGetRequest.java index ce65867be76..323d7a98872 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/internal/SimpleGetRequest.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/server/ws/internal/SimpleGetRequest.java @@ -24,6 +24,7 @@ import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.io.IOUtils; @@ -44,6 +45,7 @@ public class SimpleGetRequest extends Request { private final Map params = new HashMap<>(); private final Map parts = new HashMap<>(); + private final Map headers = new HashMap<>(); private String mediaType = "application/json"; private String path; @@ -130,4 +132,13 @@ public class SimpleGetRequest extends Request { return this; } + @Override + public Optional header(String name) { + return Optional.ofNullable(headers.get(name)); + } + + public SimpleGetRequest setHeader(String name, String value) { + headers.put(name, value); + return this; + } } diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/server/ws/RequestTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/server/ws/RequestTest.java index b940b953d08..d9395d5b598 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/server/ws/RequestTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/server/ws/RequestTest.java @@ -29,6 +29,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; import org.apache.commons.io.IOUtils; @@ -469,7 +470,7 @@ public class RequestTest { @DataProvider public static Object[][] date_times() { - return new Object[][] { + return new Object[][]{ {"2014-05-27", parseDate("2014-05-27")}, {"2014-05-27T15:50:45+0100", parseDateTime("2014-05-27T15:50:45+0100")}, {null, null} @@ -584,6 +585,7 @@ public class RequestTest { private final ListMultimap multiParams = ArrayListMultimap.create(); private final Map params = new HashMap<>(); private final Map parts = new HashMap<>(); + private final Map headers = new HashMap<>(); @Override public String method() { @@ -644,6 +646,16 @@ public class RequestTest { parts.put(key, new PartImpl(input, fileName)); return this; } + + @Override + public Optional header(String name) { + return Optional.ofNullable(headers.get(name)); + } + + public FakeRequest setHeader(String name, String value) { + headers.put(name, value); + return this; + } } private static class FakeWs implements WebService { diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/server/ws/internal/SimpleGetRequestTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/server/ws/internal/SimpleGetRequestTest.java index d048eb6351b..15a73dbf396 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/server/ws/internal/SimpleGetRequestTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/server/ws/internal/SimpleGetRequestTest.java @@ -88,4 +88,21 @@ public class SimpleGetRequestTest { assertThat(underTest.getParams()).containsOnly(entry("foo", "bar"), entry("fee", "beer")); } + + @Test + public void header_returns_empty_if_header_is_not_present() { + assertThat(underTest.header("foo")).isEmpty(); + } + + @Test + public void header_returns_value_of_header_if_present() { + underTest.setHeader("foo", "bar"); + assertThat(underTest.header("foo")).hasValue("bar"); + } + + @Test + public void header_returns_empty_string_value_if_header_is_present_without_value() { + underTest.setHeader("foo", ""); + assertThat(underTest.header("foo")).hasValue(""); + } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java index db88b99821a..8b5fbcff005 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java @@ -22,10 +22,12 @@ package org.sonarqube.ws.client; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import java.util.Collection; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -36,6 +38,7 @@ import org.sonarqube.ws.MediaTypes; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; import static java.util.Collections.singletonList; +import static java.util.Collections.unmodifiableSet; import static java.util.Objects.requireNonNull; abstract class BaseRequest implements WsRequest { @@ -45,6 +48,7 @@ abstract class BaseRequest implements WsRequest { private String mediaType = MediaTypes.JSON; private final DefaultParameters parameters = new DefaultParameters(); + private final DefaultHeaders headers = new DefaultHeaders(); BaseRequest(String path) { this.path = path; @@ -131,6 +135,17 @@ abstract class BaseRequest implements WsRequest { return parameters; } + @Override + public Headers getHeaders() { + return headers; + } + + public SELF setHeader(String name, @Nullable String value) { + requireNonNull(name, "Header name can't be null"); + headers.setValue(name, value); + return (SELF) this; + } + private static class DefaultParameters implements Parameters { // preserve insertion order private final ListMultimap keyValues = LinkedListMultimap.create(); @@ -168,4 +183,29 @@ abstract class BaseRequest implements WsRequest { return this; } } + + private static class DefaultHeaders implements Headers { + private final Map keyValues = new HashMap<>(); + + @Override + public Optional getValue(String name) { + return Optional.ofNullable(keyValues.get(name)); + } + + private DefaultHeaders setValue(String name, @Nullable String value) { + checkArgument(!isNullOrEmpty(name)); + + if (value == null) { + keyValues.remove(name); + } else { + keyValues.put(name, value); + } + return this; + } + + @Override + public Set getNames() { + return unmodifiableSet(keyValues.keySet()); + } + } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/Headers.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/Headers.java new file mode 100644 index 00000000000..b289789a1f2 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/Headers.java @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonarqube.ws.client; + +import java.util.Optional; +import java.util.Set; + +/** + * HTTP headers + * + * @since 6.6 + */ +public interface Headers { + + Optional getValue(String name); + + Set getNames(); + +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java index e652a21e28f..fcc970b7b05 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java @@ -159,11 +159,13 @@ public class HttpConnector implements WsConnector { private Request.Builder prepareOkRequestBuilder(WsRequest getRequest, HttpUrl.Builder urlBuilder) { Request.Builder okHttpRequestBuilder = new Request.Builder() .url(urlBuilder.build()) - .addHeader("Accept", getRequest.getMediaType()) - .addHeader("Accept-Charset", "UTF-8"); + .header("Accept", getRequest.getMediaType()) + .header("Accept-Charset", "UTF-8"); if (credentials != null) { okHttpRequestBuilder.header("Authorization", credentials); } + getRequest.getHeaders().getNames().forEach(name -> + okHttpRequestBuilder.header(name, getRequest.getHeaders().getValue(name).get())); return okHttpRequestBuilder; } @@ -199,6 +201,7 @@ public class HttpConnector implements WsConnector { /** * Private since 5.5. + * * @see HttpConnector#newBuilder() */ private Builder() { diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/LocalWsConnector.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/LocalWsConnector.java index 3f2cee3a5c7..5dca504438e 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/LocalWsConnector.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/LocalWsConnector.java @@ -25,6 +25,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.util.List; +import java.util.Optional; import org.sonar.api.server.ws.LocalConnector; import static java.nio.charset.StandardCharsets.UTF_8; @@ -56,11 +57,9 @@ class LocalWsConnector implements WsConnector { private static class DefaultLocalRequest implements LocalConnector.LocalRequest { private final WsRequest wsRequest; - private final Parameters parameters; public DefaultLocalRequest(WsRequest wsRequest) { this.wsRequest = wsRequest; - this.parameters = wsRequest.getParameters(); } @Override @@ -80,17 +79,22 @@ class LocalWsConnector implements WsConnector { @Override public boolean hasParam(String key) { - return !parameters.getValues(key).isEmpty(); + return !wsRequest.getParameters().getValues(key).isEmpty(); } @Override public String getParam(String key) { - return parameters.getValue(key); + return wsRequest.getParameters().getValue(key); } @Override public List getMultiParam(String key) { - return parameters.getValues(key); + return wsRequest.getParameters().getValues(key); + } + + @Override + public Optional getHeader(String name) { + return wsRequest.getHeaders().getValue(name); } } diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java index b5ad96a4542..9907ae7b39e 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java @@ -43,6 +43,8 @@ public interface WsRequest { Parameters getParameters(); + Headers getHeaders(); + enum Method { GET, POST } diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/BaseRequestTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/BaseRequestTest.java index b473e805eef..c639641a079 100644 --- a/sonar-ws/src/test/java/org/sonarqube/ws/client/BaseRequestTest.java +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/BaseRequestTest.java @@ -38,7 +38,7 @@ public class BaseRequestTest { @Rule public ExpectedException expectedException = ExpectedException.none(); - FakeRequest underTest = new FakeRequest("api/foo"); + private FakeRequest underTest = new FakeRequest("api/foo"); @Test public void test_defaults() { @@ -96,6 +96,22 @@ public class BaseRequestTest { underTest.setParam(null, "val"); } + @Test + public void headers_are_empty_by_default() { + assertThat(underTest.getHeaders().getNames()).isEmpty(); + } + + @Test + public void set_and_get_headers() { + underTest.setHeader("foo", "fooz"); + underTest.setHeader("bar", "barz"); + + assertThat(underTest.getHeaders().getNames()).containsExactlyInAnyOrder("foo", "bar"); + assertThat(underTest.getHeaders().getValue("foo")).hasValue("fooz"); + assertThat(underTest.getHeaders().getValue("bar")).hasValue("barz"); + assertThat(underTest.getHeaders().getValue("xxx")).isEmpty(); + } + private void assertParameters(MapEntry... values) { Parameters parameters = underTest.getParameters(); assertThat(parameters.getKeys()).extracting(key -> MapEntry.entry(key, parameters.getValue(key))).containsExactly(values); diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java index 449b4fcebe8..c727235a939 100644 --- a/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java @@ -95,6 +95,21 @@ public class HttpConnectorTest { assertThat(recordedRequest.getHeader("Accept-Encoding")).isEqualTo("gzip"); } + @Test + public void add_headers_to_GET_request() throws Exception { + answerHelloWorld(); + GetRequest request = new GetRequest("api/issues/search") + .setHeader("X-Foo", "fooz") + .setHeader("X-Bar", "barz"); + + underTest = HttpConnector.newBuilder().url(serverUrl).build(); + underTest.call(request); + + RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getHeader("X-Foo")).isEqualTo("fooz"); + assertThat(recordedRequest.getHeader("X-Bar")).isEqualTo("barz"); + } + @Test public void use_basic_authentication() throws Exception { answerHelloWorld(); @@ -256,6 +271,22 @@ public class HttpConnectorTest { assertThat(recordedRequest.getMethod()).isEqualTo("POST"); assertThat(recordedRequest.getPath()).isEqualTo("/api/issues/search"); assertThat(recordedRequest.getBody().readUtf8()).isEqualTo("severity=MAJOR"); + assertThat(recordedRequest.getHeader("Accept")).isEqualTo("application/x-protobuf"); + } + + @Test + public void add_header_to_POST_request() throws Exception { + answerHelloWorld(); + PostRequest request = new PostRequest("api/issues/search") + .setHeader("X-Foo", "fooz") + .setHeader("X-Bar", "barz"); + + underTest = HttpConnector.newBuilder().url(serverUrl).build(); + underTest.call(request); + + RecordedRequest recordedRequest = server.takeRequest(); + assertThat(recordedRequest.getHeader("X-Foo")).isEqualTo("fooz"); + assertThat(recordedRequest.getHeader("X-Bar")).isEqualTo("barz"); } @Test