All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.xlrit.gears.runner.graphql.GraphQLClient Maven / Gradle / Ivy

The newest version!
package com.xlrit.gears.runner.graphql;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.channels.Channels;
import java.nio.channels.Pipe;
import java.util.Objects;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.xlrit.gears.runner.runnertarget.InterpreterException;
import com.xlrit.gears.runner.utils.MiscUtils;
import lombok.Setter;
import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.StringBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GraphQLClient {
	private static final Logger LOG = LoggerFactory.getLogger(GraphQLClient.class);
	private static final ContentType JSON_GRAPHQL = ContentType.create("application/json+graphql");

	private final String url;
	private final ObjectMapper objectMapper;
	private final HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();

	@Setter
	private String token = null;

	public GraphQLClient(String url, ObjectMapper objectMapper) {
		this.url = url;
		this.objectMapper = objectMapper;
	}

	public JsonNode invoke(Operation operation) {
		try {
			HttpRequest request = makeRequest(operation);

			HttpResponse response = sendRequest(request);
			JsonNode rootNode = objectMapper.readTree(response.body());

			LOG.debug("operation={}, query=\n{}, response={}", operation.name, operation.query, rootNode);

			JsonNode errorsNode = rootNode.at("/errors");
			if (!errorsNode.isEmpty()) {
				throw new GraphQLErrorsException(operation, errorsNode);
			}
			return rootNode.get("data");
		}
		catch (URISyntaxException | GraphQLErrorsException | IOException e) {
			throw new InterpreterException(e.getMessage(), e);
		}
	}

	private HttpRequest makeRequest(Operation operation) throws URISyntaxException, IOException {
		String query = operation.query.replace('\'', '"');
		ObjectNode payload = objectMapper.createObjectNode();
		payload.put("query", query);
		payload.set("variables", objectMapper.valueToTree(operation.variables));

		HttpRequest.Builder requestBuild = HttpRequest
				.newBuilder()
				.uri(new URI(url));
		if (token != null) requestBuild.header("Authorization", "BEARER " + token);

		if (operation.files == null) {
			return requestBuild
				.header("Content-Type", "application/json")
				.header("Accept", "application/json")
				.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(payload)))
				.build();
		}

		ObjectNode map = objectMapper.createObjectNode();
		for (int i = 0; i < operation.files.size(); i++) {
			map.set(String.format("file%d", i),
					objectMapper.createArrayNode().add(objectMapper.readTree(String.format("\"variables.files.%d\"", i))));
		}

		MultipartEntityBuilder meb = MultipartEntityBuilder.create()
			.addPart("operations", new StringBody(objectMapper.writeValueAsString(payload), JSON_GRAPHQL))
			.addPart("map", new StringBody(objectMapper.writeValueAsString(map), ContentType.TEXT_PLAIN));
		int i = 0;
		for (File file : operation.files) {
			ContentType contentType = MiscUtils.getContentType(file.getName());
			meb.addBinaryBody(String.format("file%d", i++), file, contentType, file.getName());
		}

		HttpEntity httpEntity = meb.build();
		Pipe pipe = Pipe.open();
		new Thread(() -> {
			try (OutputStream os = Channels.newOutputStream(pipe.sink())) {
				httpEntity.writeTo(os);
			} catch (IOException e) {
				e.printStackTrace();
				throw new InterpreterException(e.getMessage(), e.getCause());
			}
		}).start();

		return requestBuild
			.header("Content-Type", httpEntity.getContentType().getValue())
			.POST(HttpRequest.BodyPublishers.ofInputStream(() -> Channels.newInputStream(pipe.source())))
			.build();
	}

	@SuppressWarnings("BusyWait")
	private HttpResponse sendRequest(HttpRequest request) throws IOException {
		try {
			while (true) {
				try {
					return client.send(request, HttpResponse.BodyHandlers.ofString());
				}
				catch (IOException e) {
					long delay = getRetryDelay(e);
					if (delay > 0) {
						LOG.info("Retrying HTTP request after {} ms", delay);
						Thread.sleep(delay);
					}
					else throw e;
				}
			}
		}
		catch (InterruptedException e) {
			// this shouldn't happen, but handle it anyway
			Thread.currentThread().interrupt();
			throw new InterpreterException("HTTP request/sleep was interrupted: " + e.getMessage(), e);
		}
	}

	private long getRetryDelay(IOException e) {
		String msg = Objects.requireNonNullElse(e.getMessage(), "");
		if (msg.contains("GOAWAY received")) return 1000L; // retry after one second on HTTP2 "GOAWAY received"
		return -1L; // don't retry
	}

	private static class GraphQLErrorsException extends Exception {
		private final String message;

		public GraphQLErrorsException(Operation operation, JsonNode errorsNode) {
			message = String.format("Error during operation %s: %s", operation.name, errorsNode);
		}

		@Override
		public String getMessage() {
			return message;
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy