Skip to content

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 whose errors array is not empty.
  • listJobs() follows pageInfo.endCursor until hasNextPage is false. The API returns at most 100 items per page.
  • The create mutation does not select quantity or dueAt, so on the returned Job they are 0 and null.
  • externalId must be unique in your tenant. Running create twice with the same value returns a validation error in createJob.errors.

Connect machines, automate workflows, and build custom apps for print production.