Skip to content

Commit

Permalink
JS-390 Refactor HTTP layer in BridgeServerImpl
Browse files Browse the repository at this point in the history
  • Loading branch information
saberduck committed Nov 11, 2024
1 parent 6391608 commit e21e0cc
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@
*/
package org.sonar.plugins.javascript.bridge;

import static java.util.Collections.emptyList;
import static org.sonar.plugins.javascript.bridge.NetUtils.findOpenPort;
import static org.sonar.plugins.javascript.nodejs.NodeCommandBuilderImpl.NODE_EXECUTABLE_PROPERTY;
import static org.sonar.plugins.javascript.nodejs.NodeCommandBuilderImpl.NODE_FORCE_HOST_PROPERTY;
import static org.sonar.plugins.javascript.nodejs.NodeCommandBuilderImpl.SKIP_NODE_PROVISIONING_PROPERTY;

import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import java.io.File;
Expand All @@ -47,15 +41,6 @@
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.util.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.SonarProduct;
Expand All @@ -65,6 +50,12 @@
import org.sonar.plugins.javascript.nodejs.NodeCommandBuilder;
import org.sonar.plugins.javascript.nodejs.NodeCommandException;

import static java.util.Collections.emptyList;
import static org.sonar.plugins.javascript.bridge.NetUtils.findOpenPort;
import static org.sonar.plugins.javascript.nodejs.NodeCommandBuilderImpl.NODE_EXECUTABLE_PROPERTY;
import static org.sonar.plugins.javascript.nodejs.NodeCommandBuilderImpl.NODE_FORCE_HOST_PROPERTY;
import static org.sonar.plugins.javascript.nodejs.NodeCommandBuilderImpl.SKIP_NODE_PROVISIONING_PROPERTY;

public class BridgeServerImpl implements BridgeServer {

private enum Status {
Expand Down Expand Up @@ -149,6 +140,7 @@ private static void silenceHttpClientLogs() {
* This method sets the log level of the logger with the given name to INFO.
* It assumes that SLF4J is used as the logging facade and Logback as the logging implementation.
* Since we don't want to directly depend on logback, we use reflection to set the log level.
*
* @param loggerName
*/
private static void setLoggerLevelToInfo(String loggerName) {
Expand Down Expand Up @@ -195,8 +187,8 @@ int getTimeoutSeconds() {
void deploy(Configuration configuration) throws IOException {
bundle.deploy(temporaryDeployLocation);
if (configuration.get(NODE_EXECUTABLE_PROPERTY).isPresent() ||
configuration.getBoolean(SKIP_NODE_PROVISIONING_PROPERTY).orElse(false) ||
configuration.getBoolean(NODE_FORCE_HOST_PROPERTY).orElse(false)) {
configuration.getBoolean(SKIP_NODE_PROVISIONING_PROPERTY).orElse(false) ||
configuration.getBoolean(NODE_FORCE_HOST_PROPERTY).orElse(false)) {
String property;
if (configuration.get(NODE_EXECUTABLE_PROPERTY).isPresent()) {
property = NODE_EXECUTABLE_PROPERTY;
Expand Down Expand Up @@ -421,35 +413,19 @@ public AnalysisResponse analyzeHtml(JsAnalysisRequest request) throws IOExceptio
}

private BridgeResponse request(String json, String endpoint) throws IOException {
try (var httpclient = HttpClients.createDefault()) {

var config = RequestConfig.custom()
.setResponseTimeout(Timeout.ofSeconds(timeoutSeconds))
.build();

HttpPost httpPost = new HttpPost(url(endpoint));
httpPost.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
httpPost.setConfig(config);

return httpclient.execute(httpPost, response -> {
var contentTypeHeader = response.getHeader("Content-Type");
var responseBody = EntityUtils.toByteArray(response.getEntity());
if (isFormData(contentTypeHeader)) {
return FormDataUtils.parseFormData(contentTypeHeader.toString(), responseBody);
} else {
return new BridgeResponse(new String(responseBody, StandardCharsets.UTF_8));
}
});
} catch (IOException e) {
throw new IllegalStateException("The bridge server is unresponsive", e);
var response = Http.getInstance().post(json, url(endpoint), timeoutSeconds);
if (isFormData(response.contentType())) {
return FormDataUtils.parseFormData(response.contentType(), response.body());
} else {
return new BridgeServer.BridgeResponse(new String(response.body(), StandardCharsets.UTF_8));
}
}

private static boolean isFormData(@Nullable Header contentTypeHeader) {
private static boolean isFormData(@Nullable String contentTypeHeader) {
if (contentTypeHeader == null) {
return false;
}
return contentTypeHeader.toString().contains("multipart/form-data");
return contentTypeHeader.contains("multipart/form-data");
}

private static AnalysisResponse response(BridgeResponse result, String filePath) {
Expand All @@ -470,9 +446,8 @@ public boolean isAlive() {
if (nodeCommand == null && status != Status.STARTED) {
return false;
}
try (var client = HttpClients.custom().build()) {
var get = new HttpGet(url("status"));
var res = client.execute(get, response -> EntityUtils.toString(response.getEntity()));
try {
String res = Http.getInstance().get(url("status"));
return "OK!".equals(res);
} catch (IOException e) {
return false;
Expand Down Expand Up @@ -500,7 +475,8 @@ TsConfigResponse tsConfigFiles(String tsconfigAbsolutePath) {
LOG.error("Failed to request files for tsconfig: " + tsconfigAbsolutePath, e);
} catch (JsonSyntaxException e) {
LOG.error(
"Failed to parse response when requesting files for tsconfig: {}: \n-----\n{}\n-----\n{}", tsconfigAbsolutePath, result, e.getMessage()
"Failed to parse response when requesting files for tsconfig: {}: \n-----\n{}\n-----\n{}", tsconfigAbsolutePath, result,
e.getMessage()
);
}
return new TsConfigResponse(emptyList(), emptyList(), result, null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package org.sonar.plugins.javascript.bridge;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import javax.annotation.Nullable;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.util.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

interface Http {

static Http getInstance() {
return new JdkHttp();
}

Response post(String json, URI uri, long timeoutSeconds) throws IOException;

String get(URI uri) throws IOException;

record Response(String contentType, byte[] body) {}

class JdkHttp implements Http {

private static final Logger LOG = LoggerFactory.getLogger(JdkHttp.class);
private final HttpClient client;

JdkHttp() {
this.client =
HttpClient.newBuilder().build();
}

@Override
public Response post(String json, URI uri, long timeoutSeconds) throws IOException {
var request = HttpRequest
.newBuilder()
.uri(uri)
.timeout(Duration.ofSeconds(timeoutSeconds))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();

try {
var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
var contentType = response.headers().firstValue("Content-Type")
.orElseThrow(() -> new IllegalStateException("No Content-Type header"));
return new Response(contentType, response.body());
} catch (InterruptedException e) {
throw handleInterruptedException(e, "Request " + uri + " was interrupted.");
}
}

@Override
public String get(URI uri) throws IOException{
var request = HttpRequest.newBuilder(uri).GET().build();
try {
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
return response.body();
} catch (InterruptedException e) {
throw handleInterruptedException(e, "isAlive was interrupted");
}
}

private static IllegalStateException handleInterruptedException(
InterruptedException e,
String msg
) {
LOG.error(msg, e);
Thread.currentThread().interrupt();
return new IllegalStateException(msg, e);
}

}

class ApacheHttp implements Http {

@Override
public Response post(String json, URI uri, long timeoutSeconds) throws IOException {
try (var httpclient = HttpClients.createDefault()) {

var config = RequestConfig.custom()
.setResponseTimeout(Timeout.ofSeconds(timeoutSeconds))
.build();

HttpPost httpPost = new HttpPost(uri);
httpPost.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
httpPost.setConfig(config);

return httpclient.execute(httpPost, response -> {
var contentTypeHeader = response.getHeader("Content-Type");
return new Response(contentTypeHeader.toString(), EntityUtils.toByteArray(response.getEntity()));
});
}
}

private static boolean isFormData(@Nullable Header contentTypeHeader) {
if (contentTypeHeader == null) {
return false;
}
return contentTypeHeader.toString().contains("multipart/form-data");
}

public String get(URI uri) throws IOException{
try (var client = HttpClients.custom().build()) {
var get = new HttpGet(uri);
return client.execute(get, response -> EntityUtils.toString(response.getEntity()));
}
}
}
}

0 comments on commit e21e0cc

Please sign in to comment.