]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9740 support HTTP headers in API Request and sonar-ws
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 23 Aug 2017 15:18:08 +0000 (17:18 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 13 Sep 2017 13:50:49 +0000 (15:50 +0200)
17 files changed:
server/sonar-server/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java
server/sonar-server/src/main/java/org/sonar/server/ws/ServletRequest.java
server/sonar-server/src/test/java/org/sonar/server/ws/ServletRequestTest.java
server/sonar-server/src/test/java/org/sonar/server/ws/TestRequest.java
server/sonar-server/src/test/java/org/sonar/server/ws/WsTester.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/LocalConnector.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/internal/SimpleGetRequest.java
sonar-plugin-api/src/test/java/org/sonar/api/server/ws/RequestTest.java
sonar-plugin-api/src/test/java/org/sonar/api/server/ws/internal/SimpleGetRequestTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/BaseRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/Headers.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/HttpConnector.java
sonar-ws/src/main/java/org/sonarqube/ws/client/LocalWsConnector.java
sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java
sonar-ws/src/test/java/org/sonarqube/ws/client/BaseRequestTest.java
sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java

index 0f13fae01615ecad84836193acba14dc63db3015..64ce9d05baa277ada16d1b141c32169fd3894a99 100644 (file)
@@ -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<String> header(String name) {
+    return localRequest.getHeader(name);
+  }
 }
index 5c0afa73a145bd668335fd90f541302edf83af03..b22822bc1faadf576e69e8a49033f0b676b7a26c 100644 (file)
@@ -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<String> header(String name) {
+    return Optional.ofNullable(source.getHeader(name));
+  }
 }
index 4fc4a5c8c082c38d1ed31ebcbe65951f102aed2b..2108ba4f030d90dce5cabc5ba08b7628c54aadbc 100644 (file)
@@ -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("");
+  }
+
 }
index 5d173a9071c82e4c6a0c46ca71c54e42313c1920..2e83af69bce9fcdce18ee00663c229ecd5231e5b 100644 (file)
@@ -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<String, String> multiParams = ArrayListMultimap.create();
   private final Map<String, String> params = new HashMap<>();
+  private final Map<String, String> headers = new HashMap<>();
   private final Map<String, Part> 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<String> 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();
index 043fb1fa29700b802e524b14894ae504dbddde5e..c2aac40d96a124a1782313073a817b941ad54e6f 100644 (file)
 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<String, String> params = Maps.newHashMap();
+    private Map<String, String> headers = Maps.newHashMap();
     private final Map<String, Part> parts = Maps.newHashMap();
 
     private TestRequest(String method) {
@@ -90,6 +91,16 @@ public class WsTester {
       return params.keySet().contains(key);
     }
 
+    @Override
+    public Optional<String> 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;
index a6cdc79c098de5c9d13673f7c20c0e4d4a769051..7e12c0fd1415eaa972637218d8ca22cb1d9e19a7 100644 (file)
@@ -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<String> getMultiParam(String key);
+
+    /**
+     * @see Request#header(String)
+     * @since 6.6
+     */
+    Optional<String> getHeader(String name);
   }
 
   interface LocalResponse {
index 2428fc441f57cd2fef3e8385ff398b40c059b812..2a210e519e2ad6ccfd40f51ef3043be1223d1f72 100644 (file)
@@ -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<String> header(String name);
+
   /**
    * Allows a web service to call another web service.
    * @see LocalConnector
index ce65867be76a37b51f1e799cff6c5f075ada4d39..323d7a98872d60f772a60f226d06a1b075343cf4 100644 (file)
@@ -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<String, String> params = new HashMap<>();
   private final Map<String, Part> parts = new HashMap<>();
+  private final Map<String, String> 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<String> header(String name) {
+    return Optional.ofNullable(headers.get(name));
+  }
+
+  public SimpleGetRequest setHeader(String name, String value) {
+    headers.put(name, value);
+    return this;
+  }
 }
index b940b953d0897a66be996aa3bfe195a92621bbfe..d9395d5b598881f51beca2311ca69deb26f88a4f 100644 (file)
@@ -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<String, String> multiParams = ArrayListMultimap.create();
     private final Map<String, String> params = new HashMap<>();
     private final Map<String, Part> parts = new HashMap<>();
+    private final Map<String, String> 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<String> 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 {
index d048eb6351b1ddbaccd22bb435b9cabaafb3b570..15a73dbf396ced11ff0f1c2643e4236df28ecfe2 100644 (file)
@@ -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("");
+  }
 }
index db88b99821a6e05830721cf3ac020de219047e84..8b5fbcff005d0affc734d723015913b54c9f60b2 100644 (file)
@@ -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<SELF extends BaseRequest> implements WsRequest {
@@ -45,6 +48,7 @@ abstract class BaseRequest<SELF extends 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<SELF extends 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<String, String> keyValues = LinkedListMultimap.create();
@@ -168,4 +183,29 @@ abstract class BaseRequest<SELF extends BaseRequest> implements WsRequest {
       return this;
     }
   }
+
+  private static class DefaultHeaders implements Headers {
+    private final Map<String, String> keyValues = new HashMap<>();
+
+    @Override
+    public Optional<String> 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<String> 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 (file)
index 0000000..b289789
--- /dev/null
@@ -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<String> getValue(String name);
+
+  Set<String> getNames();
+
+}
index e652a21e28ff0427d7e077517d975879a583256e..fcc970b7b05e059b6a876dd9e3ad5d52245eeb5a 100644 (file)
@@ -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() {
index 3f2cee3a5c7d74bbaf769b17f2f3b482e57c9402..5dca504438e965a31bbf98b13e4b14ca14446c56 100644 (file)
@@ -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<String> getMultiParam(String key) {
-      return parameters.getValues(key);
+      return wsRequest.getParameters().getValues(key);
+    }
+
+    @Override
+    public Optional<String> getHeader(String name) {
+      return wsRequest.getHeaders().getValue(name);
     }
   }
 
index b5ad96a454292a4090c00eb3d26c658af82e615f..9907ae7b39e11dfc5dfa51e5a26115a9fa4beda8 100644 (file)
@@ -43,6 +43,8 @@ public interface WsRequest {
 
   Parameters getParameters();
 
+  Headers getHeaders();
+
   enum Method {
     GET, POST
   }
index b473e805eefaae088a024830dd208169e1b363a2..c639641a079269abcf829d36d913e1784f053db6 100644 (file)
@@ -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<String, String>... values) {
     Parameters parameters = underTest.getParameters();
     assertThat(parameters.getKeys()).extracting(key -> MapEntry.entry(key, parameters.getValue(key))).containsExactly(values);
index 449b4fcebe8766c1fac1747420f8396611d80fa4..c727235a939137e271c35c4b95000a4c85702b49 100644 (file)
@@ -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