Appearance
Using the API from Java
This example uses the HTTP client built into Java 11 and later (java.net.http) and Jackson for JSON, because the JDK has no JSON parser. It needs Java 17 or later (for text blocks and records), an API token, and the COCOCO_URL and COCOCO_TOKEN environment variables described in Calling the CoCoCo API.
The operations
graphql
query ListJobs($first: Int, $after: String) {
listJobs(first: $first, after: $after) {
totalCount
pageInfo { hasNextPage endCursor }
edges { node { id name quantity status dueAt } }
}
}graphql
mutation CreateJob($input: CreateJobInput!) {
createJob(input: $input) {
job { id name status }
errors { path message }
}
}The program
Add Jackson to your build. With Maven:
xml
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.2</version>
</dependency>Save this as Jobs.java. Running it with no arguments lists every job; running it with the argument create creates one.
java
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Jobs {
static final String LIST_JOBS = """
query ListJobs($first: Int, $after: String) {
listJobs(first: $first, after: $after) {
totalCount
pageInfo { hasNextPage endCursor }
edges { node { id name quantity status dueAt } }
}
}""";
static final String CREATE_JOB = """
mutation CreateJob($input: CreateJobInput!) {
createJob(input: $input) {
job { id name status }
errors { path message }
}
}""";
/** A job as the two operations select it. dueAt is ISO 8601 or null. */
record Job(String id, String name, int quantity, String status, String dueAt) {}
static final ObjectMapper JSON = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
static final HttpClient HTTP = HttpClient.newHttpClient();
static final String ENDPOINT = System.getenv("COCOCO_URL").replaceAll("/$", "") + "/graphql";
static final String TOKEN = System.getenv("COCOCO_TOKEN");
/** Posts one operation and returns its data. Throws on an HTTP error or any top-level error. */
static JsonNode graphql(String query, Map<String, Object> variables)
throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(URI.create(ENDPOINT))
.timeout(Duration.ofSeconds(30))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
JSON.writeValueAsString(Map.of("query", query, "variables", variables))))
.build();
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode() + ": " + response.body());
}
JsonNode result = JSON.readTree(response.body());
// Authentication, permission and query errors arrive here with HTTP 200.
JsonNode errors = result.path("errors");
if (!errors.isEmpty()) {
List<String> messages = new ArrayList<>();
errors.forEach(e -> messages.add(e.path("message").asText()));
throw new IOException(String.join("; ", messages));
}
return result.get("data");
}
/** Returns every job, fetching 100 per request. */
static List<Job> listJobs() throws IOException, InterruptedException {
List<Job> jobs = new ArrayList<>();
String after = null;
while (true) {
Map<String, Object> variables = new HashMap<>(); // Map.of does not allow a null value
variables.put("first", 100);
variables.put("after", after);
JsonNode page = graphql(LIST_JOBS, variables).get("listJobs");
for (JsonNode edge : page.get("edges")) {
jobs.add(JSON.treeToValue(edge.get("node"), Job.class));
}
JsonNode pageInfo = page.get("pageInfo");
if (!pageInfo.get("hasNextPage").asBoolean()) {
return jobs;
}
after = pageInfo.get("endCursor").asText();
}
}
/** Creates a job. Validation problems come back in the payload, not as top-level errors. */
static Job createJob(String name, int quantity, String externalId)
throws IOException, InterruptedException {
Map<String, Object> input = Map.of("name", name, "quantity", quantity, "externalId", externalId);
JsonNode payload = graphql(CREATE_JOB, Map.of("input", input)).get("createJob");
JsonNode errors = payload.get("errors");
if (!errors.isEmpty()) {
JsonNode e = errors.get(0);
throw new IllegalStateException(e.get("path").asText() + ": " + e.get("message").asText());
}
return JSON.treeToValue(payload.get("job"), Job.class);
}
public static void main(String[] args) throws Exception {
if (args.length > 0 && args[0].equals("create")) {
Job job = createJob("Business cards", 500, "ERP-10042");
System.out.println("Created " + job.id() + " (" + job.status() + ")");
return;
}
for (Job job : listJobs()) {
System.out.println(job.id() + " " + job.status() + " " + job.quantity() + " " + job.name());
}
}
}Notes
graphql()throws for both kinds of failure: an HTTP error status, and a response whoseerrorsarray is not empty.listJobs()followspageInfo.endCursoruntilhasNextPageisfalse. The API returns at most 100 items per page.- The create mutation does not select
quantityordueAt, so on the returnedJobthey are0andnull. externalIdmust be unique in your tenant. Runningcreatetwice with the same value returns a validation error increateJob.errors.