]> source.dussan.org Git - sonarqube.git/commitdiff
create sonar-webserver-es from sonar-server
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 8 Aug 2019 09:22:13 +0000 (11:22 +0200)
committerSonarTech <sonartech@sonarsource.com>
Wed, 14 Aug 2019 18:21:12 +0000 (20:21 +0200)
ie. move WS engine to dedicated project

84 files changed:
server/sonar-server/build.gradle
server/sonar-server/src/main/java/org/sonar/server/exceptions/BadRequestException.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/exceptions/ForbiddenException.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/exceptions/Message.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/exceptions/NotFoundException.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/exceptions/ServerException.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/exceptions/UnauthorizedException.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/exceptions/package-info.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentAction.java
server/sonar-server/src/main/java/org/sonar/server/ws/CacheWriter.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/DefaultLocalResponse.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/DeprecatedPropertiesWsFilter.java
server/sonar-server/src/main/java/org/sonar/server/ws/JsonWriterUtils.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/KeyExamples.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/RemovedWebServiceHandler.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/RequestVerifier.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/ServletFilterHandler.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/ServletRequest.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/ServletResponse.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/WebServiceEngine.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/WebServiceFilter.java
server/sonar-server/src/main/java/org/sonar/server/ws/WsAction.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/WsParameterBuilder.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/WsResponseCommonFormat.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/WsUtils.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/ws/package-info.java
server/sonar-server/src/main/resources/org/sonar/server/ws/removed-ws-example.json [deleted file]
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueWsModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionFacetsTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java
server/sonar-server/src/test/java/org/sonar/server/ws/CacheWriterTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/ws/DeprecatedPropertiesWsFilterTest.java
server/sonar-server/src/test/java/org/sonar/server/ws/DumbResponse.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/ws/RemovedWebServiceHandlerTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/ws/ServletRequestTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/ws/ServletResponseTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/ws/TestRequest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/ws/TestResponse.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/ws/WsActionTester.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/ws/WsTester.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/ws/WsUtilsTest.java [deleted file]
server/sonar-webserver-ws/build.gradle [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/BadRequestException.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/ForbiddenException.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/Message.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/NotFoundException.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/ServerException.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/UnauthorizedException.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/exceptions/package-info.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/CacheWriter.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/DefaultLocalResponse.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/JsonWriterUtils.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/KeyExamples.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/RemovedWebServiceHandler.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/RequestVerifier.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletFilterHandler.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletRequest.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletResponse.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WebServiceEngine.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsAction.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsParameterBuilder.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/WsUtils.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/package-info.java [new file with mode: 0644]
server/sonar-webserver-ws/src/main/resources/org/sonar/server/ws/removed-ws-example.json [new file with mode: 0644]
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/CacheWriterTest.java [new file with mode: 0644]
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/DumbResponse.java [new file with mode: 0644]
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/RemovedWebServiceHandlerTest.java [new file with mode: 0644]
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletRequestTest.java [new file with mode: 0644]
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletResponseTest.java [new file with mode: 0644]
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/TestRequest.java [new file with mode: 0644]
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/TestResponse.java [new file with mode: 0644]
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java [new file with mode: 0644]
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsActionTester.java [new file with mode: 0644]
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsTester.java [new file with mode: 0644]
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsUtilsTest.java [new file with mode: 0644]
server/sonar-webserver-ws/src/test/resources/logback-test.xml [new file with mode: 0644]
settings.gradle

index 3efd483fbd9c2907abb21d6dd4ef8b0660d0347b..d15997a4a1452dca6154a7b0ba31ecb6473d1eaf 100644 (file)
@@ -50,6 +50,7 @@ dependencies {
   compile project(':server:sonar-db-migration')
   compile project(':server:sonar-process')
   compile project(':server:sonar-server-common')
+  compile project(':server:sonar-webserver-ws')
   compile project(':sonar-core')
   compile project(':sonar-duplications')
   compile project(':sonar-scanner-protocol')
@@ -80,6 +81,7 @@ dependencies {
   testCompile project(':server:sonar-db-testing')
   testCompile project(path: ":server:sonar-server-common", configuration: "tests")
   testCompile project(':sonar-testing-harness')
+  testCompile project(path: ":server:sonar-webserver-ws", configuration: "tests")
 
   runtime 'io.jsonwebtoken:jjwt-jackson'
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/exceptions/BadRequestException.java b/server/sonar-server/src/main/java/org/sonar/server/exceptions/BadRequestException.java
deleted file mode 100644 (file)
index 7a2fdf7..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/exceptions/ForbiddenException.java b/server/sonar-server/src/main/java/org/sonar/server/exceptions/ForbiddenException.java
deleted file mode 100644 (file)
index d72eefb..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/exceptions/Message.java b/server/sonar-server/src/main/java/org/sonar/server/exceptions/Message.java
deleted file mode 100644 (file)
index c069ead..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/exceptions/NotFoundException.java b/server/sonar-server/src/main/java/org/sonar/server/exceptions/NotFoundException.java
deleted file mode 100644 (file)
index ff5fb2f..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/exceptions/ServerException.java b/server/sonar-server/src/main/java/org/sonar/server/exceptions/ServerException.java
deleted file mode 100644 (file)
index 491c17e..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/exceptions/UnauthorizedException.java b/server/sonar-server/src/main/java/org/sonar/server/exceptions/UnauthorizedException.java
deleted file mode 100644 (file)
index 0b4af12..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/exceptions/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/exceptions/package-info.java
deleted file mode 100644 (file)
index c1ac144..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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;
-
index 2fb05802fa933fb373d5ca8f002c8fc4d1335e5f..30b70b5f08a6b6fc0519b6daf571afcd015021d9 100644 (file)
@@ -31,7 +31,6 @@ import org.sonar.server.issue.workflow.IssueWorkflow;
 import org.sonar.server.qualitygate.changeevent.QGChangeEventListenersImpl;
 import org.sonar.server.settings.ProjectConfigurationLoaderImpl;
 import org.sonar.server.webhook.WebhookQGChangeEventListener;
-import org.sonar.server.ws.WsResponseCommonFormat;
 
 public class IssueWsModule extends Module {
   @Override
@@ -50,7 +49,6 @@ public class IssueWsModule extends Module {
       SearchResponseLoader.class,
       SearchResponseFormat.class,
       OperationResponseWriter.class,
-      WsResponseCommonFormat.class,
       AddCommentAction.class,
       EditCommentAction.class,
       DeleteCommentAction.class,
index 9a92a1db6fb2f8fed08d7572a12fc9a673b06f2c..2f17f5922039c32dc4d2bce7246390f95be107e7 100644 (file)
@@ -44,7 +44,6 @@ import org.sonar.db.user.UserDto;
 import org.sonar.markdown.Markdown;
 import org.sonar.server.es.Facets;
 import org.sonar.server.issue.workflow.Transition;
-import org.sonar.server.ws.WsResponseCommonFormat;
 import org.sonarqube.ws.Common;
 import org.sonarqube.ws.Issues;
 import org.sonarqube.ws.Issues.Actions;
@@ -77,13 +76,11 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES;
 public class SearchResponseFormat {
 
   private final Durations durations;
-  private final WsResponseCommonFormat commonFormat;
   private final Languages languages;
   private final AvatarResolver avatarFactory;
 
-  public SearchResponseFormat(Durations durations, WsResponseCommonFormat commonFormat, Languages languages, AvatarResolver avatarFactory) {
+  public SearchResponseFormat(Durations durations, Languages languages, AvatarResolver avatarFactory) {
     this.durations = durations;
-    this.commonFormat = commonFormat;
     this.languages = languages;
     this.avatarFactory = avatarFactory;
   }
@@ -138,7 +135,14 @@ public class SearchResponseFormat {
     response.setP(paging.pageIndex());
     response.setPs(paging.pageSize());
     response.setTotal(paging.total());
-    response.setPaging(commonFormat.formatPaging(paging));
+    response.setPaging(formatPaging(paging));
+  }
+
+  private Common.Paging.Builder formatPaging(Paging paging) {
+    return Common.Paging.newBuilder()
+      .setPageIndex(paging.pageIndex())
+      .setPageSize(paging.pageSize())
+      .setTotal(paging.total());
   }
 
   private List<Issues.Issue> formatIssues(Set<SearchAdditionalField> fields, SearchResponseData data) {
@@ -316,11 +320,25 @@ public class SearchResponseFormat {
     Common.Rules.Builder wsRules = Common.Rules.newBuilder();
     List<RuleDefinitionDto> rules = firstNonNull(data.getRules(), emptyList());
     for (RuleDefinitionDto rule : rules) {
-      wsRules.addRules(commonFormat.formatRule(rule));
+      wsRules.addRules(formatRule(rule));
     }
     return wsRules;
   }
 
+  private Common.Rule.Builder formatRule(RuleDefinitionDto rule) {
+    Common.Rule.Builder builder = Common.Rule.newBuilder()
+      .setKey(rule.getKey().toString())
+      .setName(nullToEmpty(rule.getName()))
+      .setStatus(Common.RuleStatus.valueOf(rule.getStatus().name()));
+
+    builder.setLang(nullToEmpty(rule.getLanguage()));
+    Language lang = languages.get(rule.getLanguage());
+    if (lang != null) {
+      builder.setLangName(lang.getName());
+    }
+    return builder;
+  }
+
   private static List<Issues.Component> formatComponents(SearchResponseData data) {
     Collection<ComponentDto> components = data.getComponents();
     List<Issues.Component> result = new ArrayList<>();
index aec992515718e9b3cb944281dff80c5d02715316..3083e4e34e1077f1a32b1c0237851941e7efab02 100644 (file)
@@ -62,6 +62,7 @@ import org.sonar.server.qualityprofile.QPMeasureData;
 import org.sonar.server.qualityprofile.QualityProfile;
 import org.sonar.server.ui.PageRepository;
 import org.sonar.server.user.UserSession;
+import org.sonar.server.ws.WsUtils;
 
 import static java.lang.String.format;
 import static java.util.Collections.emptySortedSet;
@@ -76,10 +77,10 @@ import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesEx
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
-import static org.sonar.server.ws.WsUtils.checkComponentNotAModuleAndNotADirectory;
 
 public class ComponentAction implements NavigationWsAction {
 
+  private static final Set<String> MODULE_OR_DIR_QUALIFIERS = ImmutableSet.of(Qualifiers.MODULE, Qualifiers.DIRECTORY);
   static final String PARAM_COMPONENT = "component";
   private static final String PARAM_BRANCH = "branch";
   private static final String PARAM_PULL_REQUEST = "pullRequest";
@@ -177,6 +178,10 @@ public class ComponentAction implements NavigationWsAction {
     }
   }
 
+  private static void checkComponentNotAModuleAndNotADirectory(ComponentDto component) {
+    WsUtils.checkRequest(!MODULE_OR_DIR_QUALIFIERS.contains(component.qualifier()), "Operation not supported for module or directory components");
+  }
+
   private ComponentDto getRootProjectOrBranch(ComponentDto component, DbSession session) {
     if (!component.isRootProject()) {
       return dbClient.componentDao().selectOrFailByUuid(session, component.projectUuid());
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ws/CacheWriter.java b/server/sonar-server/src/main/java/org/sonar/server/ws/CacheWriter.java
deleted file mode 100644 (file)
index 75ffb3a..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/ws/DefaultLocalResponse.java b/server/sonar-server/src/main/java/org/sonar/server/ws/DefaultLocalResponse.java
deleted file mode 100644 (file)
index 07c1184..0000000
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * 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);
-    }
-  }
-}
index 95a09bc4925b1a377bd40ca78b2715a1c885442e..7d67c785764756f62949ee457edb65baabc80212 100644 (file)
@@ -37,15 +37,11 @@ import javax.servlet.http.HttpServletResponse;
 import org.apache.commons.io.IOUtils;
 import org.sonar.api.web.ServletFilter;
 import org.sonar.server.property.ws.IndexAction;
+import org.sonar.server.setting.ws.SettingsWsParameters;
 
 import static com.google.common.base.Strings.isNullOrEmpty;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.sonar.server.property.ws.PropertiesWs.CONTROLLER_PROPERTIES;
-import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_COMPONENT;
-import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_KEY;
-import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_KEYS;
-import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_VALUE;
-import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_VALUES;
 
 /**
  * This filter is used to execute deprecated api/properties WS, that were using REST
@@ -107,12 +103,12 @@ public class DeprecatedPropertiesWsFilter extends ServletFilter {
     }
 
     @Override
-    protected String readParam(String key) {
+    public String readParam(String key) {
       return restResponse.additionalParams.get(key);
     }
 
     @Override
-    protected List<String> readMultiParam(String key) {
+    public List<String> readMultiParam(String key) {
       return new ArrayList<>(restResponse.additionalMultiParams.get(key));
     }
 
@@ -219,20 +215,20 @@ public class DeprecatedPropertiesWsFilter extends ServletFilter {
     }
 
     private void redirectToSet(Optional<String> key, List<String> values, Optional<String> component) {
-      addParameterIfPresent(PARAM_KEY, key);
+      addParameterIfPresent(SettingsWsParameters.PARAM_KEY, key);
       if (values.size() == 1) {
-        additionalParams.put(PARAM_VALUE, values.get(0));
+        additionalParams.put(SettingsWsParameters.PARAM_VALUE, values.get(0));
       } else {
-        additionalMultiParams.putAll(PARAM_VALUES, values);
+        additionalMultiParams.putAll(SettingsWsParameters.PARAM_VALUES, values);
       }
-      addParameterIfPresent(PARAM_COMPONENT, component);
+      addParameterIfPresent(SettingsWsParameters.PARAM_COMPONENT, component);
       redirectedPath = "api/settings/set";
       redirectedMethod = "POST";
     }
 
     private void redirectToReset(Optional<String> key, Optional<String> component) {
-      addParameterIfPresent(PARAM_KEYS, key);
-      addParameterIfPresent(PARAM_COMPONENT, component);
+      addParameterIfPresent(SettingsWsParameters.PARAM_KEYS, key);
+      addParameterIfPresent(SettingsWsParameters.PARAM_COMPONENT, component);
       redirectedPath = "api/settings/reset";
       redirectedMethod = "POST";
     }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ws/JsonWriterUtils.java b/server/sonar-server/src/main/java/org/sonar/server/ws/JsonWriterUtils.java
deleted file mode 100644 (file)
index dec4bbc..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/ws/KeyExamples.java b/server/sonar-server/src/main/java/org/sonar/server/ws/KeyExamples.java
deleted file mode 100644 (file)
index ef8773d..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java b/server/sonar-server/src/main/java/org/sonar/server/ws/LocalRequestAdapter.java
deleted file mode 100644 (file)
index 849ce47..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/ws/RemovedWebServiceHandler.java b/server/sonar-server/src/main/java/org/sonar/server/ws/RemovedWebServiceHandler.java
deleted file mode 100644 (file)
index 1b2f608..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/ws/RequestVerifier.java b/server/sonar-server/src/main/java/org/sonar/server/ws/RequestVerifier.java
deleted file mode 100644 (file)
index c5e8634..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/ws/ServletFilterHandler.java b/server/sonar-server/src/main/java/org/sonar/server/ws/ServletFilterHandler.java
deleted file mode 100644 (file)
index 7ae51e2..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/ws/ServletRequest.java b/server/sonar-server/src/main/java/org/sonar/server/ws/ServletRequest.java
deleted file mode 100644 (file)
index 966e042..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * 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
-  protected String readParam(String key) {
-    return source.getParameter(key);
-  }
-
-  @Override
-  public Map<String, String[]> getParams() {
-    return source.getParameterMap();
-  }
-
-  @Override
-  protected 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-server/src/main/java/org/sonar/server/ws/ServletResponse.java b/server/sonar-server/src/main/java/org/sonar/server/ws/ServletResponse.java
deleted file mode 100644 (file)
index 2f385b8..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/ws/WebServiceEngine.java b/server/sonar-server/src/main/java/org/sonar/server/ws/WebServiceEngine.java
deleted file mode 100644 (file)
index e7569e6..0000000
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- * 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;
-    }
-  }
-
-}
index b98562835521d381ba330a1c3732aad2f3cc6e69..ebd1b3aefca207cf95c1349569c6be6be2933ed0 100644 (file)
@@ -30,9 +30,9 @@ import org.sonar.api.SonarRuntime;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.web.ServletFilter;
 import org.sonar.core.util.stream.MoreCollectors;
+import org.sonar.server.property.ws.PropertiesWs;
 
 import static java.util.stream.Stream.concat;
-import static org.sonar.server.property.ws.PropertiesWs.CONTROLLER_PROPERTIES;
 import static org.sonar.server.ws.WebServiceReroutingFilter.MOVED_WEB_SERVICES;
 
 /**
@@ -60,7 +60,7 @@ public class WebServiceFilter extends ServletFilter {
         .map(toPath()))
           .collect(MoreCollectors.toSet());
     this.excludeUrls = concat(concat(
-      Stream.of("/" + CONTROLLER_PROPERTIES + "*"),
+      Stream.of("/" + PropertiesWs.CONTROLLER_PROPERTIES + "*"),
       MOVED_WEB_SERVICES.stream()),
       webServiceEngine.controllers().stream()
         .flatMap(controller -> controller.actions().stream())
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ws/WsAction.java b/server/sonar-server/src/main/java/org/sonar/server/ws/WsAction.java
deleted file mode 100644 (file)
index bb7ed19..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/ws/WsParameterBuilder.java b/server/sonar-server/src/main/java/org/sonar/server/ws/WsParameterBuilder.java
deleted file mode 100644 (file)
index 291d9af..0000000
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * 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-server/src/main/java/org/sonar/server/ws/WsResponseCommonFormat.java b/server/sonar-server/src/main/java/org/sonar/server/ws/WsResponseCommonFormat.java
deleted file mode 100644 (file)
index 3167e90..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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.resources.Language;
-import org.sonar.api.resources.Languages;
-import org.sonar.api.utils.Paging;
-import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonarqube.ws.Common;
-
-import static com.google.common.base.Strings.nullToEmpty;
-
-public class WsResponseCommonFormat {
-
-  private final Languages languages;
-
-  public WsResponseCommonFormat(Languages languages) {
-    this.languages = languages;
-  }
-
-  public Common.Paging.Builder formatPaging(Paging paging) {
-    return Common.Paging.newBuilder()
-      .setPageIndex(paging.pageIndex())
-      .setPageSize(paging.pageSize())
-      .setTotal(paging.total());
-  }
-
-  public Common.Rule.Builder formatRule(RuleDefinitionDto rule) {
-    Common.Rule.Builder builder = Common.Rule.newBuilder()
-      .setKey(rule.getKey().toString())
-      .setName(nullToEmpty(rule.getName()))
-      .setStatus(Common.RuleStatus.valueOf(rule.getStatus().name()));
-
-    builder.setLang(nullToEmpty(rule.getLanguage()));
-    Language lang = languages.get(rule.getLanguage());
-    if (lang != null) {
-      builder.setLangName(lang.getName());
-    }
-    return builder;
-  }
-
-}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/ws/WsUtils.java b/server/sonar-server/src/main/java/org/sonar/server/ws/WsUtils.java
deleted file mode 100644 (file)
index 9c14475..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * 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.db.component.ComponentDto;
-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 static final Set<String> MODULE_OR_DIR_QUALIFIERS = ImmutableSet.of(Qualifiers.MODULE, Qualifiers.DIRECTORY);
-
-  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();
-  }
-
-  public static void checkComponentNotAModuleAndNotADirectory(ComponentDto component) {
-    checkRequest(!MODULE_OR_DIR_QUALIFIERS.contains(component.qualifier()), "Operation not supported for module or directory components");
-  }
-}
index 464c533bab7dbcae66f4f3fd37c1478b843e3cbb..d4209ca34a5bc316f9f89d92c787340f65f0604d 100644 (file)
@@ -21,3 +21,4 @@
 package org.sonar.server.ws;
 
 import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/ws/removed-ws-example.json b/server/sonar-server/src/main/resources/org/sonar/server/ws/removed-ws-example.json
deleted file mode 100644 (file)
index 6764a13..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "errors": [
-    {
-      "msg": "The web service '/api/...' doesn't exists anymore, please read its documentation to use alternatives"
-    }
-  ]
-}
index 7a44fea9f371a6f6eff4c07d08f2a006fbddf98c..b9c68da85d37f652ce2d25eead18ec2b3bdebea4 100644 (file)
@@ -30,7 +30,7 @@ public class IssueWsModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new IssueWsModule().configure(container);
-    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 31);
+    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 30);
   }
 }
 
index 9a4bacc317dad51d753392ae741ec5599f620a59..1dd9477158e9298333f4c9d0fcca2ba30a1c810c 100644 (file)
@@ -51,7 +51,6 @@ import org.sonar.server.permission.index.WebAuthorizationTypeSupport;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.view.index.ViewIndexer;
 import org.sonar.server.ws.WsActionTester;
-import org.sonar.server.ws.WsResponseCommonFormat;
 import org.sonarqube.ws.Issues;
 import org.sonarqube.ws.Issues.Component;
 import org.sonarqube.ws.Issues.Issue;
@@ -105,7 +104,7 @@ public class SearchActionComponentsTest {
   private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter);
   private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, dbClient, new TransitionService(userSession, issueWorkflow));
   private Languages languages = new Languages();
-  private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(languages), languages, new AvatarResolverImpl());
+  private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new AvatarResolverImpl());
   private PermissionIndexerTester permissionIndexer = new PermissionIndexerTester(es, issueIndexer);
 
   private WsActionTester ws = new WsActionTester(new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat,
index 1d5d65750a9892802ecd4813bd519e3d2a4ed46d..3a1c6530e737e441b4f9eb8b32224036671cd7b0 100644 (file)
@@ -48,7 +48,6 @@ import org.sonar.server.permission.index.PermissionIndexer;
 import org.sonar.server.permission.index.WebAuthorizationTypeSupport;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.WsActionTester;
-import org.sonar.server.ws.WsResponseCommonFormat;
 import org.sonarqube.ws.Common;
 import org.sonarqube.ws.Common.FacetValue;
 import org.sonarqube.ws.Issues.SearchWsResponse;
@@ -87,7 +86,7 @@ public class SearchActionFacetsTest {
   private IssueQueryFactory issueQueryFactory = new IssueQueryFactory(db.getDbClient(), Clock.systemUTC(), userSession);
   private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, db.getDbClient(), new TransitionService(userSession, null));
   private Languages languages = new Languages();
-  private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(languages), languages, new AvatarResolverImpl());
+  private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new AvatarResolverImpl());
 
   private WsActionTester ws = new WsActionTester(
     new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat,
index 2c6fc63e0db121a31e46cf6eb7d2ec2dec3ec72a..f3768030a5ee2f95bf1171f0d60ad6e753172c7e 100644 (file)
@@ -68,7 +68,6 @@ import org.sonar.server.permission.index.WebAuthorizationTypeSupport;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestResponse;
 import org.sonar.server.ws.WsActionTester;
-import org.sonar.server.ws.WsResponseCommonFormat;
 import org.sonarqube.ws.Common;
 import org.sonarqube.ws.Common.Severity;
 import org.sonarqube.ws.Issues;
@@ -99,8 +98,6 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ADDITIONAL_
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AFTER;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_HIDE_COMMENTS;
-import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PAGE_INDEX;
-import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PAGE_SIZE;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES;
 
 public class SearchActionTest {
@@ -123,7 +120,7 @@ public class SearchActionTest {
   private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter);
   private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, dbClient, new TransitionService(userSession, issueWorkflow));
   private Languages languages = new Languages();
-  private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(languages), languages, new AvatarResolverImpl());
+  private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new AvatarResolverImpl());
   private WsActionTester ws = new WsActionTester(new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat,
     new MapSettings().asConfig(), System2.INSTANCE, dbClient));
   private StartupIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
index b029c512d7e166cc5ba3b44a77afb90d7e6029d0..6b1aef9fd63ef938f01d1edc62aa226264445591 100644 (file)
@@ -50,7 +50,6 @@ import org.sonar.server.permission.index.PermissionIndexerTester;
 import org.sonar.server.permission.index.WebAuthorizationTypeSupport;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.WsActionTester;
-import org.sonar.server.ws.WsResponseCommonFormat;
 import org.sonar.test.JsonAssert;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -77,7 +76,7 @@ public class SearchActionTestOnSonarCloud {
   private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter);
   private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, dbClient, new TransitionService(userSession, issueWorkflow));
   private Languages languages = new Languages();
-  private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(languages), languages, new AvatarResolverImpl());
+  private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), languages, new AvatarResolverImpl());
   private PermissionIndexerTester permissionIndexer = new PermissionIndexerTester(es, issueIndexer);
 
   private SearchAction underTest = new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat,
diff --git a/server/sonar-server/src/test/java/org/sonar/server/ws/CacheWriterTest.java b/server/sonar-server/src/test/java/org/sonar/server/ws/CacheWriterTest.java
deleted file mode 100644 (file)
index 355d476..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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();
-  }
-}
index 848eb6eca1627d1cf592cefabc65ded293a229e0..753f4ae0bc37f8b21cf0aa3843ecc6f17d7ad0cf 100644 (file)
@@ -20,7 +20,6 @@
 package org.sonar.server.ws;
 
 import java.io.ByteArrayInputStream;
-import java.io.IOException;
 import java.util.Arrays;
 import javax.servlet.FilterChain;
 import javax.servlet.ReadListener;
@@ -250,7 +249,7 @@ public class DeprecatedPropertiesWsFilterTest {
   }
 
   @Test
-  public void redirect_delete_api_properties_to_api_settings_reset() throws Exception {
+  public void redirect_delete_api_properties_to_api_settings_reset() {
     when(request.getRequestURI()).thenReturn("/api/properties/my.property");
     when(request.getParameter("resource")).thenReturn("my_project");
     when(request.getMethod()).thenReturn("DELETE");
diff --git a/server/sonar-server/src/test/java/org/sonar/server/ws/DumbResponse.java b/server/sonar-server/src/test/java/org/sonar/server/ws/DumbResponse.java
deleted file mode 100644 (file)
index f7d2aee..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * 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-server/src/test/java/org/sonar/server/ws/RemovedWebServiceHandlerTest.java b/server/sonar-server/src/test/java/org/sonar/server/ws/RemovedWebServiceHandlerTest.java
deleted file mode 100644 (file)
index 262a832..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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-server/src/test/java/org/sonar/server/ws/ServletRequestTest.java b/server/sonar-server/src/test/java/org/sonar/server/ws/ServletRequestTest.java
deleted file mode 100644 (file)
index 502dfa9..0000000
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * 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-server/src/test/java/org/sonar/server/ws/ServletResponseTest.java b/server/sonar-server/src/test/java/org/sonar/server/ws/ServletResponseTest.java
deleted file mode 100644 (file)
index aa4cda9..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * 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-server/src/test/java/org/sonar/server/ws/TestRequest.java b/server/sonar-server/src/test/java/org/sonar/server/ws/TestRequest.java
deleted file mode 100644 (file)
index aea0e0d..0000000
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * 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-server/src/test/java/org/sonar/server/ws/TestResponse.java b/server/sonar-server/src/test/java/org/sonar/server/ws/TestResponse.java
deleted file mode 100644 (file)
index 82057f0..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * 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-server/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java b/server/sonar-server/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java
deleted file mode 100644 (file)
index 160fce7..0000000
+++ /dev/null
@@ -1,483 +0,0 @@
-/*
- * 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-server/src/test/java/org/sonar/server/ws/WsActionTester.java b/server/sonar-server/src/test/java/org/sonar/server/ws/WsActionTester.java
deleted file mode 100644 (file)
index fede932..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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-server/src/test/java/org/sonar/server/ws/WsTester.java b/server/sonar-server/src/test/java/org/sonar/server/ws/WsTester.java
deleted file mode 100644 (file)
index e69153b..0000000
+++ /dev/null
@@ -1,361 +0,0 @@
-/*
- * 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-server/src/test/java/org/sonar/server/ws/WsUtilsTest.java b/server/sonar-server/src/test/java/org/sonar/server/ws/WsUtilsTest.java
deleted file mode 100644 (file)
index 5bbd1a9..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * 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/build.gradle b/server/sonar-webserver-ws/build.gradle
new file mode 100644 (file)
index 0000000..69988d0
--- /dev/null
@@ -0,0 +1,42 @@
+description = 'WebServer "API" to write Web Services'
+
+sonarqube {
+  properties {
+    property 'sonar.projectName', "${projectTitle} :: WebServer :: WS"
+  }
+}
+
+configurations {
+  tests
+
+  testCompile.extendsFrom tests
+}
+
+dependencies {
+  // please keep the list grouped by configuration and ordered by name
+
+  compile 'com.google.guava:guava'
+  compile project(':sonar-core')
+  compile project(path: ':sonar-plugin-api', configuration: 'shadow')
+  compile project(':sonar-plugin-api-impl')
+  compile project(':sonar-ws')
+
+  compileOnly 'com.google.code.findbugs:jsr305'
+  compileOnly 'javax.servlet:javax.servlet-api'
+  compileOnly 'org.apache.tomcat.embed:tomcat-embed-core'
+
+  testCompile 'com.google.code.findbugs:jsr305'
+  testCompile 'javax.servlet:javax.servlet-api'
+  testCompile 'org.apache.tomcat.embed:tomcat-embed-core'
+  testCompile 'org.mockito:mockito-core'
+  testCompile project(':sonar-testing-harness')
+}
+
+task testJar(type: Jar) {
+  classifier = 'tests'
+  from sourceSets.test.output
+}
+
+artifacts {
+  tests testJar
+}
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 (file)
index 0000000..7a2fdf7
--- /dev/null
@@ -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 (file)
index 0000000..d72eefb
--- /dev/null
@@ -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 (file)
index 0000000..c069ead
--- /dev/null
@@ -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 (file)
index 0000000..ff5fb2f
--- /dev/null
@@ -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 (file)
index 0000000..491c17e
--- /dev/null
@@ -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 (file)
index 0000000..0b4af12
--- /dev/null
@@ -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 (file)
index 0000000..c1ac144
--- /dev/null
@@ -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 (file)
index 0000000..75ffb3a
--- /dev/null
@@ -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 (file)
index 0000000..07c1184
--- /dev/null
@@ -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 (file)
index 0000000..dec4bbc
--- /dev/null
@@ -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 (file)
index 0000000..ef8773d
--- /dev/null
@@ -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 (file)
index 0000000..849ce47
--- /dev/null
@@ -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 (file)
index 0000000..1b2f608
--- /dev/null
@@ -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 (file)
index 0000000..c5e8634
--- /dev/null
@@ -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 (file)
index 0000000..7ae51e2
--- /dev/null
@@ -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 (file)
index 0000000..1f473a9
--- /dev/null
@@ -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 (file)
index 0000000..2f385b8
--- /dev/null
@@ -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 (file)
index 0000000..e7569e6
--- /dev/null
@@ -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 (file)
index 0000000..bb7ed19
--- /dev/null
@@ -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 (file)
index 0000000..291d9af
--- /dev/null
@@ -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 (file)
index 0000000..2f93676
--- /dev/null
@@ -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 (file)
index 0000000..464c533
--- /dev/null
@@ -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 (file)
index 0000000..6764a13
--- /dev/null
@@ -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 (file)
index 0000000..355d476
--- /dev/null
@@ -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 (file)
index 0000000..f7d2aee
--- /dev/null
@@ -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 (file)
index 0000000..262a832
--- /dev/null
@@ -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 (file)
index 0000000..502dfa9
--- /dev/null
@@ -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 (file)
index 0000000..aa4cda9
--- /dev/null
@@ -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 (file)
index 0000000..aea0e0d
--- /dev/null
@@ -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 (file)
index 0000000..82057f0
--- /dev/null
@@ -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 (file)
index 0000000..160fce7
--- /dev/null
@@ -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 (file)
index 0000000..fede932
--- /dev/null
@@ -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 (file)
index 0000000..e69153b
--- /dev/null
@@ -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 (file)
index 0000000..5bbd1a9
--- /dev/null
@@ -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 (file)
index 0000000..3e34b0f
--- /dev/null
@@ -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>
index 669cd2dde6b1920fb83a051e1c518d9e486b03b7..45401a9044452c8f35fb793d1d5101a55b69035e 100644 (file)
@@ -17,6 +17,7 @@ include 'server:sonar-server'
 include 'server:sonar-server-common'
 include 'server:sonar-vsts'
 include 'server:sonar-web'
+include 'server:sonar-webserver-ws'
 
 include 'sonar-application'
 include 'sonar-check-api'