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