aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-webserver-ws/src
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2019-08-08 11:22:13 +0200
committerSonarTech <sonartech@sonarsource.com>2019-08-14 20:21:12 +0200
commit980b9f16b8a34364489d2ed3a8472f725eea4770 (patch)
treecf6e56931905baa05ae17de93d803ccbdd4c75f0 /server/sonar-webserver-ws/src
parent28147b8b05a5244c93e13b51924acfc683bd93f6 (diff)
downloadsonarqube-980b9f16b8a34364489d2ed3a8472f725eea4770.tar.gz
sonarqube-980b9f16b8a34364489d2ed3a8472f725eea4770.zip
create sonar-webserver-es from sonar-server
ie. move WS engine to dedicated project
Diffstat (limited to 'server/sonar-webserver-ws/src')
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/BadRequestException.java62
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/ForbiddenException.java34
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/Message.java66
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/NotFoundException.java29
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/ServerException.java35
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/UnauthorizedException.java32
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/package-info.java24
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/CacheWriter.java61
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/DefaultLocalResponse.java146
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/JsonWriterUtils.java66
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/KeyExamples.java41
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java92
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/RemovedWebServiceHandler.java46
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/RequestVerifier.java47
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletFilterHandler.java42
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletRequest.java172
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletResponse.java123
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WebServiceEngine.java245
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsAction.java32
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsParameterBuilder.java135
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsUtils.java123
-rw-r--r--server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/package-info.java23
-rw-r--r--server/sonar-webserver-ws/src/main/resources/org/sonar/server/ws/removed-ws-example.json7
-rw-r--r--server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/CacheWriterTest.java56
-rw-r--r--server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/DumbResponse.java133
-rw-r--r--server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/RemovedWebServiceHandlerTest.java51
-rw-r--r--server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletRequestTest.java217
-rw-r--r--server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletResponseTest.java126
-rw-r--r--server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/TestRequest.java198
-rw-r--r--server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/TestResponse.java91
-rw-r--r--server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java483
-rw-r--r--server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsActionTester.java47
-rw-r--r--server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsTester.java361
-rw-r--r--server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsUtilsTest.java99
-rw-r--r--server/sonar-webserver-ws/src/test/resources/logback-test.xml26
35 files changed, 3571 insertions, 0 deletions
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/BadRequestException.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/BadRequestException.java
new file mode 100644
index 00000000000..7a2fdf7166d
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/BadRequestException.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.exceptions;
+
+import com.google.common.base.MoreObjects;
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
+import static java.util.Arrays.asList;
+
+/**
+ * Request is not valid and can not be processed.
+ */
+public class BadRequestException extends ServerException {
+
+ private final transient List<String> errors;
+
+ private BadRequestException(List<String> errors) {
+ super(HTTP_BAD_REQUEST, errors.get(0));
+ this.errors = errors;
+ }
+
+ public static BadRequestException create(List<String> errorMessages) {
+ checkArgument(!errorMessages.isEmpty(), "At least one error message is required");
+ checkArgument(errorMessages.stream().noneMatch(message -> message == null || message.isEmpty()), "Message cannot be empty");
+ return new BadRequestException(errorMessages);
+ }
+
+ public static BadRequestException create(String... errorMessages) {
+ return create(asList(errorMessages));
+ }
+
+ public List<String> errors() {
+ return errors;
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("errors", errors)
+ .toString();
+ }
+
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/ForbiddenException.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/ForbiddenException.java
new file mode 100644
index 00000000000..d72eefbd02f
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/ForbiddenException.java
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.exceptions;
+
+import com.google.common.base.Preconditions;
+
+import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
+
+/**
+ * Permission denied. User does not have the required permissions.
+ */
+public class ForbiddenException extends ServerException {
+
+ public ForbiddenException(String message) {
+ super(HTTP_FORBIDDEN, Preconditions.checkNotNull(message));
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/Message.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/Message.java
new file mode 100644
index 00000000000..c069ead73d2
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/Message.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.exceptions;
+
+import com.google.common.base.Preconditions;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.lang.String.format;
+
+public class Message {
+
+ private final String msg;
+
+ private Message(String format, Object... params) {
+ Preconditions.checkArgument(!isNullOrEmpty(format), "Message cannot be empty");
+ this.msg = format(format, params);
+ }
+
+ public String getMessage() {
+ return msg;
+ }
+
+ public static Message of(String msg, Object... arguments) {
+ return new Message(msg, arguments);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Message other = (Message) o;
+ return this.msg.equals(other.msg);
+ }
+
+ @Override
+ public int hashCode() {
+ return msg.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return msg;
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/NotFoundException.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/NotFoundException.java
new file mode 100644
index 00000000000..ff5fb2f13c4
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/NotFoundException.java
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.exceptions;
+
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+
+public class NotFoundException extends ServerException {
+
+ public NotFoundException(String message) {
+ super(HTTP_NOT_FOUND, message);
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/ServerException.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/ServerException.java
new file mode 100644
index 00000000000..491c17ed437
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/ServerException.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.exceptions;
+
+import static java.util.Objects.requireNonNull;
+
+public class ServerException extends RuntimeException {
+ private final int httpCode;
+
+ public ServerException(int httpCode, String message) {
+ super(requireNonNull(message, "Error message cannot be null"));
+ this.httpCode = httpCode;
+ }
+
+ public int httpCode() {
+ return httpCode;
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/UnauthorizedException.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/UnauthorizedException.java
new file mode 100644
index 00000000000..0b4af12beee
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/UnauthorizedException.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.exceptions;
+
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+
+/**
+ * User needs to be authenticated. HTTP request is generally redirected to login form.
+ */
+public class UnauthorizedException extends ServerException {
+
+ public UnauthorizedException(String message) {
+ super(HTTP_UNAUTHORIZED, message);
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/package-info.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/package-info.java
new file mode 100644
index 00000000000..c1ac144f25a
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.exceptions;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/CacheWriter.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/CacheWriter.java
new file mode 100644
index 00000000000..75ffb3a6449
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/CacheWriter.java
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import org.apache.commons.io.IOUtils;
+
+/**
+ * Writer that writes only when closing the resource
+ */
+class CacheWriter extends Writer {
+ private final StringWriter bufferWriter;
+ private final Writer outputWriter;
+ private boolean isClosed;
+
+ CacheWriter(Writer outputWriter) {
+ this.bufferWriter = new StringWriter();
+ this.outputWriter = outputWriter;
+ this.isClosed = false;
+ }
+
+ @Override
+ public void write(char[] cbuf, int off, int len) {
+ bufferWriter.write(cbuf, off, len);
+ }
+
+ @Override
+ public void flush() {
+ bufferWriter.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (isClosed) {
+ return;
+ }
+
+ IOUtils.write(bufferWriter.toString(), outputWriter);
+ outputWriter.close();
+ this.isClosed = true;
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/DefaultLocalResponse.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/DefaultLocalResponse.java
new file mode 100644
index 00000000000..07c11840a16
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/DefaultLocalResponse.java
@@ -0,0 +1,146 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.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-webserver-ws/src/main/java/org/sonar/server/ws/JsonWriterUtils.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/JsonWriterUtils.java
new file mode 100644
index 00000000000..dec4bbc67c4
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/JsonWriterUtils.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import java.util.Collection;
+import java.util.Date;
+import javax.annotation.Nullable;
+import org.sonar.api.utils.text.JsonWriter;
+
+public class JsonWriterUtils {
+
+ private JsonWriterUtils() {
+ // Utility class
+ }
+
+ public static void writeIfNeeded(JsonWriter json, @Nullable String value, String field, @Nullable Collection<String> fields) {
+ if (isFieldNeeded(field, fields)) {
+ json.prop(field, value);
+ }
+ }
+
+ public static void writeIfNeeded(JsonWriter json, @Nullable Boolean value, String field, @Nullable Collection<String> fields) {
+ if (isFieldNeeded(field, fields)) {
+ json.prop(field, value);
+ }
+ }
+
+ public static void writeIfNeeded(JsonWriter json, @Nullable Integer value, String field, @Nullable Collection<String> fields) {
+ if (isFieldNeeded(field, fields)) {
+ json.prop(field, value);
+ }
+ }
+
+ public static void writeIfNeeded(JsonWriter json, @Nullable Long value, String field, @Nullable Collection<String> fields) {
+ if (isFieldNeeded(field, fields)) {
+ json.prop(field, value);
+ }
+ }
+
+ public static void writeIfNeeded(JsonWriter json, @Nullable Date value, String field, @Nullable Collection<String> fields) {
+ if (isFieldNeeded(field, fields)) {
+ json.propDateTime(field, value);
+ }
+ }
+
+ public static boolean isFieldNeeded(String field, @Nullable Collection<String> fields) {
+ return fields == null || fields.isEmpty() || fields.contains(field);
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/KeyExamples.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/KeyExamples.java
new file mode 100644
index 00000000000..ef8773deec0
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/KeyExamples.java
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+public class KeyExamples {
+ public static final String KEY_FILE_EXAMPLE_001 = "my_project:/src/foo/Bar.php";
+ public static final String KEY_FILE_EXAMPLE_002 = "another_project:/src/foo/Foo.php";
+ public static final String KEY_PROJECT_EXAMPLE_001 = "my_project";
+ public static final String KEY_PROJECT_EXAMPLE_002 = "another_project";
+ public static final String KEY_PROJECT_EXAMPLE_003 = "third_project";
+
+ public static final String KEY_ORG_EXAMPLE_001 = "my-org";
+ public static final String KEY_ORG_EXAMPLE_002 = "foo-company";
+
+ public static final String KEY_BRANCH_EXAMPLE_001 = "feature/my_branch";
+ public static final String KEY_PULL_REQUEST_EXAMPLE_001 = "5461";
+
+ public static final String NAME_WEBHOOK_EXAMPLE_001 = "My Webhook";
+ public static final String URL_WEBHOOK_EXAMPLE_001 = "https://www.my-webhook-listener.com/sonar";
+
+ private KeyExamples() {
+ // prevent instantiation
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java
new file mode 100644
index 00000000000..849ce475240
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.sonar.api.server.ws.LocalConnector;
+import org.sonar.api.impl.ws.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
+ public Map<String, String[]> getParams() {
+ return localRequest.getParameterMap();
+ }
+
+ @Override
+ protected List<String> readMultiParam(String key) {
+ return localRequest.getMultiParam(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
+ protected Part readPart(String key) {
+ throw new UnsupportedOperationException("reading part is not supported yet by local WS calls");
+ }
+
+ @Override
+ public boolean hasParam(String key) {
+ return localRequest.hasParam(key);
+ }
+
+ @Override
+ public String getPath() {
+ return localRequest.getPath();
+ }
+
+ @Override
+ public String method() {
+ return localRequest.getMethod();
+ }
+
+ @Override
+ public String getMediaType() {
+ return localRequest.getMediaType();
+ }
+
+ @Override
+ public Optional<String> header(String name) {
+ return localRequest.getHeader(name);
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/RemovedWebServiceHandler.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/RemovedWebServiceHandler.java
new file mode 100644
index 00000000000..1b2f608ecd8
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/RemovedWebServiceHandler.java
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import com.google.common.io.Resources;
+import java.net.URL;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.RequestHandler;
+import org.sonar.api.server.ws.Response;
+import org.sonar.server.exceptions.ServerException;
+
+import static java.net.HttpURLConnection.HTTP_GONE;
+
+/**
+ * Used to declare web services that are removed
+ */
+public enum RemovedWebServiceHandler implements RequestHandler {
+
+ INSTANCE;
+
+ @Override
+ public void handle(Request request, Response response) {
+ throw new ServerException(HTTP_GONE, String.format("The web service '%s' doesn't exist anymore, please read its documentation to use alternatives", request.getPath()));
+ }
+
+ public URL getResponseExample() {
+ return Resources.getResource(RemovedWebServiceHandler.class, "removed-ws-example.json");
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/RequestVerifier.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/RequestVerifier.java
new file mode 100644
index 00000000000..c5e86345593
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/RequestVerifier.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.server.exceptions.ServerException;
+
+import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
+
+public class RequestVerifier {
+ private RequestVerifier() {
+ // static methods only
+ }
+
+ public static void verifyRequest(WebService.Action action, Request request) {
+ switch (request.method()) {
+ case "GET":
+ if (action.isPost()) {
+ throw new ServerException(SC_METHOD_NOT_ALLOWED, "HTTP method POST is required");
+ }
+ return;
+ case "PUT":
+ case "DELETE":
+ throw new ServerException(SC_METHOD_NOT_ALLOWED, String.format("HTTP method %s is not allowed", request.method()));
+ default:
+ // Nothing to do
+ }
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletFilterHandler.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletFilterHandler.java
new file mode 100644
index 00000000000..7ae51e2241b
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletFilterHandler.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.RequestHandler;
+import org.sonar.api.server.ws.Response;
+
+/**
+ * Used to declare web services that are implemented by a servlet filter.
+ */
+public class ServletFilterHandler implements RequestHandler {
+
+ public static final RequestHandler INSTANCE = new ServletFilterHandler();
+
+ private ServletFilterHandler() {
+ // Nothing
+ }
+
+ @Override
+ public void handle(Request request, Response response) {
+ throw new UnsupportedOperationException("This web service is implemented as a servlet filter");
+ }
+
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletRequest.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletRequest.java
new file mode 100644
index 00000000000..1f473a9011b
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletRequest.java
@@ -0,0 +1,172 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.net.HttpHeaders;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.CheckForNull;
+import javax.servlet.http.HttpServletRequest;
+import org.sonar.api.impl.ws.PartImpl;
+import org.sonar.api.impl.ws.ValidatingRequest;
+import org.sonar.api.utils.log.Loggers;
+import org.sonarqube.ws.MediaTypes;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static java.util.Collections.emptyList;
+import static java.util.Locale.ENGLISH;
+import static org.apache.commons.lang.StringUtils.substringAfterLast;
+import static org.apache.tomcat.util.http.fileupload.FileUploadBase.MULTIPART;
+
+public class ServletRequest extends ValidatingRequest {
+
+ private final HttpServletRequest source;
+
+ static final Map<String, String> SUPPORTED_MEDIA_TYPES_BY_URL_SUFFIX = ImmutableMap.of(
+ "json", MediaTypes.JSON,
+ "protobuf", MediaTypes.PROTOBUF,
+ "text", MediaTypes.TXT);
+
+ public ServletRequest(HttpServletRequest source) {
+ this.source = source;
+ }
+
+ @Override
+ public String method() {
+ return source.getMethod();
+ }
+
+ @Override
+ public String getMediaType() {
+ return firstNonNull(
+ mediaTypeFromUrl(source.getRequestURI()),
+ firstNonNull(
+ acceptedContentTypeInResponse(),
+ MediaTypes.DEFAULT));
+ }
+
+ @Override
+ public BufferedReader getReader() {
+ try {
+ return source.getReader();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to read", e);
+ }
+ }
+
+ @Override
+ public boolean hasParam(String key) {
+ return source.getParameterMap().containsKey(key);
+ }
+
+ @Override
+ public String readParam(String key) {
+ return source.getParameter(key);
+ }
+
+ @Override
+ public Map<String, String[]> getParams() {
+ return source.getParameterMap();
+ }
+
+ @Override
+ public List<String> readMultiParam(String key) {
+ String[] values = source.getParameterValues(key);
+ return values == null ? emptyList() : ImmutableList.copyOf(values);
+ }
+
+ @Override
+ protected InputStream readInputStreamParam(String key) {
+ Part part = readPart(key);
+ return (part == null) ? null : part.getInputStream();
+ }
+
+ @Override
+ @CheckForNull
+ public Part readPart(String key) {
+ try {
+ if (!isMultipartContent()) {
+ return null;
+ }
+ javax.servlet.http.Part part = source.getPart(key);
+ if (part == null || part.getSize() == 0) {
+ return null;
+ }
+ return new PartImpl(part.getInputStream(), part.getSubmittedFileName());
+ } catch (Exception e) {
+ Loggers.get(ServletRequest.class).warn("Can't read file part for parameter " + key, e);
+ return null;
+ }
+ }
+
+ private boolean isMultipartContent() {
+ String contentType = source.getContentType();
+ return contentType != null && contentType.toLowerCase(ENGLISH).startsWith(MULTIPART);
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer url = source.getRequestURL();
+ String query = source.getQueryString();
+ if (query != null) {
+ url.append("?").append(query);
+ }
+ return url.toString();
+ }
+
+ @CheckForNull
+ private String acceptedContentTypeInResponse() {
+ return source.getHeader(HttpHeaders.ACCEPT);
+ }
+
+ @CheckForNull
+ private static String mediaTypeFromUrl(String url) {
+ String formatSuffix = substringAfterLast(url, ".");
+ return SUPPORTED_MEDIA_TYPES_BY_URL_SUFFIX.get(formatSuffix.toLowerCase(ENGLISH));
+ }
+
+ @Override
+ public String getPath() {
+ return source.getRequestURI().replaceFirst(source.getContextPath(), "");
+ }
+
+ @Override
+ public Optional<String> header(String name) {
+ return Optional.ofNullable(source.getHeader(name));
+ }
+
+ @Override
+ public Map<String, String> getHeaders() {
+ ImmutableMap.Builder<String, String> mapBuilder = ImmutableMap.builder();
+ Enumeration<String> headerNames = source.getHeaderNames();
+ while (headerNames.hasMoreElements()) {
+ String headerName = headerNames.nextElement();
+ mapBuilder.put(headerName, source.getHeader(headerName));
+ }
+ return mapBuilder.build();
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletResponse.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletResponse.java
new file mode 100644
index 00000000000..2f385b89bcd
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletResponse.java
@@ -0,0 +1,123 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.api.utils.text.XmlWriter;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.sonarqube.ws.MediaTypes.JSON;
+import static org.sonarqube.ws.MediaTypes.XML;
+
+public class ServletResponse implements Response {
+
+ private final ServletStream stream;
+
+ public ServletResponse(HttpServletResponse response) {
+ stream = new ServletStream(response);
+ }
+
+ public static class ServletStream implements Stream {
+ private final HttpServletResponse response;
+
+ public ServletStream(HttpServletResponse response) {
+ this.response = response;
+ this.response.setStatus(200);
+ // SONAR-6964 WS should not be cached by browser
+ this.response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ }
+
+ @Override
+ public ServletStream setMediaType(String s) {
+ this.response.setContentType(s);
+ return this;
+ }
+
+ @Override
+ public ServletStream setStatus(int httpStatus) {
+ this.response.setStatus(httpStatus);
+ return this;
+ }
+
+ @Override
+ public OutputStream output() {
+ try {
+ return response.getOutputStream();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ HttpServletResponse response() {
+ return response;
+ }
+
+ public ServletStream reset() {
+ response.reset();
+ return this;
+ }
+ }
+
+ @Override
+ public JsonWriter newJsonWriter() {
+ stream.setMediaType(JSON);
+ return JsonWriter.of(new CacheWriter(new OutputStreamWriter(stream.output(), StandardCharsets.UTF_8)));
+ }
+
+ @Override
+ public XmlWriter newXmlWriter() {
+ stream.setMediaType(XML);
+ return XmlWriter.of(new OutputStreamWriter(stream.output(), UTF_8));
+ }
+
+ @Override
+ public ServletStream stream() {
+ return stream;
+ }
+
+ @Override
+ public Response noContent() {
+ stream.setStatus(204);
+ return this;
+ }
+
+ @Override
+ public Response setHeader(String name, String value) {
+ stream.response().setHeader(name, value);
+ return this;
+ }
+
+ @Override
+ public Collection<String> getHeaderNames() {
+ return stream.response().getHeaderNames();
+ }
+
+ @Override
+ public String getHeader(String name) {
+ return stream.response().getHeader(name);
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WebServiceEngine.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WebServiceEngine.java
new file mode 100644
index 00000000000..e7569e60350
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WebServiceEngine.java
@@ -0,0 +1,245 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import com.google.common.base.Throwables;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Locale;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.catalina.connector.ClientAbortException;
+import org.picocontainer.Startable;
+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.impl.ws.ValidatingRequest;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.ServerException;
+import org.sonarqube.ws.MediaTypes;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.util.Collections.singletonList;
+import static java.util.Objects.requireNonNull;
+import static org.apache.commons.lang.StringUtils.substring;
+import static org.apache.commons.lang.StringUtils.substringAfterLast;
+import static org.apache.commons.lang.StringUtils.substringBeforeLast;
+import static org.sonar.server.ws.RequestVerifier.verifyRequest;
+import static org.sonar.server.ws.ServletRequest.SUPPORTED_MEDIA_TYPES_BY_URL_SUFFIX;
+import static org.sonar.server.ws.WsUtils.checkFound;
+
+/**
+ * @since 4.2
+ */
+@ServerSide
+public class WebServiceEngine implements LocalConnector, Startable {
+
+ private static final Logger LOGGER = Loggers.get(WebServiceEngine.class);
+
+ private final WebService[] webServices;
+
+ private WebService.Context context;
+
+ public WebServiceEngine(WebService[] webServices) {
+ this.webServices = webServices;
+ }
+
+ @Override
+ public void start() {
+ context = new WebService.Context();
+ for (WebService webService : webServices) {
+ webService.define(context);
+ }
+ }
+
+ @Override
+ public void stop() {
+ // nothing
+ }
+
+ private WebService.Context getContext() {
+ return requireNonNull(context, "Web services has not yet been initialized");
+ }
+
+ List<WebService.Controller> controllers() {
+ return getContext().controllers();
+ }
+
+ @Override
+ public LocalResponse call(LocalRequest request) {
+ DefaultLocalResponse localResponse = new DefaultLocalResponse();
+ execute(new LocalRequestAdapter(request), localResponse);
+ return localResponse;
+ }
+
+ public void execute(Request request, Response response) {
+ try {
+ ActionExtractor actionExtractor = new ActionExtractor(request.getPath());
+ WebService.Action action = getAction(actionExtractor);
+ checkFound(action, "Unknown url : %s", request.getPath());
+ if (request instanceof ValidatingRequest) {
+ ((ValidatingRequest) request).setAction(action);
+ ((ValidatingRequest) request).setLocalConnector(this);
+ }
+ checkActionExtension(actionExtractor.getExtension());
+ verifyRequest(action, request);
+ action.handler().handle(request, response);
+ } catch (IllegalArgumentException e) {
+ sendErrors(request, response, e, 400, singletonList(e.getMessage()));
+ } catch (BadRequestException e) {
+ sendErrors(request, response, e, 400, e.errors());
+ } catch (ServerException e) {
+ sendErrors(request, response, e, e.httpCode(), singletonList(e.getMessage()));
+ } catch (Exception e) {
+ sendErrors(request, response, e, 500, singletonList("An error has occurred. Please contact your administrator"));
+ }
+ }
+
+ @CheckForNull
+ private WebService.Action getAction(ActionExtractor actionExtractor) {
+ String controllerPath = actionExtractor.getController();
+ String actionKey = actionExtractor.getAction();
+ WebService.Controller controller = getContext().controller(controllerPath);
+ return controller == null ? null : controller.action(actionKey);
+ }
+
+ private static void sendErrors(Request request, Response response, Exception exception, int status, List<String> errors) {
+ if (isRequestAbortedByClient(exception)) {
+ // do not pollute logs. We can't do anything -> use DEBUG level
+ // see org.sonar.server.ws.ServletResponse#output()
+ LOGGER.debug(String.format("Request %s has been aborted by client", request), exception);
+ if (!isResponseCommitted(response)) {
+ // can be useful for access.log
+ response.stream().setStatus(299);
+ }
+ return;
+ }
+
+ if (status == 500) {
+ // Sending exception message into response is a vulnerability. Error must be
+ // displayed only in logs.
+ LOGGER.error("Fail to process request " + request, exception);
+ }
+
+ Response.Stream stream = response.stream();
+ if (isResponseCommitted(response)) {
+ // status can't be changed
+ LOGGER.debug(String.format("Request %s failed during response streaming", request), exception);
+ return;
+ }
+
+ // response is not committed, status and content can be changed to return the error
+ if (stream instanceof ServletResponse.ServletStream) {
+ ((ServletResponse.ServletStream) stream).reset();
+ }
+ stream.setStatus(status);
+ stream.setMediaType(MediaTypes.JSON);
+ try (JsonWriter json = JsonWriter.of(new OutputStreamWriter(stream.output(), StandardCharsets.UTF_8))) {
+ json.beginObject();
+ writeErrors(json, errors);
+ json.endObject();
+ } catch (Exception e) {
+ // Do not hide the potential exception raised in the try block.
+ throw Throwables.propagate(e);
+ }
+ }
+
+ private static boolean isRequestAbortedByClient(Exception exception) {
+ return Throwables.getCausalChain(exception).stream().anyMatch(t -> t instanceof ClientAbortException);
+ }
+
+ private static boolean isResponseCommitted(Response response) {
+ Response.Stream stream = response.stream();
+ // Request has been aborted by the client or the response was partially streamed, nothing can been done as Tomcat has committed the response
+ return stream instanceof ServletResponse.ServletStream && ((ServletResponse.ServletStream) stream).response().isCommitted();
+ }
+
+ public static void writeErrors(JsonWriter json, List<String> errorMessages) {
+ if (errorMessages.isEmpty()) {
+ return;
+ }
+ json.name("errors").beginArray();
+ errorMessages.forEach(message -> {
+ json.beginObject();
+ json.prop("msg", message);
+ json.endObject();
+ });
+ json.endArray();
+ }
+
+ private static void checkActionExtension(@Nullable String actionExtension) {
+ if (isNullOrEmpty(actionExtension)) {
+ return;
+ }
+ checkArgument(SUPPORTED_MEDIA_TYPES_BY_URL_SUFFIX.get(actionExtension.toLowerCase(Locale.ENGLISH)) != null, "Unknown action extension: %s", actionExtension);
+ }
+
+ private static class ActionExtractor {
+ private static final String SLASH = "/";
+ private static final String POINT = ".";
+
+ private final String controller;
+ private final String action;
+ private final String extension;
+ private final String path;
+
+ ActionExtractor(String path) {
+ this.path = path;
+ String pathWithoutExtension = substringBeforeLast(path, POINT);
+ this.controller = extractController(pathWithoutExtension);
+ this.action = substringAfterLast(pathWithoutExtension, SLASH);
+ checkArgument(!action.isEmpty(), "Url is incorrect : '%s'", path);
+ this.extension = substringAfterLast(path, POINT);
+ }
+
+ private static String extractController(String path) {
+ String controller = substringBeforeLast(path, SLASH);
+ if (controller.startsWith(SLASH)) {
+ return substring(controller, 1);
+ }
+ return controller;
+ }
+
+ String getController() {
+ return controller;
+ }
+
+ String getAction() {
+ return action;
+ }
+
+ @CheckForNull
+ String getExtension() {
+ return extension;
+ }
+
+ String getPath() {
+ return path;
+ }
+ }
+
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsAction.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsAction.java
new file mode 100644
index 00000000000..bb7ed1932d6
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsAction.java
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import org.sonar.api.server.ws.Definable;
+import org.sonar.api.server.ws.RequestHandler;
+import org.sonar.api.server.ws.WebService;
+
+/**
+ * Since 5.2, this interface is the base for Web Service marker interfaces
+ * Convention for naming implementations: <i>web_service_class_name</i>Action. ex: ProjectsWsAction, UsersWsAction
+ */
+public interface WsAction extends RequestHandler, Definable<WebService.NewController> {
+ // Marker interface
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsParameterBuilder.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsParameterBuilder.java
new file mode 100644
index 00000000000..291d9af0a5b
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsParameterBuilder.java
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import java.util.Locale;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import org.sonar.api.resources.ResourceType;
+import org.sonar.api.resources.ResourceTypes;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.core.i18n.I18n;
+
+import static java.lang.String.format;
+
+public class WsParameterBuilder {
+ private static final String PARAM_QUALIFIER = "qualifier";
+ private static final String PARAM_QUALIFIERS = "qualifiers";
+
+ private WsParameterBuilder() {
+ // static methods only
+ }
+
+ public static WebService.NewParam createRootQualifierParameter(WebService.NewAction action, QualifierParameterContext context) {
+ return action.createParam(PARAM_QUALIFIER)
+ .setDescription("Project qualifier. Filter the results with the specified qualifier. Possible values are:" + buildRootQualifiersDescription(context))
+ .setPossibleValues(getRootQualifiers(context.getResourceTypes()));
+ }
+
+ public static WebService.NewParam createRootQualifiersParameter(WebService.NewAction action, QualifierParameterContext context) {
+ return action.createParam(PARAM_QUALIFIERS)
+ .setDescription("Comma-separated list of component qualifiers. Filter the results with the specified qualifiers. " +
+ "Possible values are:" + buildRootQualifiersDescription(context))
+ .setPossibleValues(getRootQualifiers(context.getResourceTypes()));
+ }
+
+ public static WebService.NewParam createDefaultTemplateQualifierParameter(WebService.NewAction action, QualifierParameterContext context) {
+ return action.createParam(PARAM_QUALIFIER)
+ .setDescription("Project qualifier. Filter the results with the specified qualifier. Possible values are:" + buildDefaultTemplateQualifiersDescription(context))
+ .setPossibleValues(getDefaultTemplateQualifiers(context.getResourceTypes()));
+ }
+
+ public static WebService.NewParam createQualifiersParameter(WebService.NewAction action, QualifierParameterContext context) {
+ return action.createParam(PARAM_QUALIFIERS)
+ .setDescription(
+ "Comma-separated list of component qualifiers. Filter the results with the specified qualifiers. Possible values are:" + buildAllQualifiersDescription(context))
+ .setPossibleValues(getAllQualifiers(context.getResourceTypes()));
+ }
+
+ private static Set<String> getRootQualifiers(ResourceTypes resourceTypes) {
+ return resourceTypes.getRoots().stream()
+ .map(ResourceType::getQualifier)
+ .collect(Collectors.toCollection(TreeSet::new));
+ }
+
+ private static Set<String> getDefaultTemplateQualifiers(ResourceTypes resourceTypes) {
+ return resourceTypes.getRoots().stream()
+ .map(ResourceType::getQualifier)
+ .collect(Collectors.toCollection(TreeSet::new));
+ }
+
+ private static Set<String> getAllQualifiers(ResourceTypes resourceTypes) {
+ return resourceTypes.getAll().stream()
+ .map(ResourceType::getQualifier)
+ .collect(Collectors.toCollection(TreeSet::new));
+ }
+
+ private static String buildDefaultTemplateQualifiersDescription(QualifierParameterContext context) {
+ return buildQualifiersDescription(context, getDefaultTemplateQualifiers(context.getResourceTypes()));
+ }
+
+ private static String buildRootQualifiersDescription(QualifierParameterContext context) {
+ return buildQualifiersDescription(context, getRootQualifiers(context.getResourceTypes()));
+ }
+
+ private static String buildAllQualifiersDescription(QualifierParameterContext context) {
+ return buildQualifiersDescription(context, getAllQualifiers(context.getResourceTypes()));
+ }
+
+ private static String buildQualifiersDescription(QualifierParameterContext context, Set<String> qualifiers) {
+ StringBuilder description = new StringBuilder();
+ description.append("<ul>");
+ String qualifierPattern = "<li>%s - %s</li>";
+ for (String qualifier : qualifiers) {
+ description.append(format(qualifierPattern, qualifier, qualifierLabel(context, qualifier)));
+ }
+ description.append("</ul>");
+
+ return description.toString();
+ }
+
+ private static String qualifierLabel(QualifierParameterContext context, String qualifier) {
+ String qualifiersPropertyPrefix = "qualifiers.";
+ return context.getI18n().message(Locale.ENGLISH, qualifiersPropertyPrefix + qualifier, "no description available");
+ }
+
+ public static class QualifierParameterContext {
+ private final I18n i18n;
+ private final ResourceTypes resourceTypes;
+
+ private QualifierParameterContext(I18n i18n, ResourceTypes resourceTypes) {
+ this.i18n = i18n;
+ this.resourceTypes = resourceTypes;
+ }
+
+ public static QualifierParameterContext newQualifierParameterContext(I18n i18n, ResourceTypes resourceTypes) {
+ return new QualifierParameterContext(i18n, resourceTypes);
+ }
+
+ public I18n getI18n() {
+ return i18n;
+ }
+
+ public ResourceTypes getResourceTypes() {
+ return resourceTypes;
+ }
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsUtils.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsUtils.java
new file mode 100644
index 00000000000..2f936764f7d
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsUtils.java
@@ -0,0 +1,123 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableSet;
+import com.google.protobuf.Message;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.core.util.ProtobufJsonFormat;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.NotFoundException;
+
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.sonarqube.ws.MediaTypes.JSON;
+import static org.sonarqube.ws.MediaTypes.PROTOBUF;
+
+public class WsUtils {
+
+ private WsUtils() {
+ // only statics
+ }
+
+ public static void writeProtobuf(Message msg, Request request, Response response) {
+ OutputStream output = response.stream().output();
+ try {
+ if (request.getMediaType().equals(PROTOBUF)) {
+ response.stream().setMediaType(PROTOBUF);
+ msg.writeTo(output);
+ } else {
+ response.stream().setMediaType(JSON);
+ try (JsonWriter writer = JsonWriter.of(new OutputStreamWriter(output, UTF_8))) {
+ ProtobufJsonFormat.write(msg, writer);
+ }
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Error while writing protobuf message", e);
+ } finally {
+ IOUtils.closeQuietly(output);
+ }
+ }
+
+ /**
+ * @throws BadRequestException
+ */
+ public static void checkRequest(boolean expression, String message, Object... messageArguments) {
+ if (!expression) {
+ throw BadRequestException.create(format(message, messageArguments));
+ }
+ }
+
+ public static void checkRequest(boolean expression, List<String> messages) {
+ if (!expression) {
+ throw BadRequestException.create(messages);
+ }
+ }
+
+ /**
+ * @throws NotFoundException if the value if null
+ * @return the value
+ */
+ public static <T> T checkFound(@Nullable T value, String message, Object... messageArguments) {
+ if (value == null) {
+ throw new NotFoundException(format(message, messageArguments));
+ }
+
+ return value;
+ }
+
+ /**
+ * @throws NotFoundException if the value is not present
+ * @return the value
+ */
+ public static <T> T checkFoundWithOptional(Optional<T> value, String message, Object... messageArguments) {
+ if (!value.isPresent()) {
+ throw new NotFoundException(format(message, messageArguments));
+ }
+
+ return value.get();
+ }
+
+ public static <T> T checkFoundWithOptional(java.util.Optional<T> value, String message, Object... messageArguments) {
+ if (!value.isPresent()) {
+ throw new NotFoundException(format(message, messageArguments));
+ }
+
+ return value.get();
+ }
+
+ public static <T> T checkStateWithOptional(java.util.Optional<T> value, String message, Object... messageArguments) {
+ if (!value.isPresent()) {
+ throw new IllegalStateException(format(message, messageArguments));
+ }
+
+ return value.get();
+ }
+}
diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/package-info.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/package-info.java
new file mode 100644
index 00000000000..464c533bab7
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.ws;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-ws/src/main/resources/org/sonar/server/ws/removed-ws-example.json b/server/sonar-webserver-ws/src/main/resources/org/sonar/server/ws/removed-ws-example.json
new file mode 100644
index 00000000000..6764a133ed3
--- /dev/null
+++ b/server/sonar-webserver-ws/src/main/resources/org/sonar/server/ws/removed-ws-example.json
@@ -0,0 +1,7 @@
+{
+ "errors": [
+ {
+ "msg": "The web service '/api/...' doesn't exists anymore, please read its documentation to use alternatives"
+ }
+ ]
+}
diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/CacheWriterTest.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/CacheWriterTest.java
new file mode 100644
index 00000000000..355d4762432
--- /dev/null
+++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/CacheWriterTest.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class CacheWriterTest {
+ private Writer writer = new StringWriter();
+ private CacheWriter underTest = new CacheWriter(writer);
+
+ @Test
+ public void write_content_when_closing_resource() throws IOException {
+ underTest.write("content");
+ assertThat(writer.toString()).isEmpty();
+
+ underTest.close();
+
+ assertThat(writer.toString()).isEqualTo("content");
+ }
+
+ @Test
+ public void close_encapsulated_writer_once() throws IOException {
+ writer = mock(Writer.class);
+ underTest = new CacheWriter(writer);
+
+ underTest.close();
+ underTest.close();
+
+ verify(writer, times(1)).close();
+ }
+}
diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/DumbResponse.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/DumbResponse.java
new file mode 100644
index 00000000000..f7d2aeedd80
--- /dev/null
+++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/DumbResponse.java
@@ -0,0 +1,133 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.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.Response;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.api.utils.text.XmlWriter;
+
+public class DumbResponse implements Response {
+ private InMemoryStream stream;
+
+ private final ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+ private Map<String, String> headers = Maps.newHashMap();
+
+ 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;
+ }
+
+ public String outputAsString() {
+ return new String(output.toByteArray(), StandardCharsets.UTF_8);
+ }
+ }
+
+ @Override
+ public JsonWriter newJsonWriter() {
+ return JsonWriter.of(new OutputStreamWriter(output, StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public XmlWriter newXmlWriter() {
+ return XmlWriter.of(new OutputStreamWriter(output, StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public InMemoryStream stream() {
+ if (stream == null) {
+ stream = new InMemoryStream();
+ }
+ 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;
+ }
+
+ public Collection<String> getHeaderNames() {
+ return headers.keySet();
+ }
+
+ @CheckForNull
+ 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-webserver-ws/src/test/java/org/sonar/server/ws/RemovedWebServiceHandlerTest.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/RemovedWebServiceHandlerTest.java
new file mode 100644
index 00000000000..262a8325845
--- /dev/null
+++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/RemovedWebServiceHandlerTest.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.server.ws.Request;
+import org.sonar.server.exceptions.ServerException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class RemovedWebServiceHandlerTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void throw_server_exception() throws Exception {
+ Request request = mock(Request.class);
+ when(request.getPath()).thenReturn("/api/resources/index");
+
+ try {
+ RemovedWebServiceHandler.INSTANCE.handle(request, null);
+ fail();
+ } catch (ServerException e) {
+ assertThat(e.getMessage()).isEqualTo("The web service '/api/resources/index' doesn't exist anymore, please read its documentation to use alternatives");
+ assertThat(e.httpCode()).isEqualTo(410);
+ }
+ }
+}
diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletRequestTest.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletRequestTest.java
new file mode 100644
index 00000000000..502dfa9dec7
--- /dev/null
+++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletRequestTest.java
@@ -0,0 +1,217 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.net.HttpHeaders;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.Part;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonarqube.ws.MediaTypes;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ServletRequestTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private HttpServletRequest source = mock(HttpServletRequest.class);
+
+ private ServletRequest underTest = new ServletRequest(source);
+
+ @Test
+ public void call_method() {
+ underTest.method();
+
+ verify(source).getMethod();
+ }
+
+ @Test
+ public void getMediaType() {
+ when(source.getHeader(HttpHeaders.ACCEPT)).thenReturn(MediaTypes.JSON);
+ when(source.getRequestURI()).thenReturn("/path/to/resource/search");
+
+ assertThat(underTest.getMediaType()).isEqualTo(MediaTypes.JSON);
+ }
+
+ @Test
+ public void default_media_type_is_octet_stream() {
+ when(source.getRequestURI()).thenReturn("/path/to/resource/search");
+
+ assertThat(underTest.getMediaType()).isEqualTo(MediaTypes.DEFAULT);
+ }
+
+ @Test
+ public void media_type_taken_in_url_first() {
+ when(source.getHeader(HttpHeaders.ACCEPT)).thenReturn(MediaTypes.JSON);
+ when(source.getRequestURI()).thenReturn("/path/to/resource/search.protobuf");
+
+ assertThat(underTest.getMediaType()).isEqualTo(MediaTypes.PROTOBUF);
+ }
+
+ @Test
+ public void has_param_from_source() {
+ when(source.getParameterMap()).thenReturn(ImmutableMap.of("param", new String[] {"value"}));
+ ServletRequest request = new ServletRequest(source);
+ assertThat(request.hasParam("param")).isTrue();
+ }
+
+ @Test
+ public void read_param_from_source() {
+ when(source.getParameter("param")).thenReturn("value");
+
+ assertThat(underTest.readParam("param")).isEqualTo("value");
+ }
+
+ @Test
+ public void read_multi_param_from_source_with_values() {
+ when(source.getParameterValues("param")).thenReturn(new String[]{"firstValue", "secondValue", "thirdValue"});
+
+ List<String> result = underTest.readMultiParam("param");
+
+ assertThat(result).containsExactly("firstValue", "secondValue", "thirdValue");
+ }
+
+ @Test
+ public void read_multi_param_from_source_with_one_value() {
+ when(source.getParameterValues("param")).thenReturn(new String[]{"firstValue"});
+
+ List<String> result = underTest.readMultiParam("param");
+
+ assertThat(result).containsExactly("firstValue");
+ }
+
+ @Test
+ public void read_multi_param_from_source_without_value() {
+ when(source.getParameterValues("param")).thenReturn(null);
+
+ List<String> result = underTest.readMultiParam("param");
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void read_input_stream() throws Exception {
+ when(source.getContentType()).thenReturn("multipart/form-data");
+ InputStream file = mock(InputStream.class);
+ Part part = mock(Part.class);
+ when(part.getInputStream()).thenReturn(file);
+ when(part.getSize()).thenReturn(10L);
+ when(source.getPart("param1")).thenReturn(part);
+
+ assertThat(underTest.readInputStreamParam("param1")).isEqualTo(file);
+ assertThat(underTest.readInputStreamParam("param2")).isNull();
+ }
+
+ @Test
+ public void read_no_input_stream_when_part_size_is_zero() throws Exception {
+ when(source.getContentType()).thenReturn("multipart/form-data");
+ InputStream file = mock(InputStream.class);
+ Part part = mock(Part.class);
+ when(part.getInputStream()).thenReturn(file);
+ when(part.getSize()).thenReturn(0L);
+ when(source.getPart("param1")).thenReturn(part);
+
+ assertThat(underTest.readInputStreamParam("param1")).isNull();
+ }
+
+ @Test
+ public void return_no_input_stream_when_content_type_is_not_multipart() {
+ when(source.getContentType()).thenReturn("multipart/form-data");
+
+ assertThat(underTest.readInputStreamParam("param1")).isNull();
+ }
+
+ @Test
+ public void return_no_input_stream_when_content_type_is_null() {
+ when(source.getContentType()).thenReturn(null);
+
+ assertThat(underTest.readInputStreamParam("param1")).isNull();
+ }
+
+ @Test
+ public void returns_null_when_invalid_part() throws Exception {
+ when(source.getContentType()).thenReturn("multipart/form-data");
+ InputStream file = mock(InputStream.class);
+ Part part = mock(Part.class);
+ when(part.getSize()).thenReturn(0L);
+ when(part.getInputStream()).thenReturn(file);
+ doThrow(IllegalArgumentException.class).when(source).getPart("param1");
+
+ assertThat(underTest.readInputStreamParam("param1")).isNull();
+ }
+
+ @Test
+ public void getPath() {
+ when(source.getRequestURI()).thenReturn("/sonar/path/to/resource/search");
+ when(source.getContextPath()).thenReturn("/sonar");
+
+ assertThat(underTest.getPath()).isEqualTo("/path/to/resource/search");
+ }
+
+ @Test
+ public void to_string() {
+ when(source.getRequestURL()).thenReturn(new StringBuffer("http:localhost:9000/api/issues"));
+ assertThat(underTest.toString()).isEqualTo("http:localhost:9000/api/issues");
+
+ when(source.getQueryString()).thenReturn("components=sonar");
+
+ assertThat(underTest.toString()).isEqualTo("http:localhost:9000/api/issues?components=sonar");
+ }
+
+ @Test
+ public void header_returns_the_value_of_http_header() {
+ when(source.getHeader("Accept")).thenReturn("text/plain");
+ assertThat(underTest.header("Accept")).hasValue("text/plain");
+ }
+
+ @Test
+ public void header_is_empty_if_absent_from_request() {
+ when(source.getHeader("Accept")).thenReturn(null);
+ assertThat(underTest.header("Accept")).isEmpty();
+ }
+
+ @Test
+ public void header_has_empty_value_if_present_in_request_without_value() {
+ when(source.getHeader("Accept")).thenReturn("");
+ assertThat(underTest.header("Accept")).hasValue("");
+ }
+
+ @Test
+ public void getReader() throws IOException {
+ BufferedReader reader = new BufferedReader(new StringReader("foo"));
+ when(source.getReader()).thenReturn(reader);
+
+ assertThat(underTest.getReader()).isEqualTo(reader);
+ }
+
+}
diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletResponseTest.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletResponseTest.java
new file mode 100644
index 00000000000..aa4cda9ca7b
--- /dev/null
+++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletResponseTest.java
@@ -0,0 +1,126 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonarqube.ws.MediaTypes.JSON;
+import static org.sonarqube.ws.MediaTypes.XML;
+
+public class ServletResponseTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private ServletOutputStream output = mock(ServletOutputStream.class);
+ private HttpServletResponse response = mock(HttpServletResponse.class);
+
+ private ServletResponse underTest = new ServletResponse(response);
+
+ @Before
+ public void setUp() throws Exception {
+ when(response.getOutputStream()).thenReturn(output);
+ }
+
+ @Test
+ public void test_default_header() {
+ verify(response).setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ }
+
+ @Test
+ public void set_header() {
+ underTest.setHeader("header", "value");
+
+ verify(response).setHeader("header", "value");
+ }
+
+ @Test
+ public void get_header() {
+ underTest.getHeader("header");
+
+ verify(response).getHeader("header");
+ }
+
+ @Test
+ public void get_header_names() {
+ underTest.getHeaderNames();
+
+ verify(response).getHeaderNames();
+ }
+
+ @Test
+ public void test_default_status() {
+ verify(response).setStatus(200);
+ }
+
+ @Test
+ public void set_status() {
+ underTest.stream().setStatus(404);
+
+ verify(response).setStatus(404);
+ }
+
+ @Test
+ public void test_output() {
+ assertThat(underTest.stream().output()).isEqualTo(output);
+ }
+
+
+ @Test
+ public void test_reset() {
+ underTest.stream().reset();
+
+ verify(response).reset();
+ }
+
+ @Test
+ public void test_newJsonWriter() throws Exception {
+ underTest.newJsonWriter();
+
+ verify(response).setContentType(JSON);
+ verify(response).getOutputStream();
+ }
+
+ @Test
+ public void test_newXmlWriter() throws Exception {
+ underTest.newXmlWriter();
+
+ verify(response).setContentType(XML);
+ verify(response).getOutputStream();
+ }
+
+ @Test
+ public void test_noContent() throws Exception {
+ underTest.noContent();
+
+ verify(response).setStatus(204);
+ verify(response, never()).getOutputStream();
+ }
+}
diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/TestRequest.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/TestRequest.java
new file mode 100644
index 00000000000..aea0e0d69bd
--- /dev/null
+++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/TestRequest.java
@@ -0,0 +1,198 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
+import com.google.protobuf.GeneratedMessageV3;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.impl.ws.PartImpl;
+import org.sonar.api.impl.ws.ValidatingRequest;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.Objects.requireNonNull;
+import static org.sonarqube.ws.MediaTypes.PROTOBUF;
+
+public class TestRequest extends ValidatingRequest {
+
+ private final ListMultimap<String, String> multiParams = ArrayListMultimap.create();
+ private final Map<String, String> params = new HashMap<>();
+ private final Map<String, String> headers = new HashMap<>();
+ private final Map<String, Part> parts = Maps.newHashMap();
+ private String payload = "";
+ private boolean payloadConsumed = false;
+ private String method = "GET";
+ private String mimeType = "application/octet-stream";
+ private String path;
+
+ @Override
+ public BufferedReader getReader() {
+ checkState(!payloadConsumed, "Payload already consumed");
+ if (payload == null) {
+ return super.getReader();
+ }
+
+ BufferedReader res = new BufferedReader(new StringReader(payload));
+ payloadConsumed = true;
+ return res;
+ }
+
+ public TestRequest setPayload(String payload) {
+ checkState(!payloadConsumed, "Payload already consumed");
+
+ this.payload = payload;
+ return this;
+ }
+
+ @Override
+ protected String readParam(String key) {
+ return params.get(key);
+ }
+
+ @Override
+ protected List<String> readMultiParam(String key) {
+ return multiParams.get(key);
+ }
+
+ @Override
+ protected InputStream readInputStreamParam(String key) {
+ String value = readParam(key);
+ if (value == null) {
+ return null;
+ }
+ return IOUtils.toInputStream(value);
+ }
+
+ @Override
+ protected Part readPart(String key) {
+ return parts.get(key);
+ }
+
+ public TestRequest setPart(String key, InputStream input, String fileName) {
+ parts.put(key, new PartImpl(input, fileName));
+ return this;
+ }
+
+ @Override
+ public String method() {
+ return method;
+ }
+
+ @Override
+ public boolean hasParam(String key) {
+ return params.containsKey(key) || multiParams.containsKey(key);
+ }
+
+ @Override
+ public String getPath() {
+ return path;
+ }
+
+ @Override
+ public Map<String, String[]> getParams() {
+ ArrayListMultimap<String, String> result = ArrayListMultimap.create(multiParams);
+ params.forEach(result::put);
+ return result.asMap().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toArray(new String[0])));
+ }
+
+ public TestRequest setPath(String path) {
+ this.path = path;
+ return this;
+ }
+
+ public TestRequest setMethod(String method) {
+ checkNotNull(method);
+ this.method = method;
+ return this;
+ }
+
+ @Override
+ public String getMediaType() {
+ return mimeType;
+ }
+
+ public TestRequest setMediaType(String type) {
+ checkNotNull(type);
+ this.mimeType = type;
+ return this;
+ }
+
+ public TestRequest setParam(String key, String value) {
+ checkNotNull(key);
+ checkNotNull(value);
+ this.params.put(key, value);
+ return this;
+ }
+
+ public TestRequest setMultiParam(String key, List<String> values) {
+ requireNonNull(key);
+ requireNonNull(values);
+
+ multiParams.putAll(key, values);
+
+ return this;
+ }
+
+ @Override
+ public Map<String, String> getHeaders() {
+ return ImmutableMap.copyOf(headers);
+ }
+
+ @Override
+ public Optional<String> header(String name) {
+ return Optional.ofNullable(headers.get(name));
+ }
+
+ public TestRequest setHeader(String name, String value) {
+ headers.put(requireNonNull(name), requireNonNull(value));
+ return this;
+ }
+
+ public TestResponse execute() {
+ try {
+ DumbResponse response = new DumbResponse();
+ action().handler().handle(this, response);
+ return new TestResponse(response);
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ public <T extends GeneratedMessageV3> T executeProtobuf(Class<T> protobufClass) {
+ return setMediaType(PROTOBUF).execute().getInputObject(protobufClass);
+ }
+
+ @Override
+ public String toString() {
+ return path;
+ }
+}
diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/TestResponse.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/TestResponse.java
new file mode 100644
index 00000000000..82057f06af0
--- /dev/null
+++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/TestResponse.java
@@ -0,0 +1,91 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import com.google.protobuf.GeneratedMessageV3;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import javax.annotation.CheckForNull;
+import org.sonar.test.JsonAssert;
+
+public class TestResponse {
+
+ private final DumbResponse dumbResponse;
+
+ TestResponse(DumbResponse dumbResponse) {
+ this.dumbResponse = dumbResponse;
+ }
+
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(dumbResponse.getFlushedOutput());
+ }
+
+ public <T extends GeneratedMessageV3> T getInputObject(Class<T> protobufClass) {
+ try (InputStream input = getInputStream()) {
+ Method parseFromMethod = protobufClass.getMethod("parseFrom", InputStream.class);
+ @SuppressWarnings("unchecked")
+ T result = (T) parseFromMethod.invoke(null, input);
+ return result;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public String getInput() {
+ return new String(dumbResponse.getFlushedOutput(), StandardCharsets.UTF_8);
+ }
+
+ public String getMediaType() {
+ return dumbResponse.stream().mediaType();
+ }
+
+ public int getStatus() {
+ return dumbResponse.stream().status();
+ }
+
+ @CheckForNull
+ public String getHeader(String headerKey) {
+ return dumbResponse.getHeader(headerKey);
+ }
+
+ public void assertJson(String expectedJson) {
+ JsonAssert.assertJson(getInput()).isSimilarTo(expectedJson);
+ }
+
+ /**
+ * Compares JSON response with JSON file available in classpath. For example if class
+ * is org.foo.BarTest and filename is index.json, then file must be located
+ * at src/test/resources/org/foo/BarTest/index.json.
+ *
+ * @param clazz the test class
+ * @param expectedJsonFilename name of the file containing the expected JSON
+ */
+ public void assertJson(Class clazz, String expectedJsonFilename) {
+ String path = clazz.getSimpleName() + "/" + expectedJsonFilename;
+ URL url = clazz.getResource(path);
+ if (url == null) {
+ throw new IllegalStateException("Cannot find " + path);
+ }
+ JsonAssert.assertJson(getInput()).isSimilarTo(url);
+ }
+}
diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java
new file mode 100644
index 00000000000..160fce7540f
--- /dev/null
+++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java
@@ -0,0 +1,483 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import java.util.function.Consumer;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.catalina.connector.ClientAbortException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.Mockito;
+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.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonarqube.ws.MediaTypes;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.lang.StringUtils.substringAfterLast;
+import static org.apache.commons.lang.StringUtils.substringBeforeLast;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class WebServiceEngineTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void load_ws_definitions_at_startup() {
+ WebServiceEngine underTest = new WebServiceEngine(new WebService[] {
+ newWs("api/foo/index", a -> {
+ }),
+ newWs("api/bar/index", a -> {
+ })
+ });
+ underTest.start();
+ try {
+ assertThat(underTest.controllers())
+ .extracting(WebService.Controller::path)
+ .containsExactlyInAnyOrder("api/foo", "api/bar");
+ } finally {
+ underTest.stop();
+ }
+ }
+
+ @Test
+ public void ws_returns_successful_response() {
+ Request request = new TestRequest().setPath("/api/ping");
+
+ DumbResponse response = run(request, newPingWs(a -> {
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("pong");
+ assertThat(response.stream().status()).isEqualTo(200);
+ }
+
+ @Test
+ public void accept_path_that_does_not_start_with_slash() {
+ Request request = new TestRequest().setPath("api/ping");
+
+ DumbResponse response = run(request, newPingWs(a -> {
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("pong");
+ assertThat(response.stream().status()).isEqualTo(200);
+ }
+
+ @Test
+ public void request_path_can_contain_valid_media_type() {
+ Request request = new TestRequest().setPath("api/ping.json");
+
+ DumbResponse response = run(request, newPingWs(a -> {
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("pong");
+ assertThat(response.stream().status()).isEqualTo(200);
+ }
+
+ @Test
+ public void bad_request_if_action_suffix_is_not_supported() {
+ Request request = new TestRequest().setPath("/api/ping.bat");
+
+ DumbResponse response = run(request, newPingWs(a -> {
+ }));
+
+ assertThat(response.stream().status()).isEqualTo(400);
+ assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"Unknown action extension: bat\"}]}");
+ }
+
+ @Test
+ public void test_response_with_no_content() {
+ Request request = new TestRequest().setPath("api/foo");
+
+ RequestHandler handler = (req, resp) -> resp.noContent();
+ DumbResponse response = run(request, newWs("api/foo", a -> a.setHandler(handler)));
+
+ assertThat(response.stream().outputAsString()).isEmpty();
+ assertThat(response.stream().status()).isEqualTo(204);
+ }
+
+ @Test
+ public void return_404_if_controller_does_not_exist() {
+ Request request = new TestRequest().setPath("xxx/ping");
+
+ DumbResponse response = run(request, newPingWs(a -> {
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"Unknown url : xxx/ping\"}]}");
+ assertThat(response.stream().status()).isEqualTo(404);
+ }
+
+ @Test
+ public void return_404_if_action_does_not_exist() {
+ Request request = new TestRequest().setPath("api/xxx");
+
+ DumbResponse response = run(request, newPingWs(a -> {
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"Unknown url : api/xxx\"}]}");
+ assertThat(response.stream().status()).isEqualTo(404);
+ }
+
+ @Test
+ public void fail_if_method_GET_is_not_allowed() {
+ Request request = new TestRequest().setMethod("GET").setPath("api/foo");
+
+ DumbResponse response = run(request, newWs("api/foo", a -> a.setPost(true)));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"HTTP method POST is required\"}]}");
+ assertThat(response.stream().status()).isEqualTo(405);
+ }
+
+ @Test
+ public void POST_is_considered_as_GET_if_POST_is_not_supported() {
+ Request request = new TestRequest().setMethod("POST").setPath("api/ping");
+
+ DumbResponse response = run(request, newPingWs(a -> {
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("pong");
+ assertThat(response.stream().status()).isEqualTo(200);
+ }
+
+ @Test
+ public void method_PUT_is_not_allowed() {
+ Request request = new TestRequest().setMethod("PUT").setPath("/api/ping");
+
+ DumbResponse response = run(request, newPingWs(a -> {
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"HTTP method PUT is not allowed\"}]}");
+ assertThat(response.stream().status()).isEqualTo(405);
+ }
+
+ @Test
+ public void method_DELETE_is_not_allowed() {
+ Request request = new TestRequest().setMethod("DELETE").setPath("api/ping");
+
+ DumbResponse response = run(request, newPingWs(a -> {
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"HTTP method DELETE is not allowed\"}]}");
+ assertThat(response.stream().status()).isEqualTo(405);
+ }
+
+ @Test
+ public void method_POST_is_required() {
+ Request request = new TestRequest().setMethod("POST").setPath("api/ping");
+
+ DumbResponse response = run(request, newPingWs(a -> a.setPost(true)));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("pong");
+ assertThat(response.stream().status()).isEqualTo(200);
+ }
+
+ @Test
+ public void fail_if_reading_an_undefined_parameter() {
+ Request request = new TestRequest().setPath("api/foo").setParam("unknown", "Unknown");
+
+ DumbResponse response = run(request, newWs("api/foo", a -> a.setHandler((req, resp) -> request.param("unknown"))));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"BUG - parameter 'unknown' is undefined for action 'foo'\"}]}");
+ assertThat(response.stream().status()).isEqualTo(400);
+ }
+
+ @Test
+ public void fail_if_request_does_not_have_required_parameter() {
+ Request request = new TestRequest().setPath("api/foo").setParam("unknown", "Unknown");
+
+ DumbResponse response = run(request, newWs("api/foo", a -> {
+ a.createParam("bar").setRequired(true);
+ a.setHandler((req, resp) -> request.mandatoryParam("bar"));
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"The 'bar' parameter is missing\"}]}");
+ assertThat(response.stream().status()).isEqualTo(400);
+ }
+
+ @Test
+ public void fail_if_request_does_not_have_required_parameter_even_if_handler_does_not_require_it() {
+ Request request = new TestRequest().setPath("api/foo").setParam("unknown", "Unknown");
+
+ DumbResponse response = run(request, newWs("api/foo", a -> {
+ a.createParam("bar").setRequired(true);
+ // do not use mandatoryParam("bar")
+ a.setHandler((req, resp) -> request.param("bar"));
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"The 'bar' parameter is missing\"}]}");
+ assertThat(response.stream().status()).isEqualTo(400);
+ }
+
+ @Test
+ public void use_default_value_of_optional_parameter() {
+ Request request = new TestRequest().setPath("api/print");
+
+ DumbResponse response = run(request, newWs("api/print", a -> {
+ a.createParam("message").setDefaultValue("hello");
+ a.setHandler((req, resp) -> resp.stream().output().write(req.param("message").getBytes(UTF_8)));
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("hello");
+ assertThat(response.stream().status()).isEqualTo(200);
+ }
+
+ @Test
+ public void use_request_parameter_on_parameter_with_default_value() {
+ Request request = new TestRequest().setPath("api/print").setParam("message", "bar");
+
+ DumbResponse response = run(request, newWs("api/print", a -> {
+ a.createParam("message").setDefaultValue("default_value");
+ a.setHandler((req, resp) -> resp.stream().output().write(req.param("message").getBytes(UTF_8)));
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("bar");
+ assertThat(response.stream().status()).isEqualTo(200);
+ }
+
+ @Test
+ public void accept_parameter_value_within_defined_possible_values() {
+ Request request = new TestRequest().setPath("api/foo").setParam("format", "json");
+
+ DumbResponse response = run(request, newWs("api/foo", a -> {
+ a.createParam("format").setPossibleValues("json", "xml");
+ a.setHandler((req, resp) -> resp.stream().output().write(req.mandatoryParam("format").getBytes(UTF_8)));
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("json");
+ assertThat(response.stream().status()).isEqualTo(200);
+ }
+
+ @Test
+ public void fail_if_parameter_value_is_not_in_defined_possible_values() {
+ Request request = new TestRequest().setPath("api/foo").setParam("format", "yml");
+
+ DumbResponse response = run(request, newWs("api/foo", a -> {
+ a.createParam("format").setPossibleValues("json", "xml");
+ a.setHandler((req, resp) -> resp.stream().output().write(req.mandatoryParam("format").getBytes(UTF_8)));
+ }));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"Value of parameter 'format' (yml) must be one of: [json, xml]\"}]}");
+ assertThat(response.stream().status()).isEqualTo(400);
+ }
+
+ @Test
+ public void return_500_on_internal_error() {
+ Request request = new TestRequest().setPath("api/foo");
+
+ DumbResponse response = run(request, newFailWs());
+
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"An error has occurred. Please contact your administrator\"}]}");
+ assertThat(response.stream().status()).isEqualTo(500);
+ assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+ assertThat(logTester.logs(LoggerLevel.ERROR)).filteredOn(l -> l.contains("Fail to process request api/foo")).isNotEmpty();
+ }
+
+ @Test
+ public void return_400_on_BadRequestException_with_single_message() {
+ Request request = new TestRequest().setPath("api/foo");
+
+ DumbResponse response = run(request, newWs("api/foo", a -> a.setHandler((req, resp) -> {
+ throw BadRequestException.create("Bad request !");
+ })));
+
+ assertThat(response.stream().outputAsString()).isEqualTo(
+ "{\"errors\":[{\"msg\":\"Bad request !\"}]}");
+ assertThat(response.stream().status()).isEqualTo(400);
+ assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+ assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
+ }
+
+ @Test
+ public void return_400_on_BadRequestException_with_multiple_messages() {
+ Request request = new TestRequest().setPath("api/foo");
+
+ DumbResponse response = run(request, newWs("api/foo", a -> a.setHandler((req, resp) -> {
+ throw BadRequestException.create("one", "two", "three");
+ })));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":["
+ + "{\"msg\":\"one\"},"
+ + "{\"msg\":\"two\"},"
+ + "{\"msg\":\"three\"}"
+ + "]}");
+ assertThat(response.stream().status()).isEqualTo(400);
+ assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+ assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
+ }
+
+ @Test
+ public void return_error_message_containing_character_percent() {
+ Request request = new TestRequest().setPath("api/foo");
+
+ DumbResponse response = run(request, newWs("api/foo", a -> a.setHandler((req, resp) -> {
+ throw new IllegalArgumentException("this should not fail %s");
+ })));
+
+ assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"this should not fail %s\"}]}");
+ assertThat(response.stream().status()).isEqualTo(400);
+ assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+ }
+
+ @Test
+ public void send_response_headers() {
+ Request request = new TestRequest().setPath("api/foo");
+
+ DumbResponse response = run(request, newWs("api/foo", a -> a.setHandler((req, resp) -> resp.setHeader("Content-Disposition", "attachment; filename=foo.zip"))));
+
+ assertThat(response.getHeader("Content-Disposition")).isEqualTo("attachment; filename=foo.zip");
+ }
+
+ @Test
+ public void support_aborted_request_when_response_is_already_committed() {
+ Request request = new TestRequest().setPath("api/foo");
+ Response response = mockServletResponse(true);
+
+ run(request, response, newClientAbortWs());
+
+ // response is committed (status is already sent), so status can't be changed
+ verify(response.stream(), never()).setStatus(anyInt());
+
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Request api/foo has been aborted by client");
+ }
+
+ @Test
+ public void support_aborted_request_when_response_is_not_committed() {
+ Request request = new TestRequest().setPath("api/foo");
+ Response response = mockServletResponse(false);
+
+ run(request, response, newClientAbortWs());
+
+ verify(response.stream()).setStatus(299);
+ assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Request api/foo has been aborted by client");
+ }
+
+ @Test
+ public void internal_error_when_response_is_already_committed() {
+ Request request = new TestRequest().setPath("api/foo");
+ Response response = mockServletResponse(true);
+
+ run(request, response, newFailWs());
+
+ // response is committed (status is already sent), so status can't be changed
+ verify(response.stream(), never()).setStatus(anyInt());
+ assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Fail to process request api/foo");
+ }
+
+ @Test
+ public void internal_error_when_response_is_not_committed() {
+ Request request = new TestRequest().setPath("api/foo");
+ Response response = mockServletResponse(false);
+
+ run(request, response, newFailWs());
+
+ verify(response.stream()).setStatus(500);
+ assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Fail to process request api/foo");
+ }
+
+ @Test
+ public void fail_when_start_in_not_called() {
+ Request request = new TestRequest().setPath("/api/ping");
+ DumbResponse response = new DumbResponse();
+ WebServiceEngine underTest = new WebServiceEngine(new WebService[] {newPingWs(a -> {
+ })});
+
+ underTest.execute(request, response);
+
+ assertThat(logTester.logs(LoggerLevel.ERROR)).contains("Fail to process request /api/ping");
+ }
+
+ private static WebService newWs(String path, Consumer<WebService.NewAction> consumer) {
+ return context -> {
+ WebService.NewController controller = context.createController(substringBeforeLast(path, "/"));
+ WebService.NewAction action = createNewDefaultAction(controller, substringAfterLast(path, "/"));
+ action.setHandler((request, response) -> {
+ });
+ consumer.accept(action);
+ controller.done();
+ };
+ }
+
+ private static WebService newPingWs(Consumer<WebService.NewAction> consumer) {
+ return newWs("api/ping", a -> {
+ a.setHandler((request, response) -> response.stream().output().write("pong".getBytes(UTF_8)));
+ consumer.accept(a);
+ });
+ }
+
+ private static WebService newFailWs() {
+ return newWs("api/foo", a -> a.setHandler((req, resp) -> {
+ throw new IllegalStateException("BOOM");
+ }));
+ }
+
+ private static DumbResponse run(Request request, WebService... webServices) {
+ DumbResponse response = new DumbResponse();
+ return (DumbResponse) run(request, response, webServices);
+ }
+
+ private static Response run(Request request, Response response, WebService... webServices) {
+ WebServiceEngine underTest = new WebServiceEngine(webServices);
+ underTest.start();
+ try {
+ underTest.execute(request, response);
+ return response;
+ } finally {
+ underTest.stop();
+ }
+ }
+
+ private static Response mockServletResponse(boolean committed) {
+ Response response = mock(Response.class, Mockito.RETURNS_DEEP_STUBS);
+ ServletResponse.ServletStream servletStream = mock(ServletResponse.ServletStream.class, Mockito.RETURNS_DEEP_STUBS);
+ when(response.stream()).thenReturn(servletStream);
+ HttpServletResponse httpServletResponse = mock(HttpServletResponse.class, Mockito.RETURNS_DEEP_STUBS);
+ when(httpServletResponse.isCommitted()).thenReturn(committed);
+ when(servletStream.response()).thenReturn(httpServletResponse);
+ return response;
+ }
+
+ private static WebService newClientAbortWs() {
+ return newWs("api/foo", a -> a.setHandler((req, resp) -> {
+ throw new ClientAbortException();
+ }));
+ }
+
+ private static WebService.NewAction createNewDefaultAction(WebService.NewController controller, String key) {
+ return controller
+ .createAction(key)
+ .setDescription("Dummy Description")
+ .setSince("5.3")
+ .setResponseExample(WebServiceEngineTest.class.getResource("web-service-engine-test.txt"));
+ }
+}
diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsActionTester.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsActionTester.java
new file mode 100644
index 00000000000..fede932156e
--- /dev/null
+++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsActionTester.java
@@ -0,0 +1,47 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import com.google.common.collect.Iterables;
+import org.sonar.api.server.ws.WebService;
+
+public class WsActionTester {
+
+ public static final String CONTROLLER_KEY = "test";
+ private final WebService.Action action;
+
+ public WsActionTester(WsAction wsAction) {
+ WebService.Context context = new WebService.Context();
+ WebService.NewController newController = context.createController(CONTROLLER_KEY);
+ wsAction.define(newController);
+ newController.done();
+ action = Iterables.get(context.controller(CONTROLLER_KEY).actions(), 0);
+ }
+
+ public WebService.Action getDef() {
+ return action;
+ }
+
+ public TestRequest newRequest() {
+ TestRequest request = new TestRequest();
+ request.setAction(action);
+ return request;
+ }
+}
diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsTester.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsTester.java
new file mode 100644
index 00000000000..e69153b5033
--- /dev/null
+++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsTester.java
@@ -0,0 +1,361 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import com.google.common.collect.Maps;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.impl.ws.PartImpl;
+import org.sonar.api.impl.ws.ValidatingRequest;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.api.utils.text.XmlWriter;
+import org.sonar.server.ws.WsTester.TestResponse.TestStream;
+import org.sonar.test.JsonAssert;
+import org.sonarqube.ws.MediaTypes;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.Objects.requireNonNull;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.ws.RequestVerifier.verifyRequest;
+
+/**
+ * @since 4.2
+ * @deprecated use {@link WsActionTester} in preference to this class
+ */
+@Deprecated
+public class WsTester {
+
+ public static class TestRequest extends ValidatingRequest {
+
+ private final String method;
+ private String path;
+ private String mediaType = MediaTypes.JSON;
+
+ private Map<String, String> params = Maps.newHashMap();
+ private Map<String, String> headers = Maps.newHashMap();
+ private final Map<String, Part> parts = Maps.newHashMap();
+
+ private TestRequest(String method) {
+ this.method = method;
+ }
+
+ @Override
+ public String method() {
+ return method;
+ }
+
+ @Override
+ public String getMediaType() {
+ return mediaType;
+ }
+
+ public TestRequest setMediaType(String s) {
+ this.mediaType = s;
+ return this;
+ }
+
+ @Override
+ public boolean hasParam(String key) {
+ return params.keySet().contains(key);
+ }
+
+ @Override
+ public Optional<String> header(String name) {
+ return Optional.ofNullable(headers.get(name));
+ }
+
+ public TestRequest setHeader(String name, String value) {
+ this.headers.put(requireNonNull(name), requireNonNull(value));
+ return this;
+ }
+
+ @Override
+ public String getPath() {
+ return path;
+ }
+
+ public TestRequest setPath(String path) {
+ this.path = path;
+ return this;
+ }
+
+ public TestRequest setParams(Map<String, String> m) {
+ this.params = m;
+ return this;
+ }
+
+ public TestRequest setParam(String key, @Nullable String value) {
+ if (value != null) {
+ params.put(key, value);
+ }
+ return this;
+ }
+
+ @Override
+ protected String readParam(String key) {
+ return params.get(key);
+ }
+
+ @Override
+ protected List<String> readMultiParam(String key) {
+ String value = params.get(key);
+ return value == null ? emptyList() : singletonList(value);
+ }
+
+ @Override
+ public Map<String, String[]> getParams() {
+ return params.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> new String[] {e.getValue()}));
+ }
+
+ @Override
+ protected InputStream readInputStreamParam(String key) {
+ String param = readParam(key);
+
+ return param == null ? null : IOUtils.toInputStream(param);
+ }
+
+ @Override
+ protected Part readPart(String key) {
+ return parts.get(key);
+ }
+
+ public TestRequest setPart(String key, InputStream input, String fileName) {
+ parts.put(key, new PartImpl(input, fileName));
+ return this;
+ }
+
+ public Result execute() throws Exception {
+ TestResponse response = new TestResponse();
+ verifyRequest(action(), this);
+ action().handler().handle(this, response);
+ return new Result(response);
+ }
+ }
+
+ public static class TestResponse implements Response {
+
+ private TestStream stream;
+
+ private Map<String, String> headers = Maps.newHashMap();
+
+ public class TestStream implements Response.Stream {
+ private String mediaType;
+ private int status;
+
+ @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;
+ }
+ }
+
+ private final ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+ @Override
+ public JsonWriter newJsonWriter() {
+ return JsonWriter.of(new OutputStreamWriter(output, StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public XmlWriter newXmlWriter() {
+ return XmlWriter.of(new OutputStreamWriter(output, StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public Stream stream() {
+ if (stream == null) {
+ stream = new TestStream();
+ }
+ 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 static class Result {
+ private final TestResponse response;
+
+ private Result(TestResponse response) {
+ this.response = response;
+ }
+
+ public Result assertNoContent() {
+ return assertStatus(HttpURLConnection.HTTP_NO_CONTENT);
+ }
+
+ public String outputAsString() {
+ return new String(response.output.toByteArray(), StandardCharsets.UTF_8);
+ }
+
+ public byte[] output() {
+ return response.output.toByteArray();
+ }
+
+ public Result assertJson(String expectedJson) {
+ String json = outputAsString();
+ JsonAssert.assertJson(json).isSimilarTo(expectedJson);
+ return this;
+ }
+
+ /**
+ * Compares JSON response with JSON file available in classpath. For example if class
+ * is org.foo.BarTest and filename is index.json, then file must be located
+ * at src/test/resources/org/foo/BarTest/index.json.
+ *
+ * @param clazz the test class
+ * @param expectedJsonFilename name of the file containing the expected JSON
+ */
+ public Result assertJson(Class clazz, String expectedJsonFilename) {
+ String path = clazz.getSimpleName() + "/" + expectedJsonFilename;
+ URL url = clazz.getResource(path);
+ if (url == null) {
+ throw new IllegalStateException("Cannot find " + path);
+ }
+ String json = outputAsString();
+ JsonAssert.assertJson(json).isSimilarTo(url);
+ return this;
+ }
+
+ public Result assertNotModified() {
+ return assertStatus(HttpURLConnection.HTTP_NOT_MODIFIED);
+ }
+
+ public Result assertStatus(int httpStatus) {
+ assertThat(((TestStream) response.stream()).status()).isEqualTo(httpStatus);
+ return this;
+ }
+
+ public Result assertHeader(String name, String value) {
+ assertThat(response.getHeader(name)).isEqualTo(value);
+ return this;
+ }
+ }
+
+ private final WebService.Context context = new WebService.Context();
+
+ public WsTester(WebService... webServices) {
+ for (WebService webService : webServices) {
+ webService.define(context);
+ }
+ }
+
+ public WebService.Context context() {
+ return context;
+ }
+
+ @CheckForNull
+ public WebService.Controller controller(String key) {
+ return context.controller(key);
+ }
+
+ @CheckForNull
+ public WebService.Action action(String controllerKey, String actionKey) {
+ WebService.Controller controller = context.controller(controllerKey);
+ if (controller != null) {
+ return controller.action(actionKey);
+ }
+ return null;
+ }
+
+ public TestRequest newGetRequest(String controllerKey, String actionKey) {
+ return newRequest(controllerKey, actionKey, "GET");
+ }
+
+ public TestRequest newPostRequest(String controllerKey, String actionKey) {
+ return newRequest(controllerKey, actionKey, "POST");
+ }
+
+ private TestRequest newRequest(String controllerKey, String actionKey, String method) {
+ TestRequest request = new TestRequest(method);
+ WebService.Controller controller = context.controller(controllerKey);
+ if (controller == null) {
+ throw new IllegalArgumentException(
+ String.format("Controller '%s' is unknown, did you forget to call NewController.done()?", controllerKey));
+ }
+ WebService.Action action = controller.action(actionKey);
+ if (action == null) {
+ throw new IllegalArgumentException(
+ String.format("Action '%s' not found on controller '%s'.", actionKey, controllerKey));
+ }
+ request.setAction(action);
+ return request;
+ }
+}
diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsUtilsTest.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsUtilsTest.java
new file mode 100644
index 00000000000..5bbd1a9a891
--- /dev/null
+++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsUtilsTest.java
@@ -0,0 +1,99 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import java.io.IOException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonarqube.ws.Issues;
+import org.sonarqube.ws.MediaTypes;
+import org.sonarqube.ws.Permissions;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.test.ExceptionCauseMatcher.hasType;
+
+public class WsUtilsTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Rule
+ public LogTester logger = new LogTester();
+
+ @Test
+ public void write_json_by_default() {
+ TestRequest request = new TestRequest();
+ DumbResponse response = new DumbResponse();
+
+ Issues.Issue msg = Issues.Issue.newBuilder().setKey("I1").build();
+ WsUtils.writeProtobuf(msg, request, response);
+
+ assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+ assertThat(response.outputAsString())
+ .startsWith("{")
+ .contains("\"key\":\"I1\"")
+ .endsWith("}");
+ }
+
+ @Test
+ public void write_protobuf() throws Exception {
+ TestRequest request = new TestRequest();
+ request.setMediaType(MediaTypes.PROTOBUF);
+ DumbResponse response = new DumbResponse();
+
+ Issues.Issue msg = Issues.Issue.newBuilder().setKey("I1").build();
+ WsUtils.writeProtobuf(msg, request, response);
+
+ assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.PROTOBUF);
+ assertThat(Issues.Issue.parseFrom(response.getFlushedOutput()).getKey()).isEqualTo("I1");
+ }
+
+ @Test
+ public void rethrow_error_as_ISE_when_error_writing_message() {
+ TestRequest request = new TestRequest();
+ request.setMediaType(MediaTypes.PROTOBUF);
+
+ Permissions.Permission message = Permissions.Permission.newBuilder().setName("permission-name").build();
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectCause(hasType(NullPointerException.class));
+ expectedException.expectMessage("Error while writing protobuf message");
+ // provoke NullPointerException
+ WsUtils.writeProtobuf(message, null, new DumbResponse());
+ }
+
+ @Test
+ public void checkRequest_ok() {
+ WsUtils.checkRequest(true, "Missing param: %s", "foo");
+ // do not fail
+ }
+
+ @Test
+ public void checkRequest_ko() {
+ expectedException.expect(BadRequestException.class);
+ expectedException.expectMessage("Missing param: foo");
+
+ WsUtils.checkRequest(false, "Missing param: %s", "foo");
+ }
+
+}
diff --git a/server/sonar-webserver-ws/src/test/resources/logback-test.xml b/server/sonar-webserver-ws/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..3e34b0f9fc8
--- /dev/null
+++ b/server/sonar-webserver-ws/src/test/resources/logback-test.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration debug="false">
+ <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
+
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>
+ %d{yyyy.MM.dd HH:mm:ss} %-5level %msg%n
+ </pattern>
+ </encoder>
+ </appender>
+
+ <root>
+ <level value="INFO"/>
+ <appender-ref ref="CONSOLE"/>
+ </root>
+
+ <logger name="ch.qos.logback">
+ <level value="WARN"/>
+ </logger>
+
+ <logger name="okhttp3.mockwebserver">
+ <level value="WARN"/>
+ </logger>
+
+</configuration>