diff options
Diffstat (limited to 'sonar-ws')
11 files changed, 1102 insertions, 1 deletions
diff --git a/sonar-ws/pom.xml b/sonar-ws/pom.xml index c82579b6d41..3b6e21d7cd7 100644 --- a/sonar-ws/pom.xml +++ b/sonar-ws/pom.xml @@ -1,5 +1,6 @@ <?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/xsd/maven-4.0.0.xsd"> +<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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> @@ -21,6 +22,61 @@ <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> </dependency> + <dependency> + <groupId>com.google.code.findbugs</groupId> + <artifactId>jsr305</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + </dependency> + + <!-- Tests --> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>sonar-testing-harness</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>javax.servlet</groupId> + <artifactId>javax.servlet-api</artifactId> + <scope>test</scope> + </dependency> + + <!-- Jetty dependencies --> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-server</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>test-jetty-servlet</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.googlecode.json-simple</groupId> + <artifactId>json-simple</artifactId> + </dependency> + <dependency> + <groupId>com.github.kevinsawicki</groupId> + <artifactId>http-request</artifactId> + </dependency> + <dependency> + <groupId>commons-httpclient</groupId> + <artifactId>commons-httpclient</artifactId> + <version>3.1</version> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <version>4.3.5</version> + </dependency> </dependencies> <build> diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpException.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpException.java new file mode 100644 index 00000000000..09aa68e5995 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpException.java @@ -0,0 +1,43 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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; + +/** + * @since 3.6 + */ +public class HttpException extends RuntimeException { + + private final String url; + private final int status; + + public HttpException(String url, int status, String message) { + super(String.format("Error %d on %s : %s", status, url, message)); + this.url = url; + this.status = status; + } + + public String url() { + return url; + } + + public int status() { + return status; + } +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpRequestFactory.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpRequestFactory.java new file mode 100644 index 00000000000..4ae2ce1f912 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/HttpRequestFactory.java @@ -0,0 +1,220 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.github.kevinsawicki.http.HttpRequest; +import com.google.common.base.Throwables; +import com.google.common.net.MediaType; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.Parser; +import java.util.Arrays; +import java.util.Map; +import javax.annotation.Nullable; + +import static java.net.HttpURLConnection.HTTP_CREATED; +import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static java.net.HttpURLConnection.HTTP_OK; + +/** + * Not an API. Please do not use this class, except maybe for unit tests. + */ +public class HttpRequestFactory { + + private static final int[] RESPONSE_SUCCESS = {HTTP_OK, HTTP_CREATED, HTTP_NO_CONTENT}; + + private final String baseUrl; + private String login, password, proxyHost, proxyLogin, proxyPassword; + private int proxyPort; + private int connectTimeoutInMilliseconds; + private int readTimeoutInMilliseconds; + + public HttpRequestFactory(String baseUrl) { + this.baseUrl = baseUrl; + } + + public HttpRequestFactory setLogin(@Nullable String login) { + this.login = login; + return this; + } + + public HttpRequestFactory setPassword(@Nullable String password) { + this.password = password; + return this; + } + + public HttpRequestFactory setProxyHost(@Nullable String proxyHost) { + this.proxyHost = proxyHost; + return this; + } + + public HttpRequestFactory setProxyLogin(@Nullable String proxyLogin) { + this.proxyLogin = proxyLogin; + return this; + } + + public HttpRequestFactory setProxyPassword(@Nullable String proxyPassword) { + this.proxyPassword = proxyPassword; + return this; + } + + public HttpRequestFactory setProxyPort(int proxyPort) { + this.proxyPort = proxyPort; + return this; + } + + public HttpRequestFactory setConnectTimeoutInMilliseconds(int connectTimeoutInMilliseconds) { + this.connectTimeoutInMilliseconds = connectTimeoutInMilliseconds; + return this; + } + + public HttpRequestFactory setReadTimeoutInMilliseconds(int readTimeoutInMilliseconds) { + this.readTimeoutInMilliseconds = readTimeoutInMilliseconds; + return this; + } + + public String getBaseUrl() { + return baseUrl; + } + + public String getLogin() { + return login; + } + + public String getPassword() { + return password; + } + + public String getProxyHost() { + return proxyHost; + } + + public String getProxyLogin() { + return proxyLogin; + } + + public String getProxyPassword() { + return proxyPassword; + } + + public int getProxyPort() { + return proxyPort; + } + + public int getConnectTimeoutInMilliseconds() { + return connectTimeoutInMilliseconds; + } + + public int getReadTimeoutInMilliseconds() { + return readTimeoutInMilliseconds; + } + + public String get(String wsUrl, Map<String, Object> queryParams) { + HttpRequest request = prepare(HttpRequest.get(buildUrl(wsUrl), queryParams, true)); + return execute(request); + } + + public String post(String wsUrl, Map<String, Object> queryParams) { + HttpRequest request = prepare(HttpRequest.post(buildUrl(wsUrl), true)).form(queryParams, HttpRequest.CHARSET_UTF8); + return execute(request); + } + + public String execute(WsRequest wsRequest) { + HttpRequest httpRequest = wsRequestToHttpRequest(wsRequest); + return execute(httpRequest); + } + + public <T extends Message> T execute(WsRequest wsRequest, Parser<T> protobufParser) { + HttpRequest httpRequest = wsRequestToHttpRequest(wsRequest); + try { + return protobufParser.parseFrom(httpRequest.bytes()); + } catch (InvalidProtocolBufferException e) { + Throwables.propagate(e); + } + + throw new IllegalStateException("Uncatched exception when parsing protobuf response"); + } + + private HttpRequest wsRequestToHttpRequest(WsRequest wsRequest) { + HttpRequest httpRequest = wsRequest.getMethod().equals(WsRequest.Method.GET) + ? HttpRequest.post(buildUrl(wsRequest.getUrl()), wsRequest.getParams(), true) + : HttpRequest.get(buildUrl(wsRequest.getUrl()), wsRequest.getParams(), true); + httpRequest = prepare(httpRequest); + switch (wsRequest.getMediaType()) { + case PROTOBUF: + httpRequest.accept(MediaType.PROTOBUF.toString()); + break; + default: + httpRequest.accept(MediaType.JSON_UTF_8.toString()); + break; + } + + return httpRequest; + } + + private String buildUrl(String part) { + StringBuilder url = new StringBuilder(); + url.append(baseUrl); + if (!part.startsWith("/")) { + url.append('/'); + } + url.append(part); + return url.toString(); + } + + private String execute(HttpRequest request) { + try { + if (isSuccess(request)) { + return request.body(HttpRequest.CHARSET_UTF8); + } + // TODO better handle error messages + throw new HttpException(request.url().toString(), request.code(), request.body()); + + } catch (HttpRequest.HttpRequestException e) { + throw new IllegalStateException("Fail to request " + request.url(), e.getCause()); + } + } + + private boolean isSuccess(HttpRequest request) { + return Arrays.binarySearch(RESPONSE_SUCCESS, request.code()) >= 0; + } + + private HttpRequest prepare(HttpRequest request) { + if (proxyHost != null) { + request.useProxy(proxyHost, proxyPort); + if (proxyLogin != null) { + request.proxyBasic(proxyLogin, proxyPassword); + } + } + request + .acceptGzipEncoding() + .uncompress(true) + .acceptJson() + .acceptCharset(HttpRequest.CHARSET_UTF8) + .connectTimeout(connectTimeoutInMilliseconds) + .readTimeout(readTimeoutInMilliseconds) + .trustAllCerts() + .trustAllHosts(); + if (login != null) { + request.basic(login, password); + } + return request; + } +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java new file mode 100644 index 00000000000..e6e6c827bd6 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java @@ -0,0 +1,168 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.protobuf.Message; +import com.google.protobuf.Parser; +import javax.annotation.Nullable; + +/** + * Entry point of the Java Client for SonarQube Web Services. + * <p/> + * Example: + * <pre> + * WsClient client = WsClient.create("http://localhost:9000"); + * </pre> + * + * @since 5.2 + */ +public class WsClient { + + public static final int DEFAULT_CONNECT_TIMEOUT_MILLISECONDS = 30000; + public static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = 60000; + + /** + * Visibility relaxed for unit tests + */ + final HttpRequestFactory requestFactory; + + private WsClient(Builder builder) { + this(new HttpRequestFactory(builder.url) + .setLogin(builder.login) + .setPassword(builder.password) + .setProxyHost(builder.proxyHost) + .setProxyPort(builder.proxyPort) + .setProxyLogin(builder.proxyLogin) + .setProxyPassword(builder.proxyPassword) + .setConnectTimeoutInMilliseconds(builder.connectTimeoutMs) + .setReadTimeoutInMilliseconds(builder.readTimeoutMs)); + } + + /** + * Visible for testing + */ + WsClient(HttpRequestFactory requestFactory) { + this.requestFactory = requestFactory; + } + + /** + * Create a builder of {@link WsClient}s. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Create a client with default configuration. Use {@link #builder()} to define + * a custom configuration (credentials, HTTP proxy, HTTP timeouts). + */ + public static WsClient create(String serverUrl) { + return builder().url(serverUrl).build(); + } + + public String execute(WsRequest wsRequest) { + return requestFactory.execute(wsRequest); + } + + public <T extends Message> T execute(WsRequest wsRequest, Parser<T> protobufParser) { + return requestFactory.execute(wsRequest, protobufParser); + } + + public static class Builder { + private String login, password, url, proxyHost, proxyLogin, proxyPassword; + private int proxyPort = 0; + private int connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLISECONDS, readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLISECONDS; + + private Builder() { + } + + /** + * Mandatory HTTP server URL, eg "http://localhost:9000" + */ + public Builder url(String url) { + this.url = url; + return this; + } + + /** + * Optional login, for example "admin" + */ + public Builder login(@Nullable String login) { + this.login = login; + return this; + } + + /** + * Optional password related to {@link #login(String)}, for example "admin" + */ + public Builder password(@Nullable String password) { + this.password = password; + return this; + } + + /** + * Host and port of the optional HTTP proxy + */ + public Builder proxy(@Nullable String proxyHost, int proxyPort) { + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + return this; + } + + public Builder proxyLogin(@Nullable String proxyLogin) { + this.proxyLogin = proxyLogin; + return this; + } + + public Builder proxyPassword(@Nullable String proxyPassword) { + this.proxyPassword = proxyPassword; + return this; + } + + /** + * Sets a specified timeout value, in milliseconds, to be used when opening HTTP connection. + * A timeout of zero is interpreted as an infinite timeout. Default value is {@link WsClient#DEFAULT_CONNECT_TIMEOUT_MILLISECONDS} + */ + public Builder connectTimeoutMilliseconds(int i) { + this.connectTimeoutMs = i; + return this; + } + + /** + * Sets the read timeout to a specified timeout, in milliseconds. + * A timeout of zero is interpreted as an infinite timeout. Default value is {@link WsClient#DEFAULT_READ_TIMEOUT_MILLISECONDS} + */ + public Builder readTimeoutMilliseconds(int i) { + this.readTimeoutMs = i; + return this; + } + + /** + * Build a new client + */ + public WsClient build() { + if (url == null || "".equals(url)) { + throw new IllegalStateException("Server URL must be set"); + } + return new WsClient(this); + } + } +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java new file mode 100644 index 00000000000..9641e0de785 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/WsRequest.java @@ -0,0 +1,80 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.HashMap; +import java.util.Map; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class WsRequest { + private final Map<String, Object> params = new HashMap<>(); + private Method method = Method.GET; + private MediaType mimeType = MediaType.JSON; + private String url; + + public WsRequest(String url) { + this.url = url; + } + + public Method getMethod() { + return method; + } + + public WsRequest setMethod(Method method) { + checkNotNull(method); + this.method = method; + return this; + } + + public MediaType getMediaType() { + return mimeType; + } + + public WsRequest setMediaType(MediaType type) { + checkNotNull(type); + this.mimeType = type; + return this; + } + + public WsRequest setParam(String key, Object value) { + checkNotNull(key); + checkNotNull(value); + this.params.put(key, value); + return this; + } + + public String getUrl() { + return url; + } + + public Map<String, Object> getParams() { + return params; + } + + public enum Method { + GET, POST + } + + public enum MediaType { + PROTOBUF, JSON, TEXT + } +} diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/package-info.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/package-info.java new file mode 100644 index 00000000000..d9978db6002 --- /dev/null +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/package-info.java @@ -0,0 +1,22 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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. + */ + +@javax.annotation.ParametersAreNonnullByDefault +package org.sonarqube.ws.client; diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpExceptionTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpExceptionTest.java new file mode 100644 index 00000000000..3eab476fec9 --- /dev/null +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpExceptionTest.java @@ -0,0 +1,34 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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 static org.assertj.core.api.Assertions.assertThat; + +public class HttpExceptionTest { + @Test + public void test_exception() throws Exception { + HttpException exception = new HttpException("http://localhost:9000/api/search", 500, "Not found"); + assertThat(exception.status()).isEqualTo(500); + assertThat(exception.url()).isEqualTo("http://localhost:9000/api/search"); + assertThat(exception.getMessage()).isEqualTo("Error 500 on http://localhost:9000/api/search : Not found"); + } +} diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpRequestFactoryTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpRequestFactoryTest.java new file mode 100644 index 00000000000..f14ad893b54 --- /dev/null +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/HttpRequestFactoryTest.java @@ -0,0 +1,138 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.net.ConnectException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import org.junit.Rule; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +public class HttpRequestFactoryTest { + @Rule + public MockHttpServerInterceptor httpServer = new MockHttpServerInterceptor(); + + @Test + public void test_get() { + httpServer.stubStatusCode(200).stubResponseBody("{'issues': []}"); + + HttpRequestFactory factory = new HttpRequestFactory(httpServer.url()); + String json = factory.get("/api/issues", Collections.<String, Object>emptyMap()); + + assertThat(json).isEqualTo("{'issues': []}"); + assertThat(httpServer.requestedPath()).isEqualTo("/api/issues"); + } + + @Test + public void should_throw_illegal_state_exc_if_connect_exception() { + HttpRequestFactory factory = new HttpRequestFactory("http://localhost:1"); + try { + factory.get("/api/issues", Collections.<String, Object>emptyMap()); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalStateException.class); + assertThat(e).hasMessage("Fail to request http://localhost:1/api/issues"); + assertThat(e.getCause().getMessage()).matches(".*(Connection refused|Connexion refusée).*"); + } + } + + @Test + public void test_post() { + httpServer.stubStatusCode(200).stubResponseBody("{}"); + + HttpRequestFactory factory = new HttpRequestFactory(httpServer.url()); + String json = factory.post("/api/issues/change", Collections.<String, Object>emptyMap()); + + assertThat(json).isEqualTo("{}"); + assertThat(httpServer.requestedPath()).isEqualTo("/api/issues/change"); + } + + @Test + public void post_successful_if_created() { + httpServer.stubStatusCode(201).stubResponseBody("{}"); + + HttpRequestFactory factory = new HttpRequestFactory(httpServer.url()); + String json = factory.post("/api/issues/change", Collections.<String, Object>emptyMap()); + + assertThat(json).isEqualTo("{}"); + assertThat(httpServer.requestedPath()).isEqualTo("/api/issues/change"); + } + + @Test + public void post_successful_if_no_content() { + httpServer.stubStatusCode(204).stubResponseBody(""); + + HttpRequestFactory factory = new HttpRequestFactory(httpServer.url()); + String json = factory.post("/api/issues/change", Collections.<String, Object>emptyMap()); + + assertThat(json).isEqualTo(""); + assertThat(httpServer.requestedPath()).isEqualTo("/api/issues/change"); + } + + @Test + public void test_authentication() { + httpServer.stubStatusCode(200).stubResponseBody("{}"); + + HttpRequestFactory factory = new HttpRequestFactory(httpServer.url()).setLogin("karadoc").setPassword("legrascestlavie"); + String json = factory.get("/api/issues", Collections.<String, Object>emptyMap()); + + assertThat(json).isEqualTo("{}"); + assertThat(httpServer.requestedPath()).isEqualTo("/api/issues"); + assertThat(httpServer.requestHeaders().get("Authorization")).isEqualTo("Basic a2FyYWRvYzpsZWdyYXNjZXN0bGF2aWU="); + } + + @Test + public void test_proxy() throws Exception { + HttpRequestFactory factory = new HttpRequestFactory(httpServer.url()) + .setProxyHost("localhost").setProxyPort(1) + .setProxyLogin("john").setProxyPassword("smith"); + try { + factory.get("/api/issues", Collections.<String, Object>emptyMap()); + fail(); + } catch (IllegalStateException e) { + // it's not possible to check that the proxy is correctly configured + assertThat(e.getCause()).isInstanceOf(ConnectException.class); + } + } + + @Test + public void beginning_slash_is_optional() throws Exception { + HttpRequestFactory factory = new HttpRequestFactory(httpServer.url()); + factory.get("api/foo", Collections.<String, Object>emptyMap()); + assertThat(httpServer.requestedPath()).isEqualTo("/api/foo"); + + factory.get("/api/bar", Collections.<String, Object>emptyMap()); + assertThat(httpServer.requestedPath()).isEqualTo("/api/bar"); + } + + protected static Date toDate(String sDate) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.parse(sDate); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/MockHttpServer.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/MockHttpServer.java new file mode 100644 index 00000000000..c24be3ef64c --- /dev/null +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/MockHttpServer.java @@ -0,0 +1,128 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.io.IOException; +import java.net.HttpURLConnection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; + +import static org.apache.commons.io.IOUtils.write; + +public class MockHttpServer { + private Server server; + private String responseBody; + private byte[] binaryResponseBody; + private int responseStatus = HttpURLConnection.HTTP_OK; + private String requestPath; + private Map requestHeaders = new HashMap(), requestParams = new HashMap(); + private String contentType; + + public void start() throws Exception { + // 0 is random available port + server = new Server(0); + server.setHandler(getMockHandler()); + server.start(); + } + + public Handler getMockHandler() { + Handler handler = new AbstractHandler() { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest httpServletRequest, HttpServletResponse response) throws IOException, ServletException { + requestPath = baseRequest.getUri().toString(); + requestHeaders.clear(); + Enumeration names = baseRequest.getHeaderNames(); + while (names.hasMoreElements()) { + String headerName = (String) names.nextElement(); + requestHeaders.put(headerName, baseRequest.getHeader(headerName)); + } + requestParams.clear(); + names = baseRequest.getParameterNames(); + while (names.hasMoreElements()) { + String headerName = (String) names.nextElement(); + requestParams.put(headerName, baseRequest.getParameter(headerName)); + } + response.setStatus(responseStatus); + response.setContentType("application/json;charset=utf-8"); + if (responseBody != null) { + write(responseBody, response.getOutputStream()); + } else { + write(binaryResponseBody, response.getOutputStream()); + } + baseRequest.setHandled(true); + } + }; + return handler; + } + + public void stop() { + try { + if (server != null) { + server.stop(); + } + } catch (Exception e) { + throw new IllegalStateException("Fail to stop HTTP server", e); + } + } + + public MockHttpServer doReturnBody(String responseBody) { + this.responseBody = responseBody; + return this; + } + + public MockHttpServer doReturnBody(byte[] responseBody) { + this.binaryResponseBody = responseBody; + return this; + } + + public MockHttpServer doReturnStatus(int status) { + this.responseStatus = status; + return this; + } + + public MockHttpServer doReturnContentType(String contentType) { + this.contentType = contentType; + return this; + } + + public String requestPath() { + return requestPath; + } + + public Map requestHeaders() { + return requestHeaders; + } + + public Map requestParams() { + return requestParams; + } + + public int getPort() { + return server.getConnectors()[0].getLocalPort(); + } +} diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/MockHttpServerInterceptor.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/MockHttpServerInterceptor.java new file mode 100644 index 00000000000..cd361262709 --- /dev/null +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/MockHttpServerInterceptor.java @@ -0,0 +1,71 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.rules.ExternalResource; + +import java.util.Map; + +public final class MockHttpServerInterceptor extends ExternalResource { + + private MockHttpServer server; + + @Override + protected final void before() throws Throwable { + server = new MockHttpServer(); + server.start(); + } + + @Override + protected void after() { + server.stop(); + } + + public MockHttpServerInterceptor stubResponseBody(String body) { + server.doReturnBody(body); + return this; + } + + public MockHttpServerInterceptor stubStatusCode(int status) { + server.doReturnStatus(status); + return this; + } + + public String requestedPath() { + return server.requestPath(); + } + + public Map requestHeaders() { + return server.requestHeaders(); + } + + public Map requestParams() { + return server.requestParams(); + } + + public int port() { + return server.getPort(); + } + + public String url() { + return "http://localhost:" + port(); + } +} diff --git a/sonar-ws/src/test/java/org/sonarqube/ws/client/WsClientTest.java b/sonar-ws/src/test/java/org/sonarqube/ws/client/WsClientTest.java new file mode 100644 index 00000000000..6a62da72401 --- /dev/null +++ b/sonar-ws/src/test/java/org/sonarqube/ws/client/WsClientTest.java @@ -0,0 +1,141 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.net.HttpHeaders; +import com.google.common.net.MediaType; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonarqube.ws.WsComponents; + +import static java.net.HttpURLConnection.HTTP_OK; +import static org.assertj.core.api.Assertions.assertThat; + +public class WsClientTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + MockHttpServer server; + + WsClient underTest; + + @Before + public void setUp() throws Exception { + server = new MockHttpServer(); + server.start(); + + underTest = WsClient.create("http://localhost:" + server.getPort()); + } + + @After + public void stopServer() { + if (server != null) { + server.stop(); + } + } + + @Test + public void return_protobuf_response() throws Exception { + server.doReturnBody( + WsComponents.WsSearchResponse + .newBuilder() + .addComponents(WsComponents.WsSearchResponse.Component.getDefaultInstance()) + .build() + .toByteArray()); + server.doReturnStatus(HTTP_OK); + server.doReturnContentType(MediaType.PROTOBUF.toString()); + + WsComponents.WsSearchResponse response = underTest.execute( + new WsRequest("api/components/search").setMediaType(WsRequest.MediaType.PROTOBUF), + WsComponents.WsSearchResponse.PARSER); + + assertThat(response.getComponentsCount()).isEqualTo(1); + assertThat(server.requestHeaders().get(HttpHeaders.ACCEPT)).isEqualTo(MediaType.PROTOBUF.toString()); + } + + @Test + public void return_json_response() throws Exception { + String expectedResponse = "{\"key\":value}"; + server.doReturnBody(expectedResponse); + server.doReturnStatus(HTTP_OK); + server.doReturnContentType(MediaType.JSON_UTF_8.toString()); + + String response = underTest.execute(new WsRequest("api/components/search")); + + assertThat(response).isEqualTo(expectedResponse); + assertThat(server.requestHeaders().get(HttpHeaders.ACCEPT)).isEqualTo(MediaType.JSON_UTF_8.toString()); + } + + @Test + public void url_should_not_be_null() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Server URL must be set"); + + underTest.builder().build(); + } + + @Test + public void url_should_not_be_empty() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Server URL must be set"); + + underTest.create(""); + } + + @Test + public void test_default_configuration() throws Exception { + underTest = WsClient.create("http://localhost:9000"); + assertThat(underTest.requestFactory.getBaseUrl()).isEqualTo("http://localhost:9000"); + assertThat(underTest.requestFactory.getLogin()).isNull(); + assertThat(underTest.requestFactory.getPassword()).isNull(); + assertThat(underTest.requestFactory.getConnectTimeoutInMilliseconds()).isEqualTo(WsClient.DEFAULT_CONNECT_TIMEOUT_MILLISECONDS); + assertThat(underTest.requestFactory.getReadTimeoutInMilliseconds()).isEqualTo(WsClient.DEFAULT_READ_TIMEOUT_MILLISECONDS); + assertThat(underTest.requestFactory.getProxyHost()).isNull(); + assertThat(underTest.requestFactory.getProxyPort()).isEqualTo(0); + assertThat(underTest.requestFactory.getProxyLogin()).isNull(); + assertThat(underTest.requestFactory.getProxyPassword()).isNull(); + } + + @Test + public void test_custom_configuration() throws Exception { + underTest = WsClient.builder().url("http://localhost:9000") + .login("eric") + .password("pass") + .connectTimeoutMilliseconds(12345) + .readTimeoutMilliseconds(6789) + .proxy("localhost", 2052) + .proxyLogin("proxyLogin") + .proxyPassword("proxyPass") + .build(); + assertThat(underTest.requestFactory.getBaseUrl()).isEqualTo("http://localhost:9000"); + assertThat(underTest.requestFactory.getLogin()).isEqualTo("eric"); + assertThat(underTest.requestFactory.getPassword()).isEqualTo("pass"); + assertThat(underTest.requestFactory.getConnectTimeoutInMilliseconds()).isEqualTo(12345); + assertThat(underTest.requestFactory.getReadTimeoutInMilliseconds()).isEqualTo(6789); + assertThat(underTest.requestFactory.getProxyHost()).isEqualTo("localhost"); + assertThat(underTest.requestFactory.getProxyPort()).isEqualTo(2052); + assertThat(underTest.requestFactory.getProxyLogin()).isEqualTo("proxyLogin"); + assertThat(underTest.requestFactory.getProxyPassword()).isEqualTo("proxyPass"); + } +} |