Step by step guide to build a GraphQL server in Java over multiple data sources

Benjamin Habegger
17 min readFeb 20, 2024

--

This is a step-by-step guide to build your own GraphQL server in Java using the graphql-java library. In my previous job we built a GraphQL java application serving more than 10M requests a day. So this library has proven itself in a productive environment. Furthermore, GraphQL is an excellent choice if your goal is to integrate data from multiple data sources independently of the data volatility.

In this tutorial we will be building a GraphQL server allowing to query and retrieve data about countries from different sources using their APIs. We will be using: the CIA World Fact Book’s API as a basis and combining it with DBPedia to provide an integrated view allowing to write queries over both sources in a seamless way from a client’s perspective.

Step 1 — Basic project structure

One of the cool things about graphql-java is that it is not a framework pulling in tons of dependencies but a library. It provides all you need to process and execute GraphQL queries such as schema parsing, wiring of data fetchers. In particular, it does not impose and network communication layers. You could just as well write a CLI tool, receive GraphQL queries through JMS or in a more standard way, process GraphQL queries through HTTP.

In this project, we will stick to the bare minimum to keep it clear what is query execution separate from the request and response communication layers. We will be using unit testing to show how the library works.

Below is the basic initial project structure using maven:

src
main
java
tech.habegger.graphql.example
data
StaticData.java
fetching
StaticCountriesDataFetcher.java
model
Country.java
GraphQLExampleRuntime.java
resources
schema.graphqls
test
java
tech.habegger.graphql.example
GraphQLExampleRuntimeTest.java
pom.xml

We are keeping the pom.xml to a bare minimum:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>graphql-example</artifactId>
<groupId>tech.habegger</groupId>
<packaging>jar</packaging>
<version>0.0.0-SNAPSHOT</version>
<description>GraphQL Server in Java</description>

<properties>
<jvm.xms>64</jvm.xms>
<jvm.xmx>1024</jvm.xmx>
<main.class>tech.habegger.graphql.example.GraphQLMain</main.class>
<maven.compiler.release>17</maven.compiler.release>

<junit.version>5.8.0</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java</artifactId>
<version>21.3</version>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.21.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
</plugins>
</build>
</project>

Step 2— Define your domain model

In this tutorial we will be using SDL (Schema Definition Language) to declare the different types of our domain. There are many approaches to define your schema, but this is a good place to start.

type Query {
countries: [Country!]!
}

type Country {
isoCode: String!
name: String
}

First we need to define a top level Query type which will be the root of any query. In this type, we defined the entry point countries which is a list of countries. For the time being we will keep it simple to focus on the basics.

We are going to save this file as schema.graphqls as listed above

The schema is used by the graphql engine to validate and pilot queries. The GraphQL engine works on a field level and will walk down the query tree fetching field by field using the result of the parent as source for the underlying field.

For example, the following GraphQL query requests the isoCode-s and name-s of the countries:

{
countries {
isoCode
name
}
}

As, per the schema above, the countries fields is a non-null list of non-null entries of type Country. This is at the GraphQL level in terms of what can be expected. This is the contract between the GraphQL engine and the DataFetchers. The results of our DataFetcher-s need to respect this contract at the field level. Aka, a non null type must always return something and a list type must be iterable in some way. How this is done is mostly up to the implementation and the data sources to be integrated. It is also from this freedom that comes GraphQL powerful integration capabilities: I can receive basic data from a REST call but then plugin in a field from a completely different source and the GraphQL library will do the magic for me.

However, very often, we will be receiving either POJOs (Plain Old Java Object) or Maps of Maps type of structures as our data (depending on how much we need or want runtime strong typing, knowing that we do have type safety at the query level through the schema). This is why it can be convenient to have a corresponding POJO version of the GraphQL model.

So let’s define our Country as a Java record:

package tech.habegger.graphql.example.model;

public record Country(String isoCode, String name) {
}

Step 3 — Basic setup with static data

In order to get a first feel of how the graphql-java library works, let’s setup some static data and use that as the source of our brand new query engine.

Let’s statically define a set of countries (you can adjust to your own taste, I just chose some countries that I have some personal link to):

package tech.habegger.graphql.example.data;

import tech.habegger.graphql.example.model.Country;

import java.util.List;

public class StaticData {
public static List<Country> COUNTRIES = List.of(
new Country("CH", "Switzerland"),
new Country("US", "United States Of America"),
new Country("FR", "France"),
new Country("ES", "Spain")
);
}

Now we need to tell GraphQL that when we request for countries these countries should be returned. This is done through a DataFetcher we will then plug in further below. So let’s define a StaticCountriesDataFetcher:

package tech.habegger.graphql.example.fetching;

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import tech.habegger.graphql.example.model.Country;

import java.util.List;

import static tech.habegger.graphql.example.data.StaticData.COUNTRIES;

public class StaticCountriesDataFetcher implements DataFetcher<List<Country>> {
@Override
public List<Country> get(DataFetchingEnvironment environment) {
return COUNTRIES;
}
}

As you can see this data fetcher just needs to know how to fetch, here we just return the static list define previously.

The final piece is to wire everything together. For this, we’ll need to setup our own GraphQL runtime at application level:

  • Load and parse the schema
  • Instantiate and wire the DataFetchers

And setup the query processing which will:

  • Create a proper GraphQL ExecutionInput
  • Delegate the execution to GraphQL

This looks something like this:

package tech.habegger.graphql.example;

import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import tech.habegger.graphql.example.fetching.StaticCountriesDataFetcher;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.Map;

public class GraphQLExampleRuntime {

GraphQL graphQL;

public GraphQLExampleRuntime() throws IOException {
SchemaGenerator schemaGenerator = new SchemaGenerator();
TypeDefinitionRegistry typeRegistry = buildRegistry();
RuntimeWiring runtimeWiring = buildWiring();
GraphQLSchema schema = schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
graphQL = GraphQL.newGraphQL(schema).build();
}

public ExecutionResult execute(String query, Map<String, Object> variables) {
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(query)
.variables(variables)
.build();
return graphQL.execute(executionInput);
}

static TypeDefinitionRegistry buildRegistry() throws IOException {
SchemaParser schemaParser = new SchemaParser();
try(InputStream is = GraphQLExampleRuntime.class.getResourceAsStream("/schema.graphqls")) {
assert is != null;
Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8);
return schemaParser.parse(reader);
}
}

static RuntimeWiring buildWiring() {
RuntimeWiring.Builder runtimeWiring = RuntimeWiring.newRuntimeWiring();
runtimeWiring = runtimeWiring.type("Query", builder ->
builder.dataFetcher("countries", new StaticCountriesDataFetcher())
);
return runtimeWiring.build();
}
}

In the constructor you can see that to build a GraphQL instance we need a typeRegistry on one end and a runtimeWiring on the other.

In our case, we will obtain the typeRegistry by parsing the schema.graphqls resource (which is exactly what the buildRegistry method does). This is a very simple and standard case, but here there could have been multitudes of ways and combinations to build the TypeDefinitionRegistry. I’ve even experimented cases where the type registry was populated dynamically based on user’s permissions so that user’s could only request data they were authorized to.

The runtime wiring simply registers our StaticCountriesDataFetcher on the countries field of the Query entry type. The Query type defines all the top level entry points to our GraphQL service (and here we only have one). For the rest we rely on the fact that unwired fields use the default DataFetcher which defaults to the PropertyDataFetcher. This data fetcher will do an in memory field lookup based on the source object which, among others, can be a simple POJO or a Map.

Step 4— Unit test it

Let’s write our first GraphQL query and make it run.

package tech.habegger.graphql.example;


import graphql.ExecutionResult;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

class GraphQLExampleRuntimeTest {

GraphQLExampleRuntime runtime;

GraphQLExampleRuntimeTest() throws IOException {
runtime = new GraphQLExampleRuntime();
}

@Test
public void simpleQuery() {
// Given
String query = """
{
countries {
isoCode
name
}
}
""";

// When
ExecutionResult result = runtime.execute(query, Map.of());

// Then
Map<String, Object> data = result.getData();
assertThat(data).isNotNull();
assertThat(data.get("countries")).isInstanceOf(List.class)
.asList().hasSize(4);
}
}

This first version of our GraphQL server is available here : https://gitlab.com/bhabegger/graphql-example/-/tree/static?ref_type=tags

Step 5 — Adding filtering

Currently our countries endpoint returns all the data. In reality we will likely want to allow users to select which countries they want to retrieve. To allow for this we need to declare parameters on the countries field an properly process them. Let’s update the code in this direction.

First we need to update the schema.graphqls model:

...

type Query {
countries(isoCodes: [String!]): [Country!]
}

This allows the query to specify a list of String named isoCodes. We now need to adjust our data fetcher to take this new parameter into account (without it the parameter will just be simply ignored).

...
@Override
public List<Country> get(DataFetchingEnvironment environment) {
List<String> wantedIsoCodes = environment.getArgument("isoCodes");
if(wantedIsoCodes == null) {
return COUNTRIES;
} else {
return COUNTRIES.stream()
.filter(country -> wantedIsoCodes.contains(country.isoCode()))
.toList();
}
}
...

Here if we don’t provide isoCodes the argument will be null and if we do then we will have a list of isoCodes to check against and so here we will limit the countries to those selected.

To make sure everything works as expected, let’s add a new test:

...
@Test
public void queryWithFiltering() {
// Given
String query = """
{
countries(isoCodes: ["CH"]) {
isoCode
name
}
}
""";

// When
ExecutionResult result = runtime.execute(query, Map.of());

// Then
assertThat(result.getErrors()).isEmpty();
Map<String, Object> data = result.getData();
assertThat(data).isNotNull();
assertThat(data.get("countries")).isInstanceOf(List.class)
.asList().hasSize(1);
}
...

Here we now have a single entry returned. (I’ll leave as an exercise as a more thorough test to check that indeed we are returning Switzerland, because that’s off topic here).

The version with filtering can be found here : https://gitlab.com/bhabegger/graphql-example/-/tree/filtering?ref_type=tags

Now that we have an initial version, we don’t have “real” data so let’s get some in.

Step 6— Identifying the data endpoints

The CIA World Fact Book (we’ll abreviate as WFB below) is not very volatile data it is only updated yearly. It is mainly HTML pages with differents about countries across the world. There is no real API to access it but there is a github repository of JSON files : https://github.com/factbook/factbook.json

Each country has a static JSON file in the shape:
https://github.com/factbook/factbook.json/blob/master/<continent>/<country-code>.json

For example for Switzerland, this page holds the data:
https://github.com/factbook/factbook.json/blob/master/europe/sz.json

Now we hit 2 challenges to integrate this data into our GraphQL API.

  • To be able to access the page of a country we need to know under which continent it’s stored (or subdivision, because some are not really continents).
  • The 2 letter codes are not ISO codes

This is a classical data integration problem, so for now let’s just take a small step and create a static map from country code to the WFB continent and specific code.

package tech.habegger.graphql.example.data;

public enum ISOCodes {
CH("europe", "sz"),
FR("europe", "fr"),
ES("europe", "sp"),
US("north-america", "us");

private final String wfbContinent;
private final String wfbCode;

ISOCodes(String wfbContinent, String wfbCode) {
this.wfbContinent = wfbContinent;
this.wfbCode = wfbCode;
}
}

There are many other ways to solve this problem (mostly all by creating some for of index which could be static or not). Here which choice is the “best” mostly depends on how volatile the data is (aka how often it changes) and how fresh we want or need to be. But that’s another topic.

Using this map we can now “translate” a user given ISO code into a WFB URL to be able to fetch the real data. First, we should also update our model to be able to hold the data we want to get out of WFB.

Step 7 — Adjusting the model

Let’s start with some basic data: name, capital, area and population.

First we should update the GraphQL model:

type Country {
isoCode: String!
name: String
capital: String
area: String
population: String
}

type Query {
countries(isoCodes: [String!]!): [Country!]
}

(For simplicity we will just return the data raw without any particular processing, and therefore use String as type for all fields).

We also modified, slightly the Query to impose a non null isoCodes parameter as we will now want that clients do provide a list of codes as we will no longer want to send back, and fetch, all countries with a single call.

For the time being let’s ignore the isoCode field as returning the input field is a little trickier and that the WFB data doesn’t contain it. We’ll bring it back in when integrating data from DBPedia.

Step 8 — Choosing the DataFetching approach

Here we do need to figure out where this information is found inside the WFB JSON files and create the proper DataFetchers to be able to process this data.

Below is for each field where the data will be found.

  • area: Geography/Area/total/text
  • population: People and Society/Population/text
  • name: Government/Country name/conventional short form/text
  • capital: Government/Capital/name/text

Now we have different options to implement our DataFetchers:

  • Use a single data fetcher to fetch and remap the data into a know structure and let the property data fetchers do the rest. This means adjusting the Country POJO to match the type defined above and add a WFB to Country data converter. Here the whole conversion would be done independently of the requested fields. This could be fine, especially in case the converted data was stored in cache..
  • Use a data fetcher for fetching the data and specialized DataFetchers for each field. We will use this approach in this tutorial.

Step 9 — Implementing the top level DataFetcher

Let’s start with the first DataFetcher allowing to, given a list of codes, make the HTTP Requests to fetch the corresponding data from github. We will be working with Jackson to process the Json data so first we need a new dependencies in our pom.xml:

...
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.16.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
...

As for the HttpClient, we simply uses Java standardized HttpClient present since Java 11, so no dependencies there.

Now our top level DataFetcher will work with a list of arguments which are expected to be valid ISO Codes. For each we will use the previously defined ISOCodes enum to be able to map them back into WFB continent and country code. From this pair we will construct the proper URI and use the HttpClient to fetch the Json data returned as a JsonNode. The list of JsonNode-s will be the output of this DataFetcher. GraphQL will then pass these down as the source for the field value extraction.

package tech.habegger.graphql.example.fetching;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import tech.habegger.graphql.example.data.ISOCodes;

import java.io.IOException;
import java.io.InputStream;
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.util.ArrayList;
import java.util.List;

public class WFBCountriesDataFetcher implements DataFetcher<List<JsonNode>> {
private final static String WFB_URI_PATTERN = "https://raw.githubusercontent.com/factbook/factbook.json/master/%s/%s.json";
private final static ObjectMapper MAPPER = new ObjectMapper();
private final HttpClient httpClient = HttpClient.newHttpClient();

@Override
public List<JsonNode> get(DataFetchingEnvironment environment) throws Exception {
List<String> wantedIsoCodes = environment.getArgument("isoCodes");
ArrayList<JsonNode> results = new ArrayList<>();
for(var isoCode: wantedIsoCodes) {
results.add(fetchWfbCountry(isoCode));
}
return results;
}

private JsonNode fetchWfbCountry(String isoCode) throws IOException, InterruptedException {
URI location = wfbLocationOf(ISOCodes.valueOf(isoCode));
return fetch(location);
}

private URI wfbLocationOf(ISOCodes isoCodes) {
try {
return new URI(WFB_URI_PATTERN.formatted(isoCodes.getWfbContinent(), isoCodes.getWfbCode()));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}

private JsonNode fetch(URI location) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(location).GET().build();
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
return MAPPER.readTree(response.body());
}
}

Here we split into multiple methods for proper separation of concern:

  • get does the top level loop for each argument delegating to fetchWfbCountry
  • fetchWfbCountry is the functional method, the what, to fetch the WFB data for a given country delegating to the two how’s wfbLocationOf and fetch
  • wfbLocation is the technical method calculating the github URI of a given ISOCodes entry
  • fetch is the technical method doing the effective fetching using HttpClient

Step 10 — Implementing the field DataFetcher’s

After GraphQL has fetched the countries top level field it will then seek to “fetch” the fields that were chosen by the client in the GraphQL query. In our case, the fetching has been done at the top level node, but what still remains is extracting the proper data from the raw payload. One of the nice things with the GraphQL design is that here we only need to focus on the extraction of one field for one given top level entity. Managing the list will be done for us internally by the GraphQL library.

Here each of our field Data Fetchers will in fact extract one path within the pay load so let’s write a generic DataFetcher allowing to extract a particular path given at instantiation time from a JsonNode payload.

package tech.habegger.graphql.example.fetching;

import com.fasterxml.jackson.databind.JsonNode;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;

public class WFBFieldDataFetcher implements DataFetcher<String> {
private final String jsonPath;

public WFBFieldDataFetcher(String jsonPath) {
this.jsonPath = jsonPath;
}

@Override
public String get(DataFetchingEnvironment environment) throws Exception {
JsonNode rawPayload = environment.getSource();
return rawPayload.at(jsonPath).asText();
}
}

Here this DataFetcher takes a jsonPath (e.g. “/Geography/Area/total/text”) as constructor parameter and then uses this path to extract the proper data from the payload which will be the source of this DataFetcher.

Step 11 — Wiring it all together

The only piece of the puzzle remaining is now to wire everything together and then let the GraphQL library do it’s magic.

 ...
static RuntimeWiring buildWiring() {
RuntimeWiring.Builder runtimeWiring = RuntimeWiring.newRuntimeWiring()
.type("Query", builder -> builder
.dataFetcher("countries", new WFBCountriesDataFetcher())
)
.type("Country", builder -> builder
.dataFetcher("area", new WFBFieldDataFetcher("/Geography/Area/total/text"))
.dataFetcher("population", new WFBFieldDataFetcher("/People and Society/Population/text"))
.dataFetcher("name", new WFBFieldDataFetcher("/Government/Country name/conventional short form/text"))
.dataFetcher("capital", new WFBFieldDataFetcher("/Government/Capital/name/text"))
);
return runtimeWiring.build();
}
...

The beauty of this is that, now, adding a new field now becomes quite straight forward: add the field to the model, identify it’s path in the payload, properly declare the wiring.

Step 12 — Testing the new setup

Let’s adjust the tests to match what we just implemented. First of all we can remove the first test which we voluntarily made not possible anymore.

Here is the second test which now queries the for fields and checks that the data output is indeed the one from the WFB on github. Please note that this test is flaky as is using the current state of the data on github. Ideally, the HttpClient should be mocked to return some statically set payload , but this tutorial is about GraphQL not testing.

    @Test
public void queryWithFiltering() {
// Given
String query = """
{
countries(isoCodes: ["CH"]) {
name
population
area
capital
}
}
""";

// When
ExecutionResult result = runtime.execute(query, Map.of());

// Then
assertThat(result.getErrors()).isEmpty();
Map<String, Object> data = result.getData();
assertThat(data).isNotNull();
assertThat(data.get("countries")).isInstanceOf(List.class);
List<Map<String, Object>> countries = (List<Map<String, Object>>) data.get("countries");
assertThat(countries).hasSize(1);
Map<String, Object> switzerland = countries.get(0);
assertThat(switzerland).extracting("name").isEqualTo("Switzerland");
assertThat(switzerland).extracting("population").isEqualTo("8,563,760 (2023 est.)");
assertThat(switzerland).extracting("area").isEqualTo("41,277 sq km");
assertThat(switzerland).extracting("capital").isEqualTo("Bern");
}

This version is available here: https://gitlab.com/bhabegger/graphql-example/-/tree/world-fact-book?ref_type=tags

Now that we have integrated some data from the CIA World Fact Book, let’s add in to the mix some data from Wikipedia via DBPedia’s SPARQL online engine which gives access to the linked data of Wikipedia via SPARQL.

For our tutorial, we will just add another field, let’s say the country’s currency. In DBPedia this is given by the dbp:currency property of instances of the dbo:country class.

In our setting we are refering to the country via it’s ISO Code, so here we will use the fact that and ISO_3166-XX entity redirects to the proper country of code XX using dbo:wikiPageRedirects relationship.

This is how the SPARQL query to get the currency for France the might look like (whose result you can see here):

select * where {
?country a dbo:Country.
dbr:ISO_3166-1:FR dbo:wikiPageRedirects ?country.
?country dbp:currency ?currency.
FILTER ( LANG ( ?currency ) = 'en' && ?currency != ""@en)
} LIMIT 1

Step 13 — Adjusting our DataFetcher to keep isoCode

Now we face the challenge that we will need the isoCode to be able to query DBPedia, and furthermore we only want to query World Fact Books if we have fields sourced from there.

Here we will do this by adjust our toplevel data fetcher to return not directly the WFB content, but a special data structure containing the isoCode on one hand and a WorldFactBook data supplier which will only be called if need be.

Let’s first define a simple CountryAccess structure which allows storing the isoCode and the WFB data supplier:

package tech.habegger.graphql.example.fetching;

import com.fasterxml.jackson.databind.JsonNode;
import java.util.function.Supplier;

public record CountryAccess(String isoCode, Supplier<JsonNode> wfbData) {
}

And adjust our WFBCountryDataFetcher:

...
@Override
public List<CountryAccess> get(DataFetchingEnvironment environment) throws Exception {
List<String> wantedIsoCodes = environment.getArgument("isoCodes");
ArrayList<CountryAccess> results = new ArrayList<>();
for(var isoCode: wantedIsoCodes) {
results.add(new CountryAccess(isoCode, () -> fetchWfbCountry(isoCode)));
}
return results;
}
...
private JsonNode fetchWfbCountry(String isoCode) {
URI location = wfbLocationOf(ISOCodes.valueOf(isoCode));
try {
return fetch(location);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
...

And our WFBFieldDataFetcher:

...
@Override
public String get(DataFetchingEnvironment environment) {
CountryAccess countryAccess = environment.getSource();
return countryAccess.wfbData().get().at(jsonPath).asText();
}
...

At this point, with these small changes, our previous test continues to work fine. Even better as isoCode is a direct field of CountryAccess underneath, we can even retrieve it back without further code.

Let’s adjust our test to prove this:

...
@Test
public void queryWithFiltering() {
// Given
String query = """
{
countries(isoCodes: ["CH"]) {
isoCode
name
population
area
capital
}
}
""";

// When
ExecutionResult result = runtime.execute(query, Map.of());

// Then
assertThat(result.getErrors()).isEmpty();
Map<String, Object> data = result.getData();
assertThat(data).isNotNull();
assertThat(data.get("countries")).isInstanceOf(List.class);
List<Map<String, Object>> countries = (List<Map<String, Object>>) data.get("countries");
assertThat(countries).hasSize(1);
Map<String, Object> switzerland = countries.get(0);
assertThat(switzerland).extracting("isoCode").isEqualTo("CH");
assertThat(switzerland).extracting("name").isEqualTo("Switzerland");
assertThat(switzerland).extracting("population").isEqualTo("8,563,760 (2023 est.)");
assertThat(switzerland).extracting("area").isEqualTo("41,277 sq km");
assertThat(switzerland).extracting("capital").isEqualTo("Bern");
}
...

Step 14 — Externalize our HTTP fetching

As we will also be using HTTP to fetch data from DBPedia as we are doing for World Fact Book data let’s externalize the fetching code into SimpleHttpClient class:

package tech.habegger.graphql.example;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class SimpleHttpClient {
private final static ObjectMapper MAPPER = new ObjectMapper();
private final HttpClient httpClient = HttpClient.newHttpClient();

public JsonNode fetch(URI location) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(location).GET().build();
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
return MAPPER.readTree(response.body());
}

public JsonNode fetchUnchecked(URI location) {
try {
return fetch(location);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}

And adjust the WFBCountriesDataFetcher accordingly:

...
public class WFBCountriesDataFetcher implements DataFetcher<List<CountryAccess>> {
private final static String WFB_URI_PATTERN = "https://raw.githubusercontent.com/factbook/factbook.json/master/%s/%s.json";
private final SimpleHttpClient httpClient;

public WFBCountriesDataFetcher(SimpleHttpClient httpClient) {
this.httpClient = httpClient;
}

...
private JsonNode fetchWfbCountry(String isoCode) {
URI location = wfbLocationOf(ISOCodes.valueOf(isoCode));
return httpClient.fetchUnchecked(location);
}
...
}

And the wiring:

...    
static RuntimeWiring buildWiring() {
SimpleHttpClient httpClient = new SimpleHttpClient();

RuntimeWiring.Builder runtimeWiring = RuntimeWiring.newRuntimeWiring()
.type("Query", builder -> builder
.dataFetcher("countries", new WFBCountriesDataFetcher(httpClient))
)
...
}
...

Step 14 — Plugin in the currency field from DBPedia

First we need to declare our new field in the schema:

type Country {
isoCode: String!
name: String
capital: String
area: String
population: String
currency: String
}
...

And create a currency DataFetcher:

package tech.habegger.graphql.example.fetching;

import com.fasterxml.jackson.databind.JsonNode;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import tech.habegger.graphql.example.SimpleHttpClient;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

public class DBPediaCountryCurrencyDataFetcher implements DataFetcher<String> {
private static final String DBPEDIA_QUERY = "https://dbpedia.org/sparql?" +
"default-graph-uri=http%%3A%%2F%%2Fdbpedia.org&" +
"query=%s&" +
"format=application%%2Fsparql-results%%2Bjson&" +
"timeout=30000&" +
"signal_void=on&" +
"signal_unconnected=on";
private static final String CURRENCY_QUERY = """
select * where {
?country a dbo:Country.
dbr:ISO_3166-1:%s dbo:wikiPageRedirects ?country.
?country dbo:currency ?currency.
?currency dbp:isoCode ?currencyCode.
} LIMIT 1
""";
private final SimpleHttpClient httpClient;

public DBPediaCountryCurrencyDataFetcher(SimpleHttpClient httpClient) {
this.httpClient = httpClient;
}

@Override
public String get(DataFetchingEnvironment environment) throws Exception {
CountryAccess countryAccess = environment.getSource();
return fetchCountryCurrency(countryAccess.isoCode());
}

private String fetchCountryCurrency(String isoCode) throws IOException, InterruptedException {
URI location = dbpediaCurrencyLookupLocation(isoCode);
JsonNode result = httpClient.fetch(location);
return result.at("/results/bindings/0/currencyCode/value").asText();
}

private URI dbpediaCurrencyLookupLocation(String isoCode) {
String sparqlQuery = CURRENCY_QUERY.formatted(isoCode);
String encodedQuery = URLEncoder.encode(sparqlQuery, StandardCharsets.UTF_8);
try {
return new URI(DBPEDIA_QUERY.formatted(encodedQuery));
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}

Do note the quite hidden “%s” in both CURRENCY_QUERY and DBPEDIA_QUERY allowing to inject, respectively, the isoCode and then the SPARQL query itself.

Here our data extraction is not very smart, it simply assumes that there will be one result. But JsonNode.at() should return null for non-found nodes, which is in fact a good response.

And wire it all:

...
static RuntimeWiring buildWiring() {
SimpleHttpClient httpClient = new SimpleHttpClient();

RuntimeWiring.Builder runtimeWiring = RuntimeWiring.newRuntimeWiring()
.type("Query", builder -> builder
.dataFetcher("countries", new WFBCountriesDataFetcher(httpClient))
)
.type("Country", builder -> builder
.dataFetcher("area", new WFBFieldDataFetcher("/Geography/Area/total/text"))
.dataFetcher("population", new WFBFieldDataFetcher("/People and Society/Population/text"))
.dataFetcher("name", new WFBFieldDataFetcher("/Government/Country name/conventional short form/text"))
.dataFetcher("capital", new WFBFieldDataFetcher("/Government/Capital/name/text"))
.dataFetcher("currency", new DBPediaCountryCurrencyDataFetcher(httpClient))
);
return runtimeWiring.build();
}
...

Step 15 — Finalize the test

And now our complete test which allows to fetch data from both CIA World Fact book and DBPedia as well as return the input ISOCurrency code in one single GraphQL query:

package tech.habegger.graphql.example;


import graphql.ExecutionResult;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

class GraphQLExampleRuntimeTest {

GraphQLExampleRuntime runtime;

GraphQLExampleRuntimeTest() throws IOException {
runtime = new GraphQLExampleRuntime();
}

@Test
public void queryWithFiltering() {
// Given
String query = """
{
countries(isoCodes: ["CH"]) {
isoCode
name
population
area
capital
currency
}
}
""";

// When
ExecutionResult result = runtime.execute(query, Map.of());

// Then
assertThat(result.getErrors()).isEmpty();
Map<String, Object> data = result.getData();
assertThat(data).isNotNull();
assertThat(data.get("countries")).isInstanceOf(List.class);
List<Map<String, Object>> countries = (List<Map<String, Object>>) data.get("countries");
assertThat(countries).hasSize(1);
Map<String, Object> switzerland = countries.get(0);
assertThat(switzerland).extracting("isoCode").isEqualTo("CH");
assertThat(switzerland).extracting("name").isEqualTo("Switzerland");
assertThat(switzerland).extracting("population").isEqualTo("8,563,760 (2023 est.)");
assertThat(switzerland).extracting("area").isEqualTo("41,277 sq km");
assertThat(switzerland).extracting("capital").isEqualTo("Bern");
assertThat(switzerland).extracting("currency").isEqualTo("CHF");
}
}

This final version is available here : https://gitlab.com/bhabegger/graphql-example/-/tree/dbpedia?ref_type=tags

You liked this tutorial and want to learn more ? Go further by checking out my course on Udemy:

https://www.udemy.com/course/learn-graphql-in-java

--

--

Responses (1)