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