]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6948 Ability for Java server extensions to call web services
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 6 Apr 2016 07:57:19 +0000 (09:57 +0200)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Thu, 7 Apr 2016 10:12:32 +0000 (12:12 +0200)
36 files changed:
it/it-plugins/pom.xml
it/it-plugins/ws-plugin/pom.xml [new file with mode: 0644]
it/it-plugins/ws-plugin/src/main/java/LocalCallWebService.java [new file with mode: 0644]
it/it-plugins/ws-plugin/src/main/java/WsPlugin.java [new file with mode: 0644]
it/it-tests/src/test/java/it/Category4Suite.java
it/it-tests/src/test/java/it/user/LocalAuthenticationTest.java
it/it-tests/src/test/java/it/ws/WsLocalCallTest.java [new file with mode: 0644]
it/it-tests/src/test/java/util/ItUtils.java
server/sonar-server/src/main/java/org/sonar/server/ws/DefaultLocalResponse.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/ws/WebServiceEngine.java
server/sonar-web/npm.tar.gz [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/LocalConnector.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Request.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/RequestHandler.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/Response.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/WebService.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/internal/SimpleGetRequest.java
sonar-plugin-api/src/main/java/org/sonar/api/server/ws/internal/ValidatingRequest.java
sonar-scanner-engine/src/main/java/org/sonar/batch/bootstrap/BatchWsClientProvider.java
sonar-ws/pom.xml
sonar-ws/src/main/java/org/sonarqube/ws/client/BaseResponse.java
sonar-ws/src/main/java/org/sonarqube/ws/client/BaseService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.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/HttpResponse.java [deleted file]
sonar-ws/src/main/java/org/sonarqube/ws/client/HttpWsClient.java [deleted file]
sonar-ws/src/main/java/org/sonarqube/ws/client/LocalWsClientFactory.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/LocalWsConnector.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/OkHttpResponse.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java
sonar-ws/src/main/java/org/sonarqube/ws/client/WsClientFactories.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/WsClientFactory.java [new file with mode: 0644]
sonar-ws/src/test/java/org/sonarqube/ws/client/HttpConnectorTest.java
sonar-ws/src/test/java/org/sonarqube/ws/client/LocalWsConnectorTest.java [new file with mode: 0644]
sonar-ws/src/test/java/org/sonarqube/ws/client/WsClientFactoriesTest.java [new file with mode: 0644]

index bee94bef359a34efdbca44f272b4a80ce64ea8d3..c931e338939ac5c514b866dd90f3bc43acb75232 100644 (file)
@@ -36,6 +36,7 @@
     <module>dashboard-plugin</module>
     <module>extension-lifecycle-plugin</module>
     <module>global-property-change-plugin</module>
+    <module>issue-filter-plugin</module>
     <module>l10n-fr-pack</module>
     <module>license-plugin</module>
     <module>oauth2-auth-plugin</module>
@@ -50,7 +51,7 @@
     <module>sonar-fake-plugin</module>
     <module>sonar-subcategories-plugin</module>
     <module>ui-extensions-plugin</module>
-    <module>issue-filter-plugin</module>
     <module>posttask-plugin</module>
+    <module>ws-plugin</module>
   </modules>
 </project>
diff --git a/it/it-plugins/ws-plugin/pom.xml b/it/it-plugins/ws-plugin/pom.xml
new file mode 100644 (file)
index 0000000..6ac4a5d
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.sonarsource.sonarqube</groupId>
+    <artifactId>it-plugins</artifactId>
+    <version>5.5-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>ws-plugin</artifactId>
+  <version>1.0-SNAPSHOT</version>
+  <packaging>sonar-plugin</packaging>
+  <name>SonarQube Integration Tests :: Plugins :: Ws</name>
+  <description>Plugin for WS tests</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.sonarsource.sonarqube</groupId>
+      <artifactId>sonar-plugin-api</artifactId>
+      <version>${apiVersion}</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.sonarsource.sonarqube</groupId>
+      <artifactId>sonar-ws</artifactId>
+      <version>${apiVersion}</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.sonarsource.sonar-packaging-maven-plugin</groupId>
+        <artifactId>sonar-packaging-maven-plugin</artifactId>
+        <extensions>true</extensions>
+        <configuration>
+          <pluginClass>WsPlugin</pluginClass>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/it/it-plugins/ws-plugin/src/main/java/LocalCallWebService.java b/it/it-plugins/ws-plugin/src/main/java/LocalCallWebService.java
new file mode 100644 (file)
index 0000000..b878ef2
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.RequestHandler;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonarqube.ws.MediaTypes;
+import org.sonarqube.ws.WsCe;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.LocalWsClientFactory;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsResponse;
+
+public final class LocalCallWebService implements WebService {
+
+  private final LocalWsClientFactory wsClientFactory;
+
+  public LocalCallWebService(LocalWsClientFactory wsClientFactory) {
+    this.wsClientFactory = wsClientFactory;
+  }
+
+  @Override
+  public void define(Context context) {
+    NewController controller = context.createController("local_ws_call");
+    controller.createAction("protobuf_data").setHandler(new ProtobufHandler());
+    controller.createAction("json_data").setHandler(new JsonHandler());
+    controller.createAction("require_permission").setHandler(new RequirePermissionHandler());
+    controller.done();
+  }
+
+  private class ProtobufHandler implements RequestHandler {
+    @Override
+    public void handle(Request request, Response response) throws Exception {
+      WsClient client = wsClientFactory.newClient(request.getLocalConnector());
+
+      WsCe.TaskTypesWsResponse ceTaskTypes = client.ce().taskTypes();
+      response.stream().setStatus(ceTaskTypes.getTaskTypesCount() > 0 ? 200 : 500);
+    }
+  }
+
+  private class JsonHandler implements RequestHandler {
+    @Override
+    public void handle(Request request, Response response) throws Exception {
+      WsClient client = wsClientFactory.newClient(request.getLocalConnector());
+
+      WsResponse jsonResponse = client.wsConnector().call(new GetRequest("api/issues/search"));
+      boolean ok = jsonResponse.contentType().equals(MediaTypes.JSON)
+        && jsonResponse.isSuccessful()
+        && jsonResponse.content().contains("\"issues\":");
+      response.stream().setStatus(ok ? 200 : 500);
+    }
+  }
+
+  private class RequirePermissionHandler implements RequestHandler {
+    @Override
+    public void handle(Request request, Response response) throws Exception {
+      WsClient client = wsClientFactory.newClient(request.getLocalConnector());
+
+      WsResponse jsonResponse = client.wsConnector().call(new GetRequest("api/system/info"));
+
+      boolean ok = MediaTypes.JSON.equals(jsonResponse.contentType())
+        && jsonResponse.isSuccessful()
+        && jsonResponse.content().startsWith("{");
+      response.stream().setStatus(ok ? 200 : 500);
+    }
+  }
+}
diff --git a/it/it-plugins/ws-plugin/src/main/java/WsPlugin.java b/it/it-plugins/ws-plugin/src/main/java/WsPlugin.java
new file mode 100644 (file)
index 0000000..d907059
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+
+import java.util.Arrays;
+import java.util.List;
+import org.sonar.api.SonarPlugin;
+import org.sonarqube.ws.client.WsClientFactories;
+
+public class WsPlugin extends SonarPlugin {
+  public List getExtensions() {
+    return Arrays.asList(LocalCallWebService.class, WsClientFactories.getLocal());
+  }
+}
index ee4802b478da82699d77b81be71ec28047b2a688..bb75f2473f08da9aa590dd713d7fec5e42d96793 100644 (file)
@@ -41,6 +41,7 @@ import it.user.ForceAuthenticationTest;
 import it.user.LocalAuthenticationTest;
 import it.user.MyAccountPageTest;
 import it.user.OAuth2IdentityProviderTest;
+import it.ws.WsLocalCallTest;
 import org.junit.ClassRule;
 import org.junit.runner.RunWith;
 import org.junit.runners.Suite;
@@ -83,7 +84,8 @@ import static util.ItUtils.xooPlugin;
   // ui
   UiTest.class,
   // ui extensions
-  UiExtensionsTest.class
+  UiExtensionsTest.class,
+  WsLocalCallTest.class
 })
 public class Category4Suite {
 
@@ -104,5 +106,7 @@ public class Category4Suite {
     // Used in UiExtensionsTest
     .addPlugin(pluginArtifact("ui-extensions-plugin"))
 
+    // Used by WsLocalCallTest
+    .addPlugin(pluginArtifact("ws-plugin"))
     .build();
 }
index faf0f3bae290b5e2bbb1cf70f3f072cf885728fc..61df33b9eea12150fdbf652aefae7ea0735cd811 100644 (file)
@@ -37,8 +37,8 @@ import org.junit.experimental.categories.Category;
 import org.sonarqube.ws.WsUserTokens;
 import org.sonarqube.ws.client.GetRequest;
 import org.sonarqube.ws.client.HttpConnector;
-import org.sonarqube.ws.client.HttpWsClient;
 import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsClientFactories;
 import org.sonarqube.ws.client.WsResponse;
 import org.sonarqube.ws.client.permission.AddGroupWsRequest;
 import org.sonarqube.ws.client.permission.AddUserWsRequest;
@@ -109,7 +109,7 @@ public class LocalAuthenticationTest {
     userRule.createUser(login, name, null, password);
 
     // authenticate
-    WsClient wsClient = new HttpWsClient(new HttpConnector.Builder().url(ORCHESTRATOR.getServer().getUrl()).credentials(login, password).build());
+    WsClient wsClient = WsClientFactories.getDefault().newClient(HttpConnector.newBuilder().url(ORCHESTRATOR.getServer().getUrl()).credentials(login, password).build());
     WsResponse response = wsClient.wsConnector().call(new GetRequest("api/authentication/validate"));
     assertThat(response.content()).isEqualTo("{\"valid\":true}");
   }
@@ -120,7 +120,7 @@ public class LocalAuthenticationTest {
     WsUserTokens.GenerateWsResponse generateWsResponse = userTokensWsClient.generate(new GenerateWsRequest()
       .setLogin(LOGIN)
       .setName(tokenName));
-    WsClient wsClient = new HttpWsClient(new HttpConnector.Builder()
+    WsClient wsClient = WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
       .url(ORCHESTRATOR.getServer().getUrl())
       .token(generateWsResponse.getToken()).build());
 
@@ -196,7 +196,7 @@ public class LocalAuthenticationTest {
 
     // This check is failing because signup doesn't refresh the users ES index !
     // Will be fixed by SONAR-7308
-//    userRule.verifyUserExists("signuplogin", "SignUpName", null);
+    // userRule.verifyUserExists("signuplogin", "SignUpName", null);
   }
 
   @Test
@@ -208,14 +208,14 @@ public class LocalAuthenticationTest {
       "/user/LocalAuthenticationTest/redirect_to_original_url_after_direct_login.html",
       // SONAR-2009
       "/user/LocalAuthenticationTest/redirect_to_original_url_after_indirect_login.html"
-    ).build()).runOn(ORCHESTRATOR);
+      ).build()).runOn(ORCHESTRATOR);
 
     setServerProperty(ORCHESTRATOR, "sonar.forceAuthentication", "true");
 
     new SeleneseTest(Selenese.builder().setHtmlTestsInClasspath("force-authentication",
       // SONAR-3473
       "/user/LocalAuthenticationTest/force-authentication.html"
-    ).build()).runOn(ORCHESTRATOR);
+      ).build()).runOn(ORCHESTRATOR);
   }
 
   @Test
diff --git a/it/it-tests/src/test/java/it/ws/WsLocalCallTest.java b/it/it-tests/src/test/java/it/ws/WsLocalCallTest.java
new file mode 100644 (file)
index 0000000..3ec61f0
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 it.ws;
+
+import com.sonar.orchestrator.Orchestrator;
+import it.Category4Suite;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.HttpConnector;
+import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsClientFactories;
+import org.sonarqube.ws.client.WsResponse;
+import util.QaOnly;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests the ability for a web service to call another web services.
+ */
+@Category(QaOnly.class)
+public class WsLocalCallTest {
+
+  @ClassRule
+  public static final Orchestrator orchestrator = Category4Suite.ORCHESTRATOR;
+
+  @Test
+  public void gets_protobuf() {
+    WsResponse response = newAnonymousClient().wsConnector().call(new GetRequest("local_ws_call/protobuf_data"));
+    assertThat(response.isSuccessful()).isTrue();
+  }
+
+  @Test
+  public void gets_json() {
+    WsResponse response = newAnonymousClient().wsConnector().call(new GetRequest("local_ws_call/json_data"));
+    assertThat(response.isSuccessful()).isTrue();
+  }
+
+  @Test
+  public void propagates_authorization_rights() {
+    WsClient wsClient = WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
+      .url(orchestrator.getServer().getUrl())
+      .credentials("admin", "admin")
+      .build());
+    WsResponse response = wsClient.wsConnector().call(new GetRequest("local_ws_call/require_permission"));
+    assertThat(response.isSuccessful()).isTrue();
+  }
+
+  @Test
+  public void fails_if_requires_permissions() {
+    WsResponse response = newAnonymousClient().wsConnector().call(new GetRequest("local_ws_call/require_permission"));
+
+    // this is not the unauthorized code as plugin forces it to 500
+    assertThat(response.code()).isEqualTo(500);
+  }
+
+  private static WsClient newAnonymousClient() {
+    return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder().url(orchestrator.getServer().getUrl()).build());
+  }
+}
index b435e1244c0d40ab17eb6a153d2376deb590c3c0..21c8f9765ea1d8e7020962f7ff52f2140ebf6565 100644 (file)
@@ -49,8 +49,8 @@ import org.sonar.wsclient.issue.IssueQuery;
 import org.sonar.wsclient.services.PropertyDeleteQuery;
 import org.sonar.wsclient.services.PropertyUpdateQuery;
 import org.sonarqube.ws.client.HttpConnector;
-import org.sonarqube.ws.client.HttpWsClient;
 import org.sonarqube.ws.client.WsClient;
+import org.sonarqube.ws.client.WsClientFactories;
 
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.FluentIterable.from;
@@ -77,7 +77,7 @@ public class ItUtils {
 
   public static WsClient newAdminWsClient(Orchestrator orchestrator) {
     Server server = orchestrator.getServer();
-    return new HttpWsClient(new HttpConnector.Builder()
+    return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
       .url(server.getUrl())
       .credentials(ADMIN_LOGIN, ADMIN_PASSWORD)
       .build());
@@ -85,7 +85,7 @@ public class ItUtils {
 
   public static WsClient newWsClient(Orchestrator orchestrator) {
     Server server = orchestrator.getServer();
-    return new HttpWsClient(new HttpConnector.Builder()
+    return WsClientFactories.getDefault().newClient(HttpConnector.newBuilder()
       .url(server.getUrl())
       .build());
   }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ws/DefaultLocalResponse.java b/server/sonar-server/src/main/java/org/sonar/server/ws/DefaultLocalResponse.java
new file mode 100644 (file)
index 0000000..1b3d3f3
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.sonar.server.ws;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.Maps;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.server.ws.LocalConnector;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.api.utils.text.XmlWriter;
+import org.sonarqube.ws.MediaTypes;
+
+public class DefaultLocalResponse implements Response, LocalConnector.LocalResponse {
+
+  private final InMemoryStream stream = new InMemoryStream();
+  private final ByteArrayOutputStream output = new ByteArrayOutputStream();
+  private final Map<String, String> headers = Maps.newHashMap();
+
+  @Override
+  public int getStatus() {
+    return stream().status();
+  }
+
+  @Override
+  public String getMediaType() {
+    return stream().mediaType();
+  }
+
+  @Override
+  public byte[] getBytes() {
+    return output.toByteArray();
+  }
+
+  public class InMemoryStream implements Response.Stream {
+    private String mediaType;
+
+    private int status = 200;
+
+    @CheckForNull
+    public String mediaType() {
+      return mediaType;
+    }
+
+    public int status() {
+      return status;
+    }
+
+    @Override
+    public Response.Stream setMediaType(String s) {
+      this.mediaType = s;
+      return this;
+    }
+
+    @Override
+    public Response.Stream setStatus(int i) {
+      this.status = i;
+      return this;
+    }
+
+    @Override
+    public OutputStream output() {
+      return output;
+    }
+
+  }
+
+  @Override
+  public JsonWriter newJsonWriter() {
+    stream.setMediaType(MediaTypes.JSON);
+    return JsonWriter.of(new OutputStreamWriter(output, StandardCharsets.UTF_8));
+  }
+
+  @Override
+  public XmlWriter newXmlWriter() {
+    stream.setMediaType(MediaTypes.XML);
+    return XmlWriter.of(new OutputStreamWriter(output, StandardCharsets.UTF_8));
+  }
+
+  @Override
+  public InMemoryStream stream() {
+    return stream;
+  }
+
+  @Override
+  public Response noContent() {
+    stream().setStatus(HttpURLConnection.HTTP_NO_CONTENT);
+    IOUtils.closeQuietly(output);
+    return this;
+  }
+
+  public String outputAsString() {
+    return new String(output.toByteArray(), StandardCharsets.UTF_8);
+  }
+
+  @Override
+  public Response setHeader(String name, String value) {
+    headers.put(name, value);
+    return this;
+  }
+
+  @Override
+  public Collection<String> getHeaderNames() {
+    return headers.keySet();
+  }
+
+  @Override
+  public String getHeader(String name) {
+    return headers.get(name);
+  }
+
+  public byte[] getFlushedOutput() {
+    try {
+      output.flush();
+      return output.toByteArray();
+    } catch (IOException e) {
+      throw Throwables.propagate(e);
+    }
+  }
+}
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
new file mode 100644 (file)
index 0000000..412b97d
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.sonar.server.ws;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import org.sonar.api.server.ws.LocalConnector;
+import org.sonar.api.server.ws.internal.ValidatingRequest;
+
+public class LocalRequestAdapter extends ValidatingRequest {
+
+  private final LocalConnector.LocalRequest localRequest;
+
+  public LocalRequestAdapter(LocalConnector.LocalRequest localRequest) {
+    this.localRequest = localRequest;
+  }
+
+  @Override
+  protected String readParam(String key) {
+    return localRequest.getParam(key);
+  }
+
+  @Override
+  protected InputStream readInputStreamParam(String key) {
+    String value = readParam(key);
+    if (value == null) {
+      return null;
+    }
+    return new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8));
+  }
+
+  @Override
+  public boolean hasParam(String key) {
+    return localRequest.hasParam(key);
+  }
+
+  @Override
+  public String method() {
+    return localRequest.getMethod();
+  }
+
+  @Override
+  public String getMediaType() {
+    return localRequest.getMediaType();
+  }
+}
index 024065fd4221a87889819219e83627551756651d..f0a5dad038f4bbb29d418ca0be800a6707fd9052 100644 (file)
@@ -22,9 +22,13 @@ package org.sonar.server.ws;
 import java.io.OutputStreamWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
+import org.apache.commons.lang.StringUtils;
 import org.picocontainer.Startable;
 import org.sonar.api.i18n.I18n;
 import org.sonar.api.server.ServerSide;
+import org.sonar.api.server.ws.LocalConnector;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.server.ws.internal.ValidatingRequest;
 import org.sonar.api.utils.log.Loggers;
@@ -43,7 +47,7 @@ import static org.sonar.server.ws.RequestVerifier.verifyRequest;
  * @since 4.2
  */
 @ServerSide
-public class WebServiceEngine implements Startable {
+public class WebServiceEngine implements LocalConnector, Startable {
 
   private final WebService.Context context;
   private final I18n i18n;
@@ -76,11 +80,22 @@ public class WebServiceEngine implements Startable {
     return context.controllers();
   }
 
-  public void execute(ValidatingRequest request, ServletResponse response,
-    String controllerPath, String actionKey) {
+  @Override
+  public LocalResponse call(LocalRequest request) {
+    String controller = StringUtils.substringBeforeLast(request.getPath(), "/");
+    String action = StringUtils.substringAfterLast(request.getPath(), "/");
+    DefaultLocalResponse localResponse = new DefaultLocalResponse();
+    execute(new LocalRequestAdapter(request), localResponse, controller, action);
+    return localResponse;
+  }
+
+  public void execute(Request request, Response response, String controllerPath, String actionKey) {
     try {
       WebService.Action action = getAction(controllerPath, actionKey);
-      request.setAction(action);
+      if (request instanceof ValidatingRequest) {
+        ((ValidatingRequest) request).setAction(action);
+        ((ValidatingRequest) request).setLocalConnector(this);
+      }
       verifyRequest(action, request);
       action.handler().handle(request, response);
 
@@ -112,9 +127,11 @@ public class WebServiceEngine implements Startable {
     return action;
   }
 
-  private void sendErrors(ServletResponse response, int status, Errors errors) {
-    ServletResponse.ServletStream stream = response.stream();
-    stream.reset();
+  private void sendErrors(Response response, int status, Errors errors) {
+    Response.Stream stream = response.stream();
+    if (stream instanceof ServletResponse.ServletStream) {
+      ((ServletResponse.ServletStream) stream).reset();
+    }
     stream.setStatus(status);
     stream.setMediaType(MediaTypes.JSON);
     JsonWriter json = JsonWriter.of(new OutputStreamWriter(stream.output(), StandardCharsets.UTF_8));
diff --git a/server/sonar-web/npm.tar.gz b/server/sonar-web/npm.tar.gz
new file mode 100644 (file)
index 0000000..fd6d660
Binary files /dev/null and b/server/sonar-web/npm.tar.gz differ
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
new file mode 100644 (file)
index 0000000..cf4075d
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.sonar.api.server.ws;
+
+import com.google.common.annotations.Beta;
+import java.util.Collection;
+import javax.annotation.CheckForNull;
+
+/**
+ * This class allows a web service to call another web service through the sonar-ws library.
+ * The call is in-process, synchronous and does not involve the HTTP stack.
+ * <p>
+ * Example of a web service that uses sonar-ws 5.5 to get some issues:
+ * <pre>
+ * import org.sonar.api.server.ws.RequestHandler;
+ * import org.sonarqube.ws.client.WsClientFactories;
+ *
+ * public class MyRequestHandler implements RequestHandler {
+ *   {@literal @}Override
+ *   public void handle(Request request, Response response) {
+ *     WsClient wsClient = WsClientFactories.getLocal().newClient(request.getLocalConnector());
+ *     SearchWsResponse issues = wsClient.issues().search(new SearchWsRequest());
+ *     // ...
+ *   }
+ * }
+ * </pre>
+ * </p>
+ * @since 5.5
+ */
+@Beta
+public interface LocalConnector {
+
+  LocalResponse call(LocalRequest request);
+
+  interface LocalRequest {
+    /**
+     * URL path, which is the concatenation of controller path and action key, for example "api/issues/search"
+     * @see org.sonar.api.server.ws.WebService.Controller#path
+     * @see org.sonar.api.server.ws.WebService.Action#key
+     */
+    String getPath();
+
+    /**
+     * @see Request#getMediaType()
+     */
+    String getMediaType();
+
+    /**
+     * HTTP method. Possible values are "GET" and "POST"
+     * @see Request#method()
+     */
+    String getMethod();
+
+    /**
+     * @see Request#hasParam(String)
+     */
+    boolean hasParam(String key);
+
+    /**
+     * @see Request#param(String)
+     */
+    @CheckForNull
+    String getParam(String key);
+  }
+
+  interface LocalResponse {
+    /**
+     * @see org.sonar.api.server.ws.Response.Stream#setStatus(int)
+     */
+    int getStatus();
+
+    /**
+     * @see org.sonar.api.server.ws.Response.Stream#setMediaType(String)
+     */
+    String getMediaType();
+
+    /**
+     * Response body
+     */
+    byte[] getBytes();
+
+    /**
+     * HTTP headers
+     * @see Response#setHeader(String, String)
+     */
+    Collection<String> getHeaderNames();
+
+    /**
+     * @see Response#setHeader(String, String)
+     * @return
+     */
+    @CheckForNull
+    String getHeader(String name);
+  }
+
+}
index 52d6c06003cd930bf5746612f75c14cda80905c8..fdc54cee2b0bb49beec194c1efd37d35e0ee9d5b 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.api.server.ws;
 
+import com.google.common.annotations.Beta;
 import com.google.common.base.Splitter;
 import com.google.common.collect.Lists;
 import java.io.InputStream;
@@ -234,4 +235,12 @@ public abstract class Request {
     }
     throw new IllegalArgumentException(String.format("Property %s is not a boolean value: %s", key, value));
   }
+
+  /**
+   * Used by the sonar-ws library to allow a web service to call another web service.
+   * @see LocalConnector
+   * @since 5.5
+   */
+  @Beta
+  public abstract LocalConnector getLocalConnector();
 }
index a100a9399e032ada8017e7e945fedc9ad12aac05..8b7db8953d8783559dad737391400443ea56a3c8 100644 (file)
@@ -23,7 +23,9 @@ import org.sonar.api.ExtensionPoint;
 import org.sonar.api.server.ServerSide;
 
 /**
+ * Extension point to execute a HTTP request.
  * @since 4.2
+ * @see WebService
  */
 @ServerSide
 @ExtensionPoint
index fcd239bd16b1bf331867a6a59811209c80fd8511..72aabd1d30040da787a8a7a14f8a8c94d1dab229 100644 (file)
@@ -34,6 +34,11 @@ public interface Response {
 
   interface Stream {
     Stream setMediaType(String s);
+
+    /**
+     * HTTP status code. See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes.
+     * By default value is set to 200.
+     */
     Stream setStatus(int httpStatus);
     OutputStream output();
   }
index 260df6f97220e463819d3a3862f07615bae6d9a8..4dedab64d5a346dd66fff63f13a4e8172de01990 100644 (file)
@@ -87,28 +87,6 @@ import static java.lang.String.format;
  *   }
  * }
  * </pre>
- * <h3>How to test</h3>
- * <pre>
- * public class HelloWsTest {
- *   WebService ws = new HelloWs();
- *
- *   {@literal @}Test
- *   public void should_define_ws() throws Exception {
- *     // WsTester is available in the Maven artifact org.codehaus.sonar:sonar-testing-harness
- *     WsTester tester = new WsTester(ws);
- *     WebService.Controller controller = tester.controller("api/hello");
- *     assertThat(controller).isNotNull();
- *     assertThat(controller.path()).isEqualTo("api/hello");
- *     assertThat(controller.description()).isNotEmpty();
- *     assertThat(controller.actions()).hasSize(1);
- *
- *     WebService.Action show = controller.action("show");
- *     assertThat(show).isNotNull();
- *     assertThat(show.key()).isEqualTo("show");
- *     assertThat(index.handler()).isNotNull();
- *   }
- * }
- * </pre>
  *
  * @since 4.2
  */
index adcf432f8fa4a1460c367952bf293db249cba6cb..f08816d5bd7d8cbf1f7231bff81d9039c8d5676c 100644 (file)
@@ -24,6 +24,7 @@ import java.io.InputStream;
 import java.util.Map;
 import javax.annotation.Nullable;
 import org.apache.commons.io.IOUtils;
+import org.sonar.api.server.ws.LocalConnector;
 import org.sonar.api.server.ws.Request;
 
 import static com.google.common.base.Preconditions.checkNotNull;
@@ -76,4 +77,12 @@ public class SimpleGetRequest extends Request {
     return this;
   }
 
+  public Map<String, String> getParams() {
+    return params;
+  }
+
+  @Override
+  public LocalConnector getLocalConnector() {
+    throw new UnsupportedOperationException();
+  }
 }
index 436d8b3303283433d9fe60f6ab5b5a7b868dccff..f38a1bed2e2c241dfd82048e342a1f93523f5540 100644 (file)
@@ -29,16 +29,20 @@ import java.util.Set;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.apache.commons.lang.StringUtils;
+import org.sonar.api.server.ws.LocalConnector;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.log.Loggers;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
 /**
  * @since 4.2
  */
 public abstract class ValidatingRequest extends Request {
 
   private WebService.Action action;
+  private LocalConnector localConnector;
 
   public void setAction(WebService.Action action) {
     this.action = action;
@@ -48,6 +52,16 @@ public abstract class ValidatingRequest extends Request {
     return action;
   }
 
+  @Override
+  public LocalConnector getLocalConnector() {
+    checkNotNull(localConnector, "Local connector has not been set");
+    return localConnector;
+  }
+
+  public void setLocalConnector(LocalConnector lc) {
+    this.localConnector = lc;
+  }
+
   @Override
   @CheckForNull
   public String param(String key) {
index d33baf821e5a718fca281c65d0dd01fa751956fb..5cd52b3419c089cdb84211fe4c6bd71c0959347b 100644 (file)
@@ -24,7 +24,7 @@ import org.sonar.api.CoreProperties;
 import org.sonar.api.batch.BatchSide;
 import org.sonar.batch.bootstrapper.EnvironmentInformation;
 import org.sonarqube.ws.client.HttpConnector;
-import org.sonarqube.ws.client.HttpWsClient;
+import org.sonarqube.ws.client.WsClientFactories;
 
 import static java.lang.Integer.parseInt;
 import static java.lang.String.valueOf;
@@ -42,20 +42,20 @@ public class BatchWsClientProvider extends ProviderAdapter {
   public synchronized BatchWsClient provide(final GlobalProperties settings, final EnvironmentInformation env) {
     if (wsClient == null) {
       String url = defaultIfBlank(settings.property("sonar.host.url"), CoreProperties.SERVER_BASE_URL_DEFAULT_VALUE);
-      HttpConnector.Builder builder = new HttpConnector.Builder();
+      HttpConnector.Builder connectorBuilder = HttpConnector.newBuilder();
 
       // TODO proxy
 
       String timeoutSec = defaultIfBlank(settings.property(READ_TIMEOUT_SEC_PROPERTY), valueOf(DEFAULT_READ_TIMEOUT_SEC));
       String login = defaultIfBlank(settings.property(CoreProperties.LOGIN), null);
-      builder
+      connectorBuilder
         .readTimeoutMilliseconds(parseInt(timeoutSec) * 1_000)
         .connectTimeoutMilliseconds(CONNECT_TIMEOUT_MS)
         .userAgent(env.toString())
         .url(url)
         .credentials(login, settings.property(CoreProperties.PASSWORD));
 
-      wsClient = new BatchWsClient(new HttpWsClient(builder.build()), login != null);
+      wsClient = new BatchWsClient(WsClientFactories.getDefault().newClient(connectorBuilder.build()), login != null);
     }
     return wsClient;
   }
index 9b5cc87d130f4c44687ec49cad2e8177a103e000..e6a19ead6ba2cd16af6ac934d55c29a229316deb 100644 (file)
       <groupId>commons-io</groupId>
       <artifactId>commons-io</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.sonarsource.sonarqube</groupId>
+      <artifactId>sonar-plugin-api</artifactId>
+      <scope>provided</scope>
+      <optional>true</optional>
+    </dependency>
 
     <!-- Tests -->
     <dependency>
index 83c7163e38480de8ac0232e1da78e285270880e9..2cfcd7448a5e4c679fe1cc61c1e263c477b6bec5 100644 (file)
@@ -19,6 +19,8 @@
  */
 package org.sonarqube.ws.client;
 
+import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+
 abstract class BaseResponse implements WsResponse {
 
   @Override
@@ -34,4 +36,8 @@ abstract class BaseResponse implements WsResponse {
     return this;
   }
 
+  @Override
+  public boolean hasContent() {
+    return code() != HTTP_NO_CONTENT;
+  }
 }
index aa5abe9b0a8fc3bee0e5208b70c62db90e1e006b..8d65ec983bb8be75dbade92fe5b6f308c47bdafa 100644 (file)
@@ -26,6 +26,7 @@ import java.io.InputStream;
 import java.util.List;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
+import org.apache.commons.io.IOUtils;
 import org.sonarqube.ws.MediaTypes;
 
 import static com.google.common.base.Preconditions.checkArgument;
@@ -56,8 +57,9 @@ public abstract class BaseService {
 
   public <T extends Message> T convert(WsResponse response, Parser<T> parser) {
     try (InputStream byteStream = response.contentStream()) {
+      byte[] bytes = IOUtils.toByteArray(byteStream);
       // HTTP header "Content-Type" is not verified. It may be different than protobuf.
-      return parser.parseFrom(byteStream);
+      return parser.parseFrom(bytes);
     } catch (Exception e) {
       throw new IllegalStateException("Fail to parse protobuf response of " + response.requestUrl(), e);
     }
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java
new file mode 100644 (file)
index 0000000..1c0d827
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 org.sonarqube.ws.client.ce.CeService;
+import org.sonarqube.ws.client.component.ComponentsService;
+import org.sonarqube.ws.client.issue.IssuesService;
+import org.sonarqube.ws.client.measure.MeasuresService;
+import org.sonarqube.ws.client.permission.PermissionsService;
+import org.sonarqube.ws.client.qualitygate.QualityGatesService;
+import org.sonarqube.ws.client.qualityprofile.QualityProfilesService;
+import org.sonarqube.ws.client.rule.RulesService;
+import org.sonarqube.ws.client.system.SystemService;
+import org.sonarqube.ws.client.usertoken.UserTokensService;
+
+/**
+ * This class is not public anymore since version 5.5. It is
+ * created by {@link WsClientFactory}
+ *
+ * @since 5.3
+ */
+class DefaultWsClient implements WsClient {
+
+  private final WsConnector wsConnector;
+  private final PermissionsService permissionsService;
+  private final ComponentsService componentsService;
+  private final QualityProfilesService qualityProfilesService;
+  private final IssuesService issuesService;
+  private final UserTokensService userTokensService;
+  private final QualityGatesService qualityGatesService;
+  private final MeasuresService measuresService;
+  private final SystemService systemService;
+  private final CeService ceService;
+  private final RulesService rulesService;
+
+  DefaultWsClient(WsConnector wsConnector) {
+    this.wsConnector = wsConnector;
+    this.permissionsService = new PermissionsService(wsConnector);
+    this.componentsService = new ComponentsService(wsConnector);
+    this.qualityProfilesService = new QualityProfilesService(wsConnector);
+    this.issuesService = new IssuesService(wsConnector);
+    this.userTokensService = new UserTokensService(wsConnector);
+    this.qualityGatesService = new QualityGatesService(wsConnector);
+    this.measuresService = new MeasuresService(wsConnector);
+    this.systemService = new SystemService(wsConnector);
+    this.ceService = new CeService(wsConnector);
+    this.rulesService = new RulesService(wsConnector);
+  }
+
+  @Override
+  public WsConnector wsConnector() {
+    return wsConnector;
+  }
+
+  @Override
+  public PermissionsService permissions() {
+    return this.permissionsService;
+  }
+
+  @Override
+  public ComponentsService components() {
+    return componentsService;
+  }
+
+  @Override
+  public QualityProfilesService qualityProfiles() {
+    return qualityProfilesService;
+  }
+
+  @Override
+  public IssuesService issues() {
+    return issuesService;
+  }
+
+  @Override
+  public UserTokensService userTokens() {
+    return userTokensService;
+  }
+
+  @Override
+  public QualityGatesService qualityGates() {
+    return qualityGatesService;
+  }
+
+  @Override
+  public MeasuresService measures() {
+    return measuresService;
+  }
+
+  @Override
+  public SystemService system() {
+    return systemService;
+  }
+
+  @Override
+  public CeService ce() {
+    return ceService;
+  }
+
+  @Override
+  public RulesService rules() {
+    return rulesService;
+  }
+}
index 4c0917f751c5f5dcbf0254ad8ab8fec7ae67a6e6..f66bcd6330cf15174b4ffb7a098886dfabb0a117 100644 (file)
@@ -202,16 +202,23 @@ public class HttpConnector implements WsConnector {
     return okHttpRequestBuilder;
   }
 
-  private HttpResponse doCall(Request okRequest) {
+  private OkHttpResponse doCall(Request okRequest) {
     Call call = okHttpClient.newCall(okRequest);
     try {
       Response okResponse = call.execute();
-      return new HttpResponse(okResponse);
+      return new OkHttpResponse(okResponse);
     } catch (IOException e) {
       throw new IllegalStateException("Fail to request " + okRequest.urlString(), e);
     }
   }
 
+  /**
+   * @since 5.5
+   */
+  public static Builder newBuilder() {
+    return new Builder();
+  }
+
   public static class Builder {
     private String url;
     private String userAgent;
@@ -223,6 +230,13 @@ public class HttpConnector implements WsConnector {
     private int connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLISECONDS;
     private int readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLISECONDS;
 
+    /**
+     * Private since 5.5.
+     * @see HttpConnector#newBuilder()
+     */
+    private Builder() {
+    }
+
     /**
      * Optional User  Agent
      */
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpResponse.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpResponse.java
deleted file mode 100644 (file)
index b770424..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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 com.squareup.okhttp.Response;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Reader;
-
-import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
-
-class HttpResponse extends BaseResponse {
-
-  private final Response okResponse;
-
-  HttpResponse(Response okResponse) {
-    this.okResponse = okResponse;
-  }
-
-  @Override
-  public int code() {
-    return okResponse.code();
-  }
-
-  @Override
-  public String requestUrl() {
-    return okResponse.request().urlString();
-  }
-
-  @Override
-  public boolean hasContent() {
-    return okResponse.code() != HTTP_NO_CONTENT;
-  }
-
-  @Override
-  public String contentType() {
-    return okResponse.header("Content-Type");
-  }
-
-  /**
-   * Get stream of bytes
-   */
-  @Override
-  public InputStream contentStream() {
-    try {
-      return okResponse.body().byteStream();
-    } catch (IOException e) {
-      throw fail(e);
-    }
-  }
-
-  /**
-   * Get stream of characters, decoded with the charset
-   * of the Content-Type header. If that header is either absent or lacks a
-   * charset, this will attempt to decode the response body as UTF-8.
-   */
-  @Override
-  public Reader contentReader() {
-    try {
-      return okResponse.body().charStream();
-    } catch (IOException e) {
-      throw fail(e);
-    }
-  }
-
-  @Override
-  public String content() {
-    try {
-      return okResponse.body().string();
-    } catch (IOException e) {
-      throw fail(e);
-    }
-  }
-
-  private RuntimeException fail(Exception e) {
-    throw new IllegalStateException("Fail to read response of " + requestUrl(), e);
-  }
-}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpWsClient.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpWsClient.java
deleted file mode 100644 (file)
index 5ceb0b6..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact 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 org.sonarqube.ws.client.ce.CeService;
-import org.sonarqube.ws.client.component.ComponentsService;
-import org.sonarqube.ws.client.issue.IssuesService;
-import org.sonarqube.ws.client.measure.MeasuresService;
-import org.sonarqube.ws.client.permission.PermissionsService;
-import org.sonarqube.ws.client.qualitygate.QualityGatesService;
-import org.sonarqube.ws.client.qualityprofile.QualityProfilesService;
-import org.sonarqube.ws.client.rule.RulesService;
-import org.sonarqube.ws.client.system.SystemService;
-import org.sonarqube.ws.client.usertoken.UserTokensService;
-
-/**
- * Entry point of the Java Client for SonarQube Web Services
- *
- * @since 5.3
- */
-public class HttpWsClient implements WsClient {
-
-  private final WsConnector wsConnector;
-  private final PermissionsService permissionsService;
-  private final ComponentsService componentsService;
-  private final QualityProfilesService qualityProfilesService;
-  private final IssuesService issuesService;
-  private final UserTokensService userTokensService;
-  private final QualityGatesService qualityGatesService;
-  private final MeasuresService measuresService;
-  private final SystemService systemService;
-  private final CeService ceService;
-  private final RulesService rulesService;
-
-  public HttpWsClient(WsConnector wsConnector) {
-    this.wsConnector = wsConnector;
-    this.permissionsService = new PermissionsService(wsConnector);
-    this.componentsService = new ComponentsService(wsConnector);
-    this.qualityProfilesService = new QualityProfilesService(wsConnector);
-    this.issuesService = new IssuesService(wsConnector);
-    this.userTokensService = new UserTokensService(wsConnector);
-    this.qualityGatesService = new QualityGatesService(wsConnector);
-    this.measuresService = new MeasuresService(wsConnector);
-    this.systemService = new SystemService(wsConnector);
-    this.ceService = new CeService(wsConnector);
-    this.rulesService = new RulesService(wsConnector);
-  }
-
-  @Override
-  public WsConnector wsConnector() {
-    return wsConnector;
-  }
-
-  @Override
-  public PermissionsService permissions() {
-    return this.permissionsService;
-  }
-
-  @Override
-  public ComponentsService components() {
-    return componentsService;
-  }
-
-  @Override
-  public QualityProfilesService qualityProfiles() {
-    return qualityProfilesService;
-  }
-
-  @Override
-  public IssuesService issues() {
-    return issuesService;
-  }
-
-  @Override
-  public UserTokensService userTokens() {
-    return userTokensService;
-  }
-
-  @Override
-  public QualityGatesService qualityGates() {
-    return qualityGatesService;
-  }
-
-  @Override
-  public MeasuresService measures() {
-    return measuresService;
-  }
-
-  @Override
-  public SystemService system() {
-    return systemService;
-  }
-
-  @Override
-  public CeService ce() {
-    return ceService;
-  }
-
-  @Override
-  public RulesService rules() {
-    return rulesService;
-  }
-}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/LocalWsClientFactory.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/LocalWsClientFactory.java
new file mode 100644 (file)
index 0000000..227ea09
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 org.sonar.api.server.ServerSide;
+import org.sonar.api.server.ws.LocalConnector;
+
+@ServerSide
+public interface LocalWsClientFactory extends WsClientFactory {
+
+  WsClient newClient(LocalConnector localConnector);
+
+}
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
new file mode 100644 (file)
index 0000000..3b7af43
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 com.google.common.annotations.VisibleForTesting;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import org.sonar.api.server.ws.LocalConnector;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+class LocalWsConnector implements WsConnector {
+
+  private final LocalConnector localConnector;
+
+  LocalWsConnector(LocalConnector localConnector) {
+    this.localConnector = localConnector;
+  }
+
+  @VisibleForTesting
+  LocalConnector getLocalConnector() {
+    return localConnector;
+  }
+
+  @Override
+  public String baseUrl() {
+    return "/";
+  }
+
+  @Override
+  public WsResponse call(WsRequest wsRequest) {
+    DefaultLocalRequest localRequest = new DefaultLocalRequest(wsRequest);
+    LocalConnector.LocalResponse localResponse = localConnector.call(localRequest);
+    return new ByteArrayResponse(wsRequest.getPath(), localResponse);
+  }
+
+  private static class DefaultLocalRequest implements LocalConnector.LocalRequest {
+    private final WsRequest wsRequest;
+
+    public DefaultLocalRequest(WsRequest wsRequest) {
+      this.wsRequest = wsRequest;
+    }
+
+    @Override
+    public String getPath() {
+      return wsRequest.getPath();
+    }
+
+    @Override
+    public String getMediaType() {
+      return wsRequest.getMediaType();
+    }
+
+    @Override
+    public String getMethod() {
+      return wsRequest.getMethod().name();
+    }
+
+    @Override
+    public boolean hasParam(String key) {
+      return wsRequest.getParams().containsKey(key);
+    }
+
+    @Override
+    public String getParam(String key) {
+      return wsRequest.getParams().get(key);
+    }
+  }
+
+  private static class ByteArrayResponse extends BaseResponse {
+    private final String path;
+    private final byte[] bytes;
+    private final String contentType;
+    private final int code;
+
+    ByteArrayResponse(String path, LocalConnector.LocalResponse localResponse) {
+      this.path = path;
+      this.bytes = localResponse.getBytes();
+      this.contentType = localResponse.getMediaType();
+      this.code = localResponse.getStatus();
+    }
+
+    @Override
+    public String requestUrl() {
+      return path;
+    }
+
+    @Override
+    public int code() {
+      return code;
+    }
+
+    @Override
+    public String contentType() {
+      return contentType;
+    }
+
+    @Override
+    public InputStream contentStream() {
+      return new ByteArrayInputStream(bytes);
+    }
+
+    @Override
+    public Reader contentReader() {
+      return new InputStreamReader(new ByteArrayInputStream(bytes), UTF_8);
+    }
+
+    @Override
+    public String content() {
+      return new String(bytes, UTF_8);
+    }
+  }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/OkHttpResponse.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/OkHttpResponse.java
new file mode 100644 (file)
index 0000000..30217f1
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 com.squareup.okhttp.Response;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
+class OkHttpResponse extends BaseResponse {
+
+  private final Response okResponse;
+
+  OkHttpResponse(Response okResponse) {
+    this.okResponse = okResponse;
+  }
+
+  @Override
+  public int code() {
+    return okResponse.code();
+  }
+
+  @Override
+  public String requestUrl() {
+    return okResponse.request().urlString();
+  }
+
+  @Override
+  public String contentType() {
+    return okResponse.header("Content-Type");
+  }
+
+  /**
+   * Get stream of bytes
+   */
+  @Override
+  public InputStream contentStream() {
+    try {
+      return okResponse.body().byteStream();
+    } catch (IOException e) {
+      throw fail(e);
+    }
+  }
+
+  /**
+   * Get stream of characters, decoded with the charset
+   * of the Content-Type header. If that header is either absent or lacks a
+   * charset, this will attempt to decode the response body as UTF-8.
+   */
+  @Override
+  public Reader contentReader() {
+    try {
+      return okResponse.body().charStream();
+    } catch (IOException e) {
+      throw fail(e);
+    }
+  }
+
+  @Override
+  public String content() {
+    try {
+      return okResponse.body().string();
+    } catch (IOException e) {
+      throw fail(e);
+    }
+  }
+
+  private RuntimeException fail(Exception e) {
+    throw new IllegalStateException("Fail to read response of " + requestUrl(), e);
+  }
+}
index 522e6054a464f805467fc0f8964da2645237dc28..fc79d58e69598f8c1adb02e33f442f20eee661c2 100644 (file)
@@ -31,6 +31,21 @@ import org.sonarqube.ws.client.system.SystemService;
 import org.sonarqube.ws.client.usertoken.UserTokensService;
 
 /**
+ * Allows to request the web services of SonarQube server. Instance is provided by
+ * {@link WsClientFactory}.
+ *
+ * <p>
+ * Usage:
+ * <pre>
+ *   HttpConnector httpConnector = HttpConnector.newBuilder()
+ *     .url("http://localhost:9000")
+ *     .credentials("admin", "admin")
+ *     .build();
+ *   WsClient wsClient = WsClientFactories.getDefault().newClient(httpConnector);
+ *   wsClient.issues().search(issueRequest);
+ * </pre>
+ * </p>
+ *
  * @since 5.3
  */
 public interface WsClient {
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClientFactories.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClientFactories.java
new file mode 100644 (file)
index 0000000..1f0898f
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 org.sonar.api.server.ws.LocalConnector;
+
+/**
+ * All provided implementations of {@link WsClientFactory}.
+ */
+public class WsClientFactories {
+
+  private WsClientFactories() {
+    // prevent instantiation
+  }
+
+  /**
+   * Factory to be used when connecting to a remote SonarQube web server.
+   */
+  public static WsClientFactory getDefault() {
+    return DefaultWsClientFactory.INSTANCE;
+  }
+
+  /**
+   * Factory that allows a SonarQube web service to interact
+   * with other web services, without using the HTTP stack.
+   * @see org.sonar.api.server.ws.LocalConnector
+   */
+  public static LocalWsClientFactory getLocal() {
+    return DefaultLocalWsClientFactory.INSTANCE;
+  }
+
+  private enum DefaultWsClientFactory implements WsClientFactory {
+    INSTANCE;
+
+    @Override
+    public WsClient newClient(WsConnector connector) {
+      return new DefaultWsClient(connector);
+    }
+  }
+
+  private enum DefaultLocalWsClientFactory implements LocalWsClientFactory {
+    INSTANCE;
+
+    @Override
+    public WsClient newClient(WsConnector connector) {
+      return DefaultWsClientFactory.INSTANCE.newClient(connector);
+    }
+
+    @Override
+    public WsClient newClient(LocalConnector localConnector) {
+      return new DefaultWsClient(new LocalWsConnector(localConnector));
+    }
+  }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClientFactory.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClientFactory.java
new file mode 100644 (file)
index 0000000..d7a89c2
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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;
+
+/**
+ * Creates {@link WsClient}. Implementations are provided by {@link WsClientFactories}.
+ *
+ * @since 5.5
+ */
+public interface WsClientFactory {
+
+  WsClient newClient(WsConnector connector);
+
+}
index a18407bd28bce8f96294da1a10e209ef1bc6e2b0..767978108a7826ade52379a515b78355535cfe02 100644 (file)
@@ -60,7 +60,7 @@ public class HttpConnectorTest {
   @Test
   public void test_default_settings() throws Exception {
     answerHelloWorld();
-    HttpConnector underTest = new HttpConnector.Builder().url(serverUrl).build();
+    HttpConnector underTest = HttpConnector.newBuilder().url(serverUrl).build();
     assertThat(underTest.baseUrl()).isEqualTo(serverUrl);
     GetRequest request = new GetRequest("api/issues/search").setMediaType(MediaTypes.PROTOBUF);
     WsResponse response = underTest.call(request);
@@ -87,7 +87,7 @@ public class HttpConnectorTest {
   @Test
   public void use_basic_authentication() throws Exception {
     answerHelloWorld();
-    HttpConnector underTest = new HttpConnector.Builder()
+    HttpConnector underTest = HttpConnector.newBuilder()
       .url(serverUrl)
       .credentials("theLogin", "thePassword")
       .build();
@@ -102,7 +102,7 @@ public class HttpConnectorTest {
   @Test
   public void use_basic_authentication_with_null_password() throws Exception {
     answerHelloWorld();
-    HttpConnector underTest = new HttpConnector.Builder()
+    HttpConnector underTest = HttpConnector.newBuilder()
       .url(serverUrl)
       .credentials("theLogin", null)
       .build();
@@ -121,7 +121,7 @@ public class HttpConnectorTest {
   @Test
   public void use_access_token() throws Exception {
     answerHelloWorld();
-    HttpConnector underTest = new HttpConnector.Builder()
+    HttpConnector underTest = HttpConnector.newBuilder()
       .url(serverUrl)
       .token("theToken")
       .build();
@@ -136,7 +136,7 @@ public class HttpConnectorTest {
   @Test
   public void use_proxy_authentication() throws Exception {
     answerHelloWorld();
-    HttpConnector underTest = new HttpConnector.Builder()
+    HttpConnector underTest = HttpConnector.newBuilder()
       .url(serverUrl)
       .proxyCredentials("theProxyLogin", "theProxyPassword")
       .build();
@@ -150,7 +150,7 @@ public class HttpConnectorTest {
 
   @Test
   public void override_timeouts() {
-    HttpConnector underTest = new HttpConnector.Builder()
+    HttpConnector underTest = HttpConnector.newBuilder()
       .url(serverUrl)
       .readTimeoutMilliseconds(42)
       .connectTimeoutMilliseconds(74)
@@ -163,7 +163,7 @@ public class HttpConnectorTest {
   @Test
   public void send_user_agent() throws Exception {
     answerHelloWorld();
-    HttpConnector underTest = new HttpConnector.Builder()
+    HttpConnector underTest = HttpConnector.newBuilder()
       .url(serverUrl)
       .userAgent("Maven Plugin/2.3")
       .build();
@@ -176,7 +176,7 @@ public class HttpConnectorTest {
 
   @Test
   public void fail_if_unknown_implementation_of_request() {
-    HttpConnector underTest = new HttpConnector.Builder().url(serverUrl).build();
+    HttpConnector underTest = HttpConnector.newBuilder().url(serverUrl).build();
     try {
       underTest.call(mock(WsRequest.class));
       fail();
@@ -192,7 +192,7 @@ public class HttpConnectorTest {
       .setParam("severity", "MAJOR")
       .setMediaType(MediaTypes.PROTOBUF);
 
-    HttpConnector underTest = new HttpConnector.Builder().url(serverUrl).build();
+    HttpConnector underTest = HttpConnector.newBuilder().url(serverUrl).build();
     WsResponse response = underTest.call(request);
 
     // verify response
@@ -215,7 +215,7 @@ public class HttpConnectorTest {
       .setPart("report", new PostRequest.Part(MediaTypes.TXT, reportFile))
       .setMediaType(MediaTypes.PROTOBUF);
 
-    HttpConnector underTest = new HttpConnector.Builder().url(serverUrl).build();
+    HttpConnector underTest = HttpConnector.newBuilder().url(serverUrl).build();
     WsResponse response = underTest.call(request);
 
     assertThat(response.hasContent()).isTrue();
@@ -233,7 +233,7 @@ public class HttpConnectorTest {
   public void http_error() throws Exception {
     server.enqueue(new MockResponse().setResponseCode(404));
     PostRequest request = new PostRequest("api/issues/search");
-    HttpConnector underTest = new HttpConnector.Builder().url(serverUrl).build();
+    HttpConnector underTest = HttpConnector.newBuilder().url(serverUrl).build();
 
     WsResponse wsResponse = underTest.call(request);
     assertThat(wsResponse.code()).isEqualTo(404);
@@ -242,7 +242,7 @@ public class HttpConnectorTest {
   @Test
   public void support_base_url_ending_with_slash() throws Exception {
     assertThat(serverUrl).endsWith("/");
-    HttpConnector underTest = new HttpConnector.Builder().url(StringUtils.removeEnd(serverUrl, "/")).build();
+    HttpConnector underTest = HttpConnector.newBuilder().url(StringUtils.removeEnd(serverUrl, "/")).build();
     GetRequest request = new GetRequest("api/issues/search");
 
     answerHelloWorld();
@@ -255,7 +255,7 @@ public class HttpConnectorTest {
   public void support_base_url_with_context() {
     // just to be sure
     assertThat(serverUrl).endsWith("/");
-    HttpConnector underTest = new HttpConnector.Builder().url(serverUrl + "sonar").build();
+    HttpConnector underTest = HttpConnector.newBuilder().url(serverUrl + "sonar").build();
 
     GetRequest request = new GetRequest("api/issues/search");
     answerHelloWorld();
@@ -269,7 +269,7 @@ public class HttpConnectorTest {
   @Test
   public void support_tls_1_2_on_java7() {
     when(javaVersion.isJava7()).thenReturn(true);
-    HttpConnector underTest = new HttpConnector.Builder().url(serverUrl).build(javaVersion);
+    HttpConnector underTest = HttpConnector.newBuilder().url(serverUrl).build(javaVersion);
 
     assertTlsAndClearTextSpecifications(underTest);
     // enable TLS 1.0, 1.1 and 1.2
@@ -279,7 +279,7 @@ public class HttpConnectorTest {
   @Test
   public void support_tls_versions_of_java8() {
     when(javaVersion.isJava7()).thenReturn(false);
-    HttpConnector underTest = new HttpConnector.Builder().url(serverUrl).build(javaVersion);
+    HttpConnector underTest = HttpConnector.newBuilder().url(serverUrl).build(javaVersion);
 
     assertTlsAndClearTextSpecifications(underTest);
     assertThat(underTest.okHttpClient().getSslSocketFactory()).isInstanceOf(SSLSocketFactory.getDefault().getClass());
diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/LocalWsConnectorTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/LocalWsConnectorTest.java
new file mode 100644 (file)
index 0000000..d92f4a8
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 com.google.common.collect.ImmutableMap;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.io.IOUtils;
+import org.hamcrest.Description;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Test;
+import org.sonar.api.server.ws.LocalConnector;
+import org.sonarqube.ws.MediaTypes;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class LocalWsConnectorTest {
+
+  LocalConnector connector = mock(LocalConnector.class);
+  LocalWsConnector underTest = new LocalWsConnector(connector);
+
+  @Test
+  public void baseUrl_is_always_slash() {
+    assertThat(underTest.baseUrl()).isEqualTo("/");
+  }
+
+  @Test
+  public void call_request() throws Exception {
+    WsRequest wsRequest = new PostRequest("api/issues/search")
+      .setMediaType(MediaTypes.JSON)
+      .setParam("foo", "bar");
+    answer(new DumbLocalResponse(400, MediaTypes.JSON, "{}".getBytes(UTF_8), Collections.<String>emptyList()));
+
+    WsResponse wsResponse = underTest.call(wsRequest);
+
+    verifyRequested("POST", "api/issues/search", MediaTypes.JSON, ImmutableMap.of("foo", "bar"));
+    assertThat(wsResponse.code()).isEqualTo(400);
+    assertThat(wsResponse.content()).isEqualTo("{}");
+    assertThat(IOUtils.toString(wsResponse.contentReader())).isEqualTo("{}");
+    assertThat(IOUtils.toString(wsResponse.contentStream())).isEqualTo("{}");
+    assertThat(wsResponse.contentType()).isEqualTo(MediaTypes.JSON);
+    assertThat(wsResponse.requestUrl()).isEqualTo("api/issues/search");
+  }
+
+  @Test
+  public void call_request_with_defaults() throws Exception {
+    // no parameters, no media type
+    WsRequest wsRequest = new GetRequest("api/issues/search");
+    answer(new DumbLocalResponse(200, MediaTypes.JSON, "".getBytes(UTF_8), Collections.<String>emptyList()));
+
+    WsResponse wsResponse = underTest.call(wsRequest);
+
+    verifyRequested("GET", "api/issues/search", MediaTypes.JSON, Collections.<String, String>emptyMap());
+    assertThat(wsResponse.code()).isEqualTo(200);
+    assertThat(wsResponse.content()).isEqualTo("");
+    assertThat(IOUtils.toString(wsResponse.contentReader())).isEqualTo("");
+    assertThat(IOUtils.toString(wsResponse.contentStream())).isEqualTo("");
+    assertThat(wsResponse.contentType()).isEqualTo(MediaTypes.JSON);
+  }
+
+  private void answer(DumbLocalResponse response) {
+    when(connector.call(any(LocalConnector.LocalRequest.class))).thenReturn(response);
+  }
+
+  private void verifyRequested(final String expectedMethod, final String expectedPath,
+    final String expectedMediaType,
+    final Map<String, String> expectedParams) {
+    verify(connector).call(argThat(new TypeSafeMatcher<LocalConnector.LocalRequest>() {
+      @Override
+      protected boolean matchesSafely(LocalConnector.LocalRequest localRequest) {
+        boolean ok = localRequest.getMethod().equals(expectedMethod) && localRequest.getPath().equals(expectedPath);
+        ok &= localRequest.getMediaType().equals(expectedMediaType);
+        for (Map.Entry<String, String> expectedParam : expectedParams.entrySet()) {
+          String paramKey = expectedParam.getKey();
+          ok &= localRequest.hasParam(paramKey);
+          ok &= expectedParam.getValue().equals(localRequest.getParam(paramKey));
+        }
+        return ok;
+      }
+
+      @Override
+      public void describeTo(Description description) {
+      }
+    }));
+  }
+
+  private static class DumbLocalResponse implements LocalConnector.LocalResponse {
+    private final int code;
+    private final String mediaType;
+    private final byte[] bytes;
+    private final List<String> headers;
+
+    public DumbLocalResponse(int code, String mediaType, byte[] bytes, List<String> headers) {
+      this.code = code;
+      this.mediaType = mediaType;
+      this.bytes = bytes;
+      this.headers = headers;
+    }
+
+    @Override
+    public int getStatus() {
+      return code;
+    }
+
+    @Override
+    public String getMediaType() {
+      return mediaType;
+    }
+
+    @Override
+    public byte[] getBytes() {
+      return bytes;
+    }
+
+    @Override
+    public Collection<String> getHeaderNames() {
+      return headers;
+    }
+
+    @Override
+    public String getHeader(String name) {
+      throw new UnsupportedOperationException();
+    }
+  }
+}
diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/WsClientFactoriesTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/WsClientFactoriesTest.java
new file mode 100644 (file)
index 0000000..351a80d
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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 org.junit.Test;
+import org.sonar.api.server.ws.LocalConnector;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class WsClientFactoriesTest {
+  @Test
+  public void create_http_client() {
+    HttpConnector connector = HttpConnector.newBuilder().url("http://localhost:9000").build();
+    WsClient client = WsClientFactories.getDefault().newClient(connector);
+
+    assertThat(client).isInstanceOf(DefaultWsClient.class);
+    assertThat(client.wsConnector()).isSameAs(connector);
+  }
+
+  @Test
+  public void create_local_client() {
+    LocalConnector connector = mock(LocalConnector.class);
+    WsClient client = WsClientFactories.getLocal().newClient(connector);
+
+    assertThat(client).isInstanceOf(DefaultWsClient.class);
+    assertThat(client.wsConnector()).isInstanceOf(LocalWsConnector.class);
+    assertThat(((LocalWsConnector) client.wsConnector()).getLocalConnector()).isSameAs(connector);
+  }
+}