View Javadoc

1   /*
2    * Copyright 2015 Data Archiving and Networked Services (an institute of
3    * Koninklijke Nederlandse Akademie van Wetenschappen), King's College London,
4    * Georg-August-Universitaet Goettingen Stiftung Oeffentlichen Rechts
5    *
6    * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
7    * the European Commission - subsequent versions of the EUPL (the "Licence");
8    * You may not use this work except in compliance with the Licence.
9    * You may obtain a copy of the Licence at:
10   *
11   * https://joinup.ec.europa.eu/software/page/eupl
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the Licence is distributed on an "AS IS" basis,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the Licence for the specific language governing
17   * permissions and limitations under the Licence.
18   */
19  
20  package eu.ehri.project.graphql;
21  
22  import com.google.common.base.Preconditions;
23  import com.google.common.collect.ImmutableList;
24  import com.google.common.collect.Iterables;
25  import com.google.common.collect.Lists;
26  import com.google.common.collect.Maps;
27  import com.google.common.collect.Sets;
28  import eu.ehri.project.acl.AclManager;
29  import eu.ehri.project.api.Api;
30  import eu.ehri.project.api.EventsApi;
31  import eu.ehri.project.api.QueryApi;
32  import eu.ehri.project.definitions.ContactInfo;
33  import eu.ehri.project.definitions.CountryInfo;
34  import eu.ehri.project.definitions.DefinitionList;
35  import eu.ehri.project.definitions.Entities;
36  import eu.ehri.project.definitions.EventTypes;
37  import eu.ehri.project.definitions.Geo;
38  import eu.ehri.project.definitions.Isaar;
39  import eu.ehri.project.definitions.IsadG;
40  import eu.ehri.project.definitions.Isdiah;
41  import eu.ehri.project.definitions.Ontology;
42  import eu.ehri.project.definitions.Skos;
43  import eu.ehri.project.definitions.SkosMultilingual;
44  import eu.ehri.project.exceptions.ItemNotFound;
45  import eu.ehri.project.models.AccessPointType;
46  import eu.ehri.project.models.Annotation;
47  import eu.ehri.project.models.Country;
48  import eu.ehri.project.models.DocumentaryUnit;
49  import eu.ehri.project.models.EntityClass;
50  import eu.ehri.project.models.Link;
51  import eu.ehri.project.models.Repository;
52  import eu.ehri.project.models.RepositoryDescription;
53  import eu.ehri.project.models.annotations.EntityType;
54  import eu.ehri.project.models.base.Accessible;
55  import eu.ehri.project.models.base.Annotatable;
56  import eu.ehri.project.models.base.Described;
57  import eu.ehri.project.models.base.Description;
58  import eu.ehri.project.models.base.Entity;
59  import eu.ehri.project.models.base.Linkable;
60  import eu.ehri.project.models.base.Named;
61  import eu.ehri.project.models.base.Temporal;
62  import eu.ehri.project.models.cvoc.AuthoritativeSet;
63  import eu.ehri.project.models.cvoc.Concept;
64  import eu.ehri.project.models.cvoc.Vocabulary;
65  import eu.ehri.project.models.events.SystemEvent;
66  import eu.ehri.project.persistence.Bundle;
67  import eu.ehri.project.utils.LanguageHelpers;
68  import graphql.TypeResolutionEnvironment;
69  import graphql.schema.DataFetcher;
70  import graphql.schema.GraphQLArgument;
71  import graphql.schema.GraphQLEnumType;
72  import graphql.schema.GraphQLFieldDefinition;
73  import graphql.schema.GraphQLInterfaceType;
74  import graphql.schema.GraphQLList;
75  import graphql.schema.GraphQLNonNull;
76  import graphql.schema.GraphQLObjectType;
77  import graphql.schema.GraphQLOutputType;
78  import graphql.schema.GraphQLScalarType;
79  import graphql.schema.GraphQLSchema;
80  import graphql.schema.GraphQLTypeReference;
81  import graphql.schema.TypeResolver;
82  import org.apache.commons.codec.binary.Base64;
83  import org.slf4j.Logger;
84  import org.slf4j.LoggerFactory;
85  
86  import java.text.MessageFormat;
87  import java.util.Collections;
88  import java.util.List;
89  import java.util.Map;
90  import java.util.MissingResourceException;
91  import java.util.Objects;
92  import java.util.Optional;
93  import java.util.ResourceBundle;
94  import java.util.concurrent.atomic.AtomicInteger;
95  import java.util.function.Function;
96  import java.util.function.Supplier;
97  import java.util.stream.Collectors;
98  import java.util.stream.StreamSupport;
99  
100 import static graphql.Scalars.*;
101 import static graphql.schema.GraphQLArgument.newArgument;
102 import static graphql.schema.GraphQLEnumType.newEnum;
103 import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
104 import static graphql.schema.GraphQLInterfaceType.newInterface;
105 import static graphql.schema.GraphQLObjectType.newObject;
106 
107 /**
108  * Implementation of a GraphQL schema over the API
109  */
110 public class GraphQLImpl {
111 
112     private static final ResourceBundle bundle = ResourceBundle.getBundle("eu.ehri.project.graphql.messages");
113     private static final Logger logger = LoggerFactory.getLogger(GraphQLImpl.class);
114 
115     private static final String SLICE_PARAM = "at";
116     private static final String FIRST_PARAM = "first";
117     private static final String FROM_PARAM = "from";
118     private static final String AFTER_PARAM = "after";
119     private static final String ALL_PARAM = "all";
120 
121     private static final String HAS_PREVIOUS_PAGE = "hasPreviousPage";
122     private static final String HAS_NEXT_PAGE = "hasNextPage";
123     private static final String PAGE_INFO = "pageInfo";
124     private static final String ITEMS = "items";
125     private static final String EDGES = "edges";
126     private static final String NODE = "node";
127     private static final String CURSOR = "cursor";
128     private static final String NEXT_PAGE = "nextPage";
129     private static final String PREVIOUS_PAGE = "previousPage";
130 
131     private static final int DEFAULT_LIST_LIMIT = 40;
132     private static final int MAX_LIST_LIMIT = 100;
133 
134     private static final List<EntityClass> supportedTypes = ImmutableList.of(
135       EntityClass.DOCUMENTARY_UNIT,
136       EntityClass.REPOSITORY,
137       EntityClass.COUNTRY,
138       EntityClass.HISTORICAL_AGENT,
139       EntityClass.CVOC_CONCEPT,
140       EntityClass.CVOC_VOCABULARY,
141       EntityClass.AUTHORITATIVE_SET,
142       EntityClass.ANNOTATION,
143       EntityClass.LINK
144     );
145 
146     private static final List<EventTypes> supportedEvents = ImmutableList.of(
147       EventTypes.creation,
148       EventTypes.modification,
149       EventTypes.deletion,
150       EventTypes.ingest,
151       EventTypes.annotation
152     );
153 
154     private final Api _api;
155     private final boolean stream;
156 
157     public GraphQLImpl(Api api, boolean stream) {
158         this._api = api;
159         this.stream = stream;
160     }
161 
162     public GraphQLImpl(Api api) {
163         this(api, false);
164     }
165 
166     public GraphQLSchema getSchema() {
167         return GraphQLSchema.newSchema()
168                 .query(queryType())
169                 // NB: this needed because the date type is only
170                 // references via a type reference to avoid forward-
171                 // declaration problems...
172                 .additionalTypes(Sets.newHashSet(datePeriodType, systemEventType))
173                 .build();
174     }
175 
176     private Api api() {
177         return _api;
178     }
179 
180     private EventsApi events() {
181         return api().events()
182                 .withEntityClasses(supportedTypes.toArray(new EntityClass[supportedTypes.size()]))
183                 .withEventTypes(supportedEvents.toArray(new EventTypes[supportedEvents.size()]));
184     }
185 
186     private static String __(String key) {
187         try {
188             return bundle.getString(key);
189         } catch (MissingResourceException e) {
190             logger.error("Missing resource for i18n key: {}", key);
191             return '!' + key + '!';
192         }
193 
194     }
195 
196     private static String toBase64(String s) {
197         return Base64.encodeBase64String(s.getBytes());
198     }
199 
200     private static String fromBase64(String s) {
201         return new String(Base64.decodeBase64(s));
202     }
203 
204 
205     private static Map<String, Object> mapOf(Object... items) {
206         Preconditions.checkArgument(items.length % 2 == 0, "Items must be pairs of key/value");
207         Map<String, Object> map = Maps.newHashMap();
208         for (int i = 0; i < items.length; i += 2) {
209             map.put(((String) items[i]), items[i + 1]);
210         }
211         return map;
212     }
213 
214     private static int decodeCursor(String cursor, int defaultVal) {
215         try {
216             return cursor != null
217                     ? Math.max(-1, Integer.parseInt(fromBase64(cursor)))
218                     : defaultVal;
219         } catch (NumberFormatException e) {
220             return defaultVal;
221         }
222     }
223 
224     private static int getLimit(Integer limitArg, boolean stream) {
225         if (limitArg == null) {
226             return stream ? -1 : DEFAULT_LIST_LIMIT;
227         } else if (limitArg < 0) {
228             return stream ? limitArg : MAX_LIST_LIMIT;
229         } else {
230             return stream ? limitArg : Math.min(MAX_LIST_LIMIT, limitArg);
231         }
232     }
233 
234     private static int getOffset(String afterCursor, String fromCursor) {
235         if (afterCursor != null) {
236             return decodeCursor(afterCursor, -1) + 1;
237         } else if (fromCursor != null) {
238             return decodeCursor(fromCursor, 0);
239         } else {
240             return 0;
241         }
242     }
243 
244     // Argument helpers...
245 
246     private static final GraphQLList GraphQLStringList = new GraphQLList(GraphQLString);
247     private static final GraphQLNonNull GraphQLNonNullString = new GraphQLNonNull(GraphQLString);
248 
249     private static final GraphQLScalarType CursorType =
250             new GraphQLScalarType("Cursor", __("cursor.description"), GraphQLString.getCoercing());
251 
252     private static final GraphQLArgument idArgument = newArgument()
253             .name(Bundle.ID_KEY)
254             .description(__("graphql.argument.id.description"))
255             .type(new GraphQLNonNull(GraphQLID))
256             .build();
257 
258     // Data fetchers...
259 
260     private DataFetcher<Iterable<SystemEvent>> itemEventsDataFetcher() {
261         return env -> events().listForItem(env.<Entity>getSource().as(SystemEvent.class));
262     }
263 
264     private DataFetcher<Map<String, Object>> topLevelDocDataFetcher() {
265         return connectionDataFetcher(() -> {
266             Iterable<Country> countries = api().query()
267                     .setStream(true).setLimit(-1).page(EntityClass.COUNTRY, Country.class);
268             Iterable<Iterable<Entity>> docs = Iterables.transform(countries, c ->
269                     Iterables.transform(c.getTopLevelDocumentaryUnits(), d -> d.as(Entity.class)));
270             return Iterables.concat(docs);
271         });
272     }
273 
274     private DataFetcher<Map<String, Object>> entityTypeConnectionDataFetcher(EntityClass type) {
275         // FIXME: The only way to get a list of all items of a given
276         // type via the API alone is to run a query as a stream w/ no limit.
277         // However, this means that ACL filtering will be applied twice,
278         // once here, and once by the connection data fetcher, which also
279         // applies pagination. This is a bit gross but the speed difference
280         // appears to be negligible.
281         return connectionDataFetcher(() -> api().query()
282                 .setStream(true).setLimit(-1).page(type, Entity.class));
283     }
284 
285     private DataFetcher<Map<String, Object>> hierarchicalOneToManyRelationshipConnectionFetcher(
286             Function<Entity, Iterable<? extends Entity>> top, Function<Entity, Iterable<? extends Entity>> all) {
287         // Depending on the value of the "all" argument, return either just
288         // the top level items or everything in the tree.
289         return env -> {
290             boolean allOrTop = (Boolean) Optional.ofNullable(env.getArgument(ALL_PARAM)).orElse(false);
291             Function<Entity, Iterable<? extends Entity>> func = allOrTop ? all : top;
292             return connectionDataFetcher(() -> func.apply((env.getSource()))).get(env);
293         };
294     }
295 
296     private DataFetcher<Map<String, Object>> oneToManyRelationshipConnectionFetcher(Function<Entity, Iterable<? extends Entity>> f) {
297         return env -> connectionDataFetcher(() -> f.apply((env.getSource()))).get(env);
298     }
299 
300     private DataFetcher<Map<String, Object>> connectionDataFetcher(Supplier<Iterable<? extends Entity>> iter) {
301         // NB: The data fetcher takes a supplier here so lazily generated
302         // streams can be invoked more than one (if, e.g. both the items array
303         // and the edges array is needed.) Otherwise we would have to somehow
304         // reset the Iterable.
305         return env -> {
306             int limit = getLimit(env.getArgument(FIRST_PARAM), stream);
307             int offset = getOffset(env.getArgument(AFTER_PARAM), env.getArgument(FROM_PARAM));
308             return stream && limit < 0
309                     ? lazyConnectionData(iter, limit, offset)
310                     : strictConnectionData(iter, limit, offset);
311         };
312     }
313 
314     private Map<String, Object> connectionData(Iterable<?> items,
315             Iterable<Map<String, Object>> edges, String nextCursor, String prevCursor) {
316         return mapOf(
317                 ITEMS, items,
318                 EDGES, edges,
319                 PAGE_INFO, mapOf(
320                         HAS_NEXT_PAGE, nextCursor != null,
321                         NEXT_PAGE, nextCursor,
322                         HAS_PREVIOUS_PAGE, prevCursor != null,
323                         PREVIOUS_PAGE, prevCursor
324                 )
325         );
326     }
327 
328     private Map<String, Object> strictConnectionData(Supplier<Iterable<? extends Entity>> iter, int limit, int offset) {
329         // Note: strict connections are considerably slower than lazy ones
330         // since to assemble the PageInfo we need to count the total number
331         // of items, which involves fetching the iterator twice.
332         long total = Iterables.size(iter.get());
333         QueryApi query = api().query().setStream(true).setLimit(limit).setOffset(offset);
334         QueryApi.Page<Entity> page = query.page(iter.get(), Entity.class);
335         List<Entity> items = Lists.newArrayList(page);
336 
337         // Create a list of edges, with the cursor taking into
338         // account each item's offset
339         List<Map<String, Object>> edges = Lists.newArrayListWithExpectedSize(items.size());
340         for (int i = 0; i < items.size(); i++) {
341             edges.add(mapOf(
342                     CURSOR, toBase64(String.valueOf(offset + i)),
343                     NODE, items.get(i)
344             ));
345         }
346 
347         boolean hasNext = page.getOffset() + items.size() < total;
348         boolean hasPrev = page.getOffset() > 0;
349         String nextCursor = toBase64(String.valueOf(offset + limit));
350         String prevCursor = toBase64(String.valueOf(offset - limit));
351         return connectionData(items, edges, hasNext ? nextCursor : null, hasPrev ? prevCursor : null);
352     }
353 
354     private Map<String, Object> lazyConnectionData(Supplier<Iterable<? extends Entity>> iter, int limit, int offset) {
355         QueryApi query = api().query().setLimit(limit).setOffset(offset);
356         QueryApi.Page<Entity> items = query.page(iter.get(), Entity.class);
357         boolean hasPrev = items.getOffset() > 0;
358 
359         // Create a list of edges, with the cursor taking into
360         // account each item's offset
361         final AtomicInteger index = new AtomicInteger();
362         Iterable<Map<String, Object>> edges = Iterables.transform(
363                 query.page(iter.get(), Entity.class), item -> mapOf(
364                         CURSOR, toBase64(String.valueOf(offset + index.getAndIncrement())),
365                         NODE, item
366                 ));
367 
368         String prevCursor = toBase64(String.valueOf(offset - limit));
369         return connectionData(items, edges, null, hasPrev ? prevCursor : null);
370     }
371 
372     private DataFetcher<Entity> entityIdDataFetcher(String type) {
373         return env -> {
374             try {
375                 Accessible detail = api().detail(env.getArgument(Bundle.ID_KEY), Accessible.class);
376                 return Objects.equals(detail.getType(), type) ? detail : null;
377             } catch (ItemNotFound e) {
378                 return null;
379             }
380         };
381     };
382 
383     private static final DataFetcher<String> idDataFetcher =
384             env -> (env.<Entity>getSource()).getProperty(EntityType.ID_KEY);
385 
386     private static final DataFetcher<String> typeDataFetcher =
387             env -> (env.<Entity>getSource()).getProperty(EntityType.TYPE_KEY);
388 
389     private static final DataFetcher<Object> attributeDataFetcher = env -> {
390         Entity source = env.<Entity>getSource();
391         String name = env.getFields().get(0).getName();
392         return source.getProperty(name);
393     };
394 
395     private static DataFetcher<List> listDataFetcher(DataFetcher<Object> fetcher) {
396         return env -> {
397             Object obj = fetcher.get(env);
398             if (obj == null) {
399                 return Collections.emptyList();
400             } else if (obj instanceof List) {
401                 return (List)obj;
402             } else {
403                 return Lists.newArrayList(obj);
404             }
405         };
406     }
407 
408     private static final DataFetcher<Description> descriptionDataFetcher = env -> {
409         String lang = env.getArgument(Ontology.LANGUAGE_OF_DESCRIPTION);
410         String code = env.getArgument(Ontology.IDENTIFIER_KEY);
411 
412         Entity source = env.getSource();
413         Iterable<Description> descriptions = source.as(Described.class).getDescriptions();
414 
415         if (lang == null && code == null) {
416             int at = env.getArgument(SLICE_PARAM);
417             List<Description> descList = Lists.newArrayList(descriptions);
418             return at >= 1 && descList.size() >= at ? descList.get(at - 1) : null;
419         } else {
420             for (Description next : descriptions) {
421                 String langCode = next.getLanguageOfDescription();
422                 if (langCode.equalsIgnoreCase(lang)) {
423                     if (code != null && !code.isEmpty()) {
424                         String ident = next.getDescriptionCode();
425                         if (ident.equals(code)) {
426                             return next;
427                         }
428                     } else {
429                         return next;
430                     }
431                 }
432             }
433             return null;
434         }
435     };
436 
437     private static final DataFetcher<String> annotationNameDataFetcher =
438             env -> Optional.ofNullable(env.<Entity>getSource().as(Annotation.class)
439                     .getAnnotator())
440                     .map(Named::getName).orElse(null);
441 
442     private static final DataFetcher<List<Map<String, Object>>> relatedItemsDataFetcher = env -> {
443         Entity source = env.getSource();
444         Iterable<Link> links = source.as(Linkable.class).getLinks();
445         return StreamSupport.stream(links.spliterator(), false).map(link -> {
446             Linkable target = Iterables.tryFind(link.getLinkTargets(),
447                     t -> t != null && !t.equals(source)).orNull();
448             return target == null ? null : mapOf("context", link, "item", target);
449         }).filter(Objects::nonNull).collect(Collectors.toList());
450     };
451 
452     private static DataFetcher<Object> transformingDataFetcher(DataFetcher fetcher, Function<Object, Object> transformer) {
453         return env -> transformer.apply(fetcher.get(env));
454     }
455 
456     private DataFetcher<Iterable<Entity>> oneToManyRelationshipFetcher(Function<Entity, Iterable<? extends Entity>> f) {
457         return env -> {
458             Iterable<? extends Entity> elements = f.apply((env.getSource()));
459             return api().query().setStream(true).setLimit(-1).page(elements, Entity.class);
460         };
461     }
462 
463     private DataFetcher<Entity> manyToOneRelationshipFetcher(Function<Entity, Entity> f) {
464         return env -> {
465             Entity elem = f.apply(env.getSource());
466             if (elem != null &&
467                     AclManager.getAclFilterFunction(api().accessor())
468                             .compute(elem.asVertex())) {
469                 return elem;
470             }
471 
472             return null;
473         };
474     }
475 
476     // Field definition helpers...
477 
478     private static GraphQLFieldDefinition.Builder nullAttr(String name, String description, GraphQLOutputType type) {
479         return newFieldDefinition()
480                 .type(type)
481                 .name(name)
482                 .description(description)
483                 .dataFetcher(attributeDataFetcher);
484     }
485 
486     private static GraphQLFieldDefinition.Builder nullAttr(String name, String description) {
487         return nullAttr(name, description, GraphQLString);
488     }
489 
490     private static GraphQLFieldDefinition.Builder nonNullAttr(String name, String description, GraphQLOutputType type) {
491         return newFieldDefinition()
492                 .type(type)
493                 .name(name)
494                 .description(description)
495                 .dataFetcher(attributeDataFetcher);
496     }
497 
498     private static GraphQLFieldDefinition.Builder nonNullAttr(String name, String description) {
499         return nonNullAttr(name, description, GraphQLString);
500     }
501 
502     private static List<GraphQLFieldDefinition> nullStringAttrs(DefinitionList[] items) {
503         return Lists.newArrayList(items)
504                 .stream().filter(i -> !i.isMultiValued())
505                 .map(f ->
506                         newFieldDefinition()
507                                 .type(GraphQLString)
508                                 .name(f.name())
509                                 .description(f.getDescription())
510                                 .dataFetcher(attributeDataFetcher)
511                                 .build()
512                 ).collect(Collectors.toList());
513     }
514 
515     private static List<GraphQLFieldDefinition> listStringAttrs(DefinitionList[] items) {
516         return Lists.newArrayList(items)
517                 .stream().filter(DefinitionList::isMultiValued)
518                 .map(f ->
519                         newFieldDefinition()
520                                 .type(GraphQLStringList)
521                                 .name(f.name())
522                                 .description(f.getDescription())
523                                 .dataFetcher(listDataFetcher(attributeDataFetcher))
524                                 .build()
525                 ).collect(Collectors.toList());
526     }
527 
528     private static final GraphQLFieldDefinition.Builder idField = newFieldDefinition()
529             .type(GraphQLNonNullString)
530             .name(Bundle.ID_KEY)
531             .description(__("graphql.field.id.description"))
532             .dataFetcher(idDataFetcher);
533 
534     private static final GraphQLFieldDefinition.Builder typeField = newFieldDefinition()
535             .type(new GraphQLNonNull(GraphQLString))
536             .name(Bundle.TYPE_KEY)
537             .description(__("graphql.field.type.description"))
538             .dataFetcher(typeDataFetcher);
539 
540     private static GraphQLFieldDefinition.Builder singleDescriptionFieldDefinition(GraphQLOutputType descriptionType) {
541         return newFieldDefinition()
542                 .type(descriptionType)
543                 .name("description")
544                 .argument(newArgument()
545                         .name(Ontology.LANGUAGE_OF_DESCRIPTION)
546                         .description(__("graphql.argument.languageCode.description"))
547                         .type(GraphQLString)
548                         .build()
549                 )
550                 .argument(newArgument()
551                         .name(Ontology.IDENTIFIER_KEY)
552                         .description(__("graphql.argument.identifier.description"))
553                         .type(GraphQLString)
554                         .build()
555                 )
556                 .argument(newArgument()
557                         .name(SLICE_PARAM)
558                         .description(__("graphql.argument.at.description"))
559                         .type(GraphQLInt)
560                         .defaultValue(1)
561                         .build()
562                 )
563                 .description(__("graphl.field.description.description"))
564                 .dataFetcher(descriptionDataFetcher);
565     }
566 
567     private static GraphQLFieldDefinition.Builder listFieldDefinition(String name, String description,
568             GraphQLOutputType type, DataFetcher dataFetcher) {
569         return newFieldDefinition()
570                 .name(name)
571                 .type(new GraphQLList(type))
572                 .description(description)
573                 .dataFetcher(dataFetcher);
574     }
575 
576     private GraphQLFieldDefinition.Builder itemEventsFieldDefinition() {
577         return newFieldDefinition()
578                 .name("systemEvents")
579                 .description(__("graphql.field.systemEvents.description"))
580                 .type(new GraphQLList(new GraphQLTypeReference(Entities.SYSTEM_EVENT)))
581                 .dataFetcher(itemEventsDataFetcher());
582     }
583 
584     private static GraphQLFieldDefinition.Builder connectionFieldDefinition(String name, String description,
585             GraphQLOutputType type, DataFetcher dataFetcher, GraphQLArgument... arguments) {
586         return newFieldDefinition()
587                 .name(name)
588                 .description(description)
589                 .type(type)
590                 .dataFetcher(dataFetcher)
591                 .argument(newArgument()
592                         .name(FIRST_PARAM)
593                         .type(GraphQLInt)
594                         .description(__("graphql.argument.first.description"))
595                         .build()
596                 )
597                 .argument(newArgument()
598                         .name(AFTER_PARAM)
599                         .description(__("graphql.argument.after.description"))
600                         .type(CursorType)
601                         .build()
602                 )
603                 .argument(newArgument()
604                         .name(FROM_PARAM)
605                         .description(__("graphql.argument.from.description"))
606                         .type(CursorType)
607                         .build()
608                 )
609                 .argument(Lists.newArrayList(arguments));
610     }
611 
612     private static final List<GraphQLFieldDefinition> entityFields =
613             ImmutableList.of(idField.build(), typeField.build());
614 
615     private static final List<GraphQLFieldDefinition> geoFields = ImmutableList.of(
616             newFieldDefinition()
617                     .name(Geo.latitude.name())
618                     .description(Geo.latitude.getDescription())
619                     .type(GraphQLBigDecimal)
620                     .dataFetcher(attributeDataFetcher)
621                     .build(),
622             newFieldDefinition()
623                     .name(Geo.longitude.name())
624                     .description(Geo.longitude.getDescription())
625                     .type(GraphQLBigDecimal)
626                     .dataFetcher(attributeDataFetcher)
627                     .build()
628     );
629 
630     private static List<GraphQLFieldDefinition> descriptionFields() {
631         return Lists.newArrayList(
632                 nonNullAttr(Ontology.LANGUAGE_OF_DESCRIPTION, __("graphql.field.languageCode.description")).build(),
633                 nonNullAttr(Ontology.NAME_KEY, __("graphql.field.name.description")).build(),
634                 nullAttr(Ontology.IDENTIFIER_KEY, __("graphql.field.description.identifier.description")).build()
635         );
636     }
637 
638     private GraphQLFieldDefinition.Builder descriptionsFieldDefinition(GraphQLOutputType descriptionType) {
639         return newFieldDefinition()
640                 .type(new GraphQLList(descriptionType))
641                 .name("descriptions")
642                 .description(__("graphql.field.descriptions.description"))
643                 .dataFetcher(oneToManyRelationshipFetcher(r -> r.as(Described.class).getDescriptions()));
644     }
645 
646     private GraphQLFieldDefinition.Builder itemCountFieldDefinition(Function<Entity, Integer> f) {
647         return newFieldDefinition()
648                 .type(new GraphQLNonNull(GraphQLInt))
649                 .name("itemCount")
650                 .description(__("graphql.field.itemCount.description"))
651                 .dataFetcher(env -> Math.toIntExact(f.apply(env.<Entity>getSource())));
652     }
653 
654     private final GraphQLFieldDefinition.Builder linkFieldDefinition =
655             listFieldDefinition("links", __("graphql.field.links.description"),
656                     new GraphQLTypeReference(Entities.LINK),
657                     oneToManyRelationshipFetcher(r -> r.as(Linkable.class).getLinks()));
658 
659     private final GraphQLFieldDefinition.Builder annotationsFieldDefinition =
660             listFieldDefinition("annotations", __("graphql.field.annotations.description"),
661                     new GraphQLTypeReference(Entities.ANNOTATION),
662                     oneToManyRelationshipFetcher(r -> r.as(Annotatable.class).getAnnotations()));
663 
664     private List<GraphQLFieldDefinition> linksAndAnnotationsFields() {
665         return Lists.newArrayList(linkFieldDefinition.build(), annotationsFieldDefinition.build());
666     }
667 
668     private final GraphQLFieldDefinition.Builder accessPointFieldDefinition =
669         listFieldDefinition("accessPoints", __("graphql.field.accessPoints.description"),
670                 new GraphQLTypeReference(Entities.ACCESS_POINT),
671                 oneToManyRelationshipFetcher(d -> d.as(Description.class).getAccessPoints()));
672 
673     private final GraphQLFieldDefinition.Builder datePeriodFieldDefinition =
674         listFieldDefinition("dates", __("graphql.field.dates.description"),
675                 new GraphQLTypeReference(Entities.DATE_PERIOD),
676                 oneToManyRelationshipFetcher(d -> d.as(Temporal.class).getDatePeriods()));
677 
678     private GraphQLFieldDefinition.Builder itemFieldDefinition(String name, String description,
679             GraphQLOutputType type, DataFetcher dataFetcher, GraphQLArgument... arguments) {
680         return newFieldDefinition()
681                 .name(name)
682                 .type(type)
683                 .description(description)
684                 .dataFetcher(dataFetcher)
685                 .argument(Lists.newArrayList(arguments));
686     }
687 
688     private GraphQLFieldDefinition relatedTypeFieldDefinition() {
689         return newFieldDefinition()
690                 .name("related")
691                 .description(__("graphql.field.related.description"))
692                 .type(new GraphQLList(relatedType))
693                 .dataFetcher(relatedItemsDataFetcher)
694                 .build();
695     }
696 
697     private GraphQLFieldDefinition relatedItemsItemFieldDefinition() {
698         return newFieldDefinition()
699                 .type(linkableInterface)
700                 .name("item")
701                 .description(__("graphql.field.related.item.description"))
702                 .build();
703     }
704 
705     // Type definitions...
706 
707     private static GraphQLOutputType edgeType(GraphQLOutputType wrapped) {
708         return newObject()
709                 .name(wrapped.getName() + "Edge")
710                 .description(MessageFormat.format(__("graphql.edge.description"), wrapped.getName()))
711                 .field(newFieldDefinition()
712                         .name(NODE)
713                         .type(wrapped)
714                         .build()
715                 )
716                 .field(newFieldDefinition()
717                         .name(CURSOR)
718                         .type(CursorType)
719                         .build()
720                 )
721                 .build();
722     }
723 
724     private static List<GraphQLFieldDefinition> connectionFields(GraphQLOutputType wrappedType) {
725         return ImmutableList.of(
726                 newFieldDefinition()
727                         .name(ITEMS)
728                         .description(MessageFormat.format(__("graphql.connection.field.items.description"), wrappedType.getName()))
729                         .type(new GraphQLList(wrappedType))
730                         .build(),
731                 newFieldDefinition()
732                         .name(EDGES)
733                         .description(MessageFormat.format(__("graphql.connection.field.edges.description"), wrappedType.getName()))
734                         .type(new GraphQLList(edgeType(wrappedType)))
735                         .build(),
736                 newFieldDefinition()
737                         .name(PAGE_INFO)
738                         .description(__("graphql.field.pageInfo.description"))
739                         .type(newObject()
740                                 .name(PAGE_INFO + wrappedType.getName())
741                                 .field(newFieldDefinition()
742                                         .name(HAS_PREVIOUS_PAGE)
743                                         .description(__("graphql.field.pageInfo.hasPreviousPage.description"))
744                                         .type(GraphQLBoolean)
745                                         .build()
746                                 )
747                                 .field(newFieldDefinition()
748                                         .name(PREVIOUS_PAGE)
749                                         .description(__("graphql.field.pageInfo.previousPage.description"))
750                                         .type(CursorType)
751                                         .build()
752                                 )
753                                 .field(newFieldDefinition()
754                                         .name(HAS_NEXT_PAGE)
755                                         .description(__("graphql.field.pageInfo.hasNextPage.description"))
756                                         .type(GraphQLBoolean)
757                                         .build()
758                                 )
759                                 .field(newFieldDefinition()
760                                         .name(NEXT_PAGE)
761                                         .description(__("graphql.field.pageInfo.nextPage.description"))
762                                         .type(CursorType)
763                                         .build()
764                                 )
765                                 .build())
766                         .build()
767         );
768     }
769 
770     private static GraphQLOutputType connectionType(GraphQLOutputType wrappedType, String name) {
771         return newObject()
772                 .name(name)
773                 .description(MessageFormat.format(__("graphql.connection.description"), wrappedType.getName()))
774                 .fields(connectionFields(wrappedType))
775                 .build();
776     }
777 
778     // NB: These are static since loading them, and all the resources, is quite
779     // slow to do per query.
780     private static final List<GraphQLFieldDefinition> documentaryUnitDescriptionNullFields = nullStringAttrs(IsadG.values());
781     private static final List<GraphQLFieldDefinition> documentaryUnitDescriptionListFields = listStringAttrs(IsadG.values());
782     private static final List<GraphQLFieldDefinition> repositoryDescriptionNullFields = nullStringAttrs(Isdiah.values());
783     private static final List<GraphQLFieldDefinition> repositoryDescriptionListFields = listStringAttrs(Isdiah.values());
784     private static final List<GraphQLFieldDefinition> historicalAgentDescriptionNullFields = nullStringAttrs(Isaar.values());
785     private static final List<GraphQLFieldDefinition> historicalAgentDescriptionListFields = listStringAttrs(Isaar.values());
786     private static final List<GraphQLFieldDefinition> countryDescriptionNullFields = nullStringAttrs(CountryInfo.values());
787     private static final List<GraphQLFieldDefinition> countryDescriptionListFields = listStringAttrs(CountryInfo.values());
788     private static final List<GraphQLFieldDefinition> conceptNullFields = nullStringAttrs(Skos.values());
789     private static final List<GraphQLFieldDefinition> conceptListFields = listStringAttrs(Skos.values());
790     private static final List<GraphQLFieldDefinition> conceptDescriptionNullFields = nullStringAttrs(SkosMultilingual.values());
791     private static final List<GraphQLFieldDefinition> conceptDescriptionListFields = listStringAttrs(SkosMultilingual.values());
792 
793 
794     // Interfaces and type resolvers...
795 
796     private final TypeResolver entityTypeResolver = new TypeResolver() {
797         @Override
798         public GraphQLObjectType getType(TypeResolutionEnvironment env) {
799             Entity entity = env.getObject();
800             switch (entity.getType()) {
801                 case Entities.DOCUMENTARY_UNIT:
802                     return documentaryUnitType;
803                 case Entities.REPOSITORY:
804                     return repositoryType;
805                 case Entities.COUNTRY:
806                     return countryType;
807                 case Entities.HISTORICAL_AGENT:
808                     return historicalAgentType;
809                 case Entities.CVOC_CONCEPT:
810                     return conceptType;
811                 case Entities.CVOC_VOCABULARY:
812                     return vocabularyType;
813                 case Entities.AUTHORITATIVE_SET:
814                     return authoritativeSetType;
815                 case Entities.ANNOTATION:
816                     return annotationType;
817                 case Entities.LINK:
818                     return linkType;
819                 case Entities.ACCESS_POINT:
820                     return accessPointType;
821                 case Entities.DATE_PERIOD:
822                     return datePeriodType;
823                 default:
824                     return null;
825             }
826         }
827     };
828 
829     private final TypeResolver descriptionTypeResolver = new TypeResolver() {
830         @Override
831         public GraphQLObjectType getType(TypeResolutionEnvironment env) {
832             Entity entity = env.getObject();
833             switch (entity.getType()) {
834                 case Entities.DOCUMENTARY_UNIT_DESCRIPTION:
835                     return documentaryUnitDescriptionType;
836                 case Entities.REPOSITORY_DESCRIPTION:
837                     return repositoryDescriptionType;
838                 case Entities.HISTORICAL_AGENT_DESCRIPTION:
839                     return historicalAgentDescriptionType;
840                 case Entities.CVOC_CONCEPT_DESCRIPTION:
841                     return conceptDescriptionType;
842                 default:
843                     return null;
844             }
845         }
846     };
847 
848     private final GraphQLInterfaceType entityInterface = newInterface()
849             .name(Entity.class.getSimpleName())
850             .description(__("graphql.interface.entity.description"))
851             .fields(entityFields)
852             .typeResolver(entityTypeResolver)
853             .build();
854 
855     private final GraphQLInterfaceType descriptionInterface = newInterface()
856             .name(Description.class.getSimpleName())
857             .description(__("graphql.interface.description.description"))
858             .fields(descriptionFields())
859             .typeResolver(descriptionTypeResolver)
860             .build();
861 
862     private final GraphQLInterfaceType temporalDescriptionInterface = newInterface()
863             .name(Temporal.class.getSimpleName() + Description.class.getSimpleName())
864             .description(__("graphql.interface.temporalDescription.description"))
865             .fields(descriptionFields())
866             .field((f) -> datePeriodFieldDefinition)
867             .typeResolver(descriptionTypeResolver)
868             .build();
869 
870     private final GraphQLInterfaceType describedInterface = newInterface()
871             .name(Described.class.getSimpleName())
872             .description(__("graphql.interface.described.description"))
873             .fields(entityFields)
874             .field(singleDescriptionFieldDefinition(descriptionInterface))
875             .field(descriptionsFieldDefinition(descriptionInterface))
876             .field(nonNullAttr(Ontology.IDENTIFIER_KEY, __("graphql.field.identifier.description")))
877             .fields(linksAndAnnotationsFields())
878             .typeResolver(entityTypeResolver)
879             .build();
880 
881     private final GraphQLInterfaceType temporalInterface = newInterface()
882             .name(Temporal.class.getSimpleName())
883             .description(__("graphql.interface.temporal.description"))
884             .fields(entityFields)
885             .field(nonNullAttr(Ontology.IDENTIFIER_KEY, __("graphql.field.identifier.description")))
886             .field(singleDescriptionFieldDefinition(temporalDescriptionInterface))
887             .field(descriptionsFieldDefinition(temporalDescriptionInterface))
888             .typeResolver(entityTypeResolver)
889             .build();
890 
891     private final GraphQLInterfaceType annotatableInterface = newInterface()
892             .fields(entityFields)
893             .field(annotationsFieldDefinition)
894             .typeResolver(entityTypeResolver)
895             .name(Annotatable.class.getSimpleName())
896             .description(__("graphql.interface.annotatable.description"))
897             .build();
898 
899     private final GraphQLInterfaceType linkableInterface = newInterface()
900             .fields(entityFields)
901             .field(linkFieldDefinition)
902             .typeResolver(entityTypeResolver)
903             .name(Linkable.class.getSimpleName())
904             .description(__("graphql.interface.linkable.description"))
905             .build();
906 
907     private final GraphQLObjectType systemEventType = newObject()
908             .name(Entities.SYSTEM_EVENT)
909             .description(__("systemEvent.description"))
910             .field(nonNullAttr(Ontology.EVENT_TIMESTAMP, bundle.getString("systemEvent.field.timestamp.description")))
911             .field(nullAttr(Ontology.EVENT_LOG_MESSAGE, bundle.getString("systemEvent.field.logMessage.description")))
912             .field(nonNullAttr(Ontology.EVENT_TYPE, bundle.getString("systemEvent.field.eventType.description")))
913             .build();
914 
915     private final GraphQLEnumType accessPointTypeEnum = newEnum()
916             .name(AccessPointType.class.getSimpleName())
917             .description(__("graphql.enum.accessPointType.description"))
918             .value(AccessPointType.person.name())
919             .value(AccessPointType.family.name())
920             .value(AccessPointType.corporateBody.name())
921             .value(AccessPointType.subject.name())
922             .value(AccessPointType.creator.name())
923             .value(AccessPointType.place.name())
924             .value(AccessPointType.genre.name())
925             .build();
926 
927     private final GraphQLObjectType accessPointType = newObject()
928             .name(Entities.ACCESS_POINT)
929             .description(__("accessPoint.description"))
930             .field(nonNullAttr(Ontology.NAME_KEY, __("accessPoint.field.name.description")))
931             .field(newFieldDefinition()
932                     .name(Ontology.ACCESS_POINT_TYPE)
933                     .description(__("accessPoint.field.type.description"))
934                     .type(new GraphQLNonNull(accessPointTypeEnum))
935                     .dataFetcher(attributeDataFetcher)
936                     .build())
937             .build();
938 
939     private final GraphQLObjectType datePeriodType = newObject()
940             .name(Entities.DATE_PERIOD)
941             .description(__("datePeriod.description"))
942             .field(nullAttr(Ontology.DATE_PERIOD_START_DATE, __("datePeriod.field.startDate.description")))
943             .field(nullAttr(Ontology.DATE_PERIOD_END_DATE, __("datePeriod.field.endDate.description")))
944             .build();
945 
946     private final GraphQLObjectType addressType = newObject()
947             .name(Entities.ADDRESS)
948             .description(__("address.description"))
949             .fields(nullStringAttrs(ContactInfo.values()))
950             .fields(listStringAttrs(ContactInfo.values()))
951             .build();
952 
953     private final GraphQLObjectType relatedType = newObject()
954             .name("Relationship")
955             .description(__("relationship.description"))
956             .field(newFieldDefinition()
957                     .name("context")
958                     .description(__("relationship.field.context.description"))
959                     .type(new GraphQLTypeReference(Entities.LINK))
960                     .build())
961             .field(relatedItemsItemFieldDefinition())
962             .build();
963 
964 
965     private final GraphQLObjectType documentaryUnitDescriptionType = newObject()
966             .name(Entities.DOCUMENTARY_UNIT_DESCRIPTION)
967             .description(__("documentaryUnitDescription.description"))
968             .fields(descriptionFields())
969             .field(accessPointFieldDefinition)
970             .field(datePeriodFieldDefinition)
971             .fields(documentaryUnitDescriptionNullFields)
972             .fields(documentaryUnitDescriptionListFields)
973             .withInterfaces(descriptionInterface, temporalDescriptionInterface)
974             .build();
975 
976     private final GraphQLObjectType repositoryDescriptionType = newObject()
977             .name(Entities.REPOSITORY_DESCRIPTION)
978             .description(__("repositoryDescription.description"))
979             .fields(descriptionFields())
980             .field(accessPointFieldDefinition)
981             .field(listFieldDefinition("addresses", __("repositoryDescription.field.addresses.description"),
982                     addressType, oneToManyRelationshipFetcher(d ->
983                             d.as(RepositoryDescription.class).getAddresses())))
984             .fields(repositoryDescriptionNullFields)
985             .fields(repositoryDescriptionListFields)
986             .withInterfaces(descriptionInterface)
987             .build();
988 
989     private final GraphQLObjectType historicalAgentDescriptionType = newObject()
990             .name(Entities.HISTORICAL_AGENT_DESCRIPTION)
991             .description(__("historicalAgentDescription.description"))
992             .fields(descriptionFields())
993             .field(accessPointFieldDefinition)
994             .field(datePeriodFieldDefinition)
995             .fields(historicalAgentDescriptionNullFields)
996             .fields(historicalAgentDescriptionListFields)
997             .withInterfaces(descriptionInterface, temporalDescriptionInterface)
998             .build();
999 
1000     private final GraphQLObjectType conceptDescriptionType = newObject()
1001             .name(Entities.CVOC_CONCEPT_DESCRIPTION)
1002             .description(__("conceptDescription.description"))
1003             .fields(descriptionFields())
1004             .field(accessPointFieldDefinition)
1005             .fields(conceptDescriptionNullFields)
1006             .fields(conceptDescriptionListFields)
1007             .withInterfaces(descriptionInterface)
1008             .build();
1009 
1010     private static final GraphQLArgument allArgument = newArgument()
1011             .name(ALL_PARAM)
1012             .description(__("graphql.argument.all"))
1013             .type(GraphQLBoolean)
1014             .defaultValue(false)
1015             .build();
1016 
1017     private final GraphQLObjectType repositoryType = newObject()
1018             .name(Entities.REPOSITORY)
1019             .description(__("repository.description"))
1020             .fields(entityFields)
1021             .field(nonNullAttr(Ontology.IDENTIFIER_KEY, __("repository.field.identifier.description")))
1022             .field(itemCountFieldDefinition(r -> r.as(Repository.class).getChildCount()))
1023             .field(connectionFieldDefinition("documentaryUnits", __("repository.field.documentaryUnits.description"),
1024                     new GraphQLTypeReference("documentaryUnits"),
1025                     hierarchicalOneToManyRelationshipConnectionFetcher(
1026                             r -> r.as(Repository.class).getTopLevelDocumentaryUnits(),
1027                             r -> r.as(Repository.class).getAllDocumentaryUnits()),
1028                     allArgument))
1029             .field(singleDescriptionFieldDefinition(repositoryDescriptionType))
1030             .field(descriptionsFieldDefinition(repositoryDescriptionType))
1031             .field(itemFieldDefinition("country", __("repository.field.country.description"),
1032                     new GraphQLTypeReference(Entities.COUNTRY),
1033                     manyToOneRelationshipFetcher(r -> r.as(Repository.class).getCountry())))
1034             .fields(linksAndAnnotationsFields())
1035             .field(itemEventsFieldDefinition())
1036             .withInterfaces(entityInterface, describedInterface, linkableInterface, annotatableInterface)
1037             .build();
1038 
1039     private final GraphQLObjectType documentaryUnitType = newObject()
1040             .name(Entities.DOCUMENTARY_UNIT)
1041             .description(__("documentaryUnit.description"))
1042             .fields(entityFields)
1043             .field(nonNullAttr(Ontology.IDENTIFIER_KEY, __("documentaryUnit.field.identifier.description")))
1044             .field(descriptionsFieldDefinition(documentaryUnitDescriptionType))
1045             .field(singleDescriptionFieldDefinition(documentaryUnitDescriptionType))
1046             .field(itemFieldDefinition("repository", __("documentaryUnit.field.repository.description"), repositoryType,
1047                     manyToOneRelationshipFetcher(d -> d.as(DocumentaryUnit.class).getRepository())))
1048             .field(itemCountFieldDefinition(d -> d.as(DocumentaryUnit.class).getChildCount()))
1049             .field(connectionFieldDefinition("children", __("documentaryUnit.field.children.description"),
1050                     new GraphQLTypeReference("documentaryUnits"),
1051                     hierarchicalOneToManyRelationshipConnectionFetcher(
1052                             d -> d.as(DocumentaryUnit.class).getChildren(),
1053                             d -> d.as(DocumentaryUnit.class).getAllChildren()),
1054                     allArgument))
1055             .field(itemFieldDefinition("parent", __("documentaryUnit.field.parent.description"),
1056                     new GraphQLTypeReference(Entities.DOCUMENTARY_UNIT),
1057                     manyToOneRelationshipFetcher(d -> d.as(DocumentaryUnit.class).getParent())))
1058             .field(listFieldDefinition("ancestors", __("documentaryUnit.field.ancestors.description"),
1059                     new GraphQLTypeReference(Entities.DOCUMENTARY_UNIT),
1060                     oneToManyRelationshipFetcher(d -> d.as(DocumentaryUnit.class).getAncestors())))
1061             .fields(linksAndAnnotationsFields())
1062             .field(relatedTypeFieldDefinition())
1063             .field(itemEventsFieldDefinition())
1064             .withInterfaces(entityInterface, describedInterface, linkableInterface, annotatableInterface, temporalInterface)
1065             .build();
1066 
1067     private final GraphQLObjectType historicalAgentType = newObject()
1068             .name(Entities.HISTORICAL_AGENT)
1069             .description(__("historicalAgent.description"))
1070             .fields(entityFields)
1071             .field(nonNullAttr(Ontology.IDENTIFIER_KEY, __("historicalAgent.field.identifier.description")))
1072             .field(singleDescriptionFieldDefinition(historicalAgentDescriptionType))
1073             .field(descriptionsFieldDefinition(historicalAgentDescriptionType))
1074             .fields(linksAndAnnotationsFields())
1075             .field(relatedTypeFieldDefinition())
1076             .field(itemEventsFieldDefinition())
1077             .withInterfaces(entityInterface, describedInterface, linkableInterface, annotatableInterface, temporalInterface)
1078             .build();
1079 
1080     private final GraphQLObjectType authoritativeSetType = newObject()
1081             .name(Entities.AUTHORITATIVE_SET)
1082             .description(__("authoritativeSet.description"))
1083             .fields(entityFields)
1084             .field(nonNullAttr(Ontology.IDENTIFIER_KEY, __("authoritativeSet.field.identifier.description")))
1085             .field(nonNullAttr(Ontology.NAME_KEY, __("authoritativeSet.field.name.description")))
1086             .field(nullAttr("description", __("authoritativeSet.field.description.description")))
1087             .field(itemCountFieldDefinition(a -> a.as(AuthoritativeSet.class).getChildCount()))
1088             .field(connectionFieldDefinition("authorities", __("authoritativeSet.field.authorities.description"),
1089                     new GraphQLTypeReference("historicalAgents"),
1090                     oneToManyRelationshipConnectionFetcher(
1091                             c -> c.as(AuthoritativeSet.class).getAuthoritativeItems())))
1092             .fields(linksAndAnnotationsFields())
1093             .field(itemEventsFieldDefinition())
1094             .withInterfaces(entityInterface, annotatableInterface)
1095             .build();
1096 
1097     private final GraphQLObjectType countryType = newObject()
1098             .name(Entities.COUNTRY)
1099             .description(__("country.description"))
1100             .fields(entityFields)
1101             .field(nonNullAttr(Ontology.IDENTIFIER_KEY, __("country.field.identifier.description")))
1102             .field(newFieldDefinition()
1103                     .name(Ontology.NAME_KEY)
1104                     .description(__("country.field.name.description"))
1105                     .type(new GraphQLNonNull(GraphQLString))
1106                     .dataFetcher(transformingDataFetcher(idDataFetcher,
1107                             obj -> LanguageHelpers.countryCodeToName(obj.toString())))
1108                     .build()
1109             )
1110             .fields(countryDescriptionNullFields)
1111             .fields(countryDescriptionListFields)
1112             .field(itemCountFieldDefinition(c -> c.as(Country.class).getChildCount()))
1113             .field(connectionFieldDefinition("repositories", __("country.field.repositories.description"),
1114                     new GraphQLTypeReference("repositories"),
1115                     oneToManyRelationshipConnectionFetcher(
1116                             c -> c.as(Country.class).getRepositories())))
1117             .fields(linksAndAnnotationsFields())
1118             .field(itemEventsFieldDefinition())
1119             .withInterfaces(entityInterface, annotatableInterface)
1120             .build();
1121 
1122     private final GraphQLObjectType conceptType = newObject()
1123             .name(Entities.CVOC_CONCEPT)
1124             .description(__("cvocConcept.description"))
1125             .fields(entityFields)
1126             .field(nonNullAttr(Ontology.IDENTIFIER_KEY, __("cvocConcept.field.identifier.description")))
1127             .fields(conceptNullFields)
1128             .fields(conceptListFields)
1129             .fields(geoFields)
1130             .field(descriptionsFieldDefinition(conceptDescriptionType))
1131             .field(singleDescriptionFieldDefinition(conceptDescriptionType))
1132             .field(listFieldDefinition("related", __("cvocConcept.field.related.description"),
1133                     new GraphQLTypeReference(Entities.CVOC_CONCEPT),
1134                     oneToManyRelationshipFetcher(
1135                             c -> c.as(Concept.class).getRelatedConcepts())))
1136             .field(itemCountFieldDefinition(c -> c.as(Concept.class).getChildCount()))
1137             .field(listFieldDefinition("broader", __("cvocConcept.field.broader.description"),
1138                     new GraphQLTypeReference(Entities.CVOC_CONCEPT),
1139                     oneToManyRelationshipFetcher(c -> c.as(Concept.class).getBroaderConcepts())))
1140             .field(listFieldDefinition("narrower", __("cvocConcept.field.narrower.description"),
1141                     new GraphQLTypeReference(Entities.CVOC_CONCEPT),
1142                     oneToManyRelationshipFetcher(
1143                             c -> c.as(Concept.class).getNarrowerConcepts())))
1144             .field(itemFieldDefinition("vocabulary", __("cvocConcept.field.vocabulary.description"),
1145                     new GraphQLTypeReference(Entities.CVOC_VOCABULARY),
1146                     manyToOneRelationshipFetcher(c -> c.as(Concept.class).getVocabulary())))
1147             .fields(linksAndAnnotationsFields())
1148             .field(itemEventsFieldDefinition())
1149             .withInterfaces(entityInterface, describedInterface, linkableInterface, annotatableInterface)
1150             .build();
1151 
1152     private final GraphQLObjectType vocabularyType = newObject()
1153             .name(Entities.CVOC_VOCABULARY)
1154             .description(__("cvocVocabulary.description"))
1155             .fields(entityFields)
1156             .field(nonNullAttr(Ontology.IDENTIFIER_KEY, __("cvocVocabulary.field.identifier.description")))
1157             .field(nonNullAttr(Ontology.NAME_KEY, __("cvocVocabulary.field.name.description")))
1158             .field(nullAttr("description", __("cvocVocabulary.field.description.description")))
1159             .field(itemCountFieldDefinition(r -> r.as(Vocabulary.class).getChildCount()))
1160             .field(connectionFieldDefinition("concepts", __("cvocVocabulary.field.concepts.description"),
1161                     new GraphQLTypeReference("concepts"),
1162                     oneToManyRelationshipConnectionFetcher(
1163                             c -> c.as(Vocabulary.class).getConcepts())))
1164             .fields(linksAndAnnotationsFields())
1165             .field(itemEventsFieldDefinition())
1166             .withInterfaces(entityInterface, annotatableInterface)
1167             .build();
1168 
1169     private final GraphQLObjectType annotationType = newObject()
1170             .name(Entities.ANNOTATION)
1171             .description(__("annotation.description"))
1172             .fields(entityFields)
1173             .field(nonNullAttr(Ontology.ANNOTATION_NOTES_BODY, __("annotation.field.body.description")))
1174             .field(nullAttr(Ontology.ANNOTATION_FIELD, __("annotation.field.field.description")))
1175             .field(nullAttr(Ontology.ANNOTATION_TYPE, __("annotation.field.annotationType.description")))
1176             .field(newFieldDefinition()
1177                     .type(GraphQLString)
1178                     .name("by")
1179                     .description(__("annotation.field.by.description"))
1180                     .dataFetcher(annotationNameDataFetcher)
1181                     .build()
1182             )
1183             .field(listFieldDefinition("targets", __("annotation.field.targets.description"),
1184                     annotatableInterface,
1185                     oneToManyRelationshipFetcher(a -> a.as(Annotation.class).getTargets())))
1186             .field(listFieldDefinition("annotations", __("annotation.field.annotations.description"),
1187                     new GraphQLTypeReference(Entities.ANNOTATION),
1188                     oneToManyRelationshipFetcher(r -> r.as(Annotatable.class).getAnnotations())))
1189             .field(itemEventsFieldDefinition())
1190             .withInterfaces(entityInterface, annotatableInterface)
1191             .build();
1192 
1193     private final GraphQLObjectType linkType = newObject()
1194             .name(Entities.LINK)
1195             .description(__("link.description"))
1196             .fields(entityFields)
1197             .field(nullAttr(Ontology.LINK_HAS_DESCRIPTION, __("link.field.description.description")))
1198             .field(nullAttr(Ontology.LINK_HAS_FIELD, __("link.field.field.description")))
1199             .field(listFieldDefinition("targets", __("link.field.targets.description"), linkableInterface,
1200                     oneToManyRelationshipFetcher(a -> a.as(Link.class).getLinkTargets())))
1201             .field(listFieldDefinition("body", __("link.field.body.description"), accessPointType,
1202                     oneToManyRelationshipFetcher(a -> a.as(Link.class).getLinkBodies())))
1203             .field(annotationsFieldDefinition)
1204             .field(datePeriodFieldDefinition)
1205             .field(itemEventsFieldDefinition())
1206             .withInterfaces(entityInterface, annotatableInterface)
1207             .build();
1208 
1209     private final GraphQLOutputType documentaryUnitsConnection = connectionType(
1210             new GraphQLTypeReference(Entities.DOCUMENTARY_UNIT),"documentaryUnits");
1211 
1212     private final GraphQLOutputType repositoriesConnection = connectionType(
1213             new GraphQLTypeReference(Entities.REPOSITORY),"repositories");
1214 
1215     private final GraphQLOutputType countriesConnection = connectionType(
1216             new GraphQLTypeReference(Entities.COUNTRY),"countries");
1217 
1218     private final GraphQLOutputType historicalAgentsConnection = connectionType(
1219             new GraphQLTypeReference(Entities.HISTORICAL_AGENT),"historicalAgents");
1220 
1221     private final GraphQLOutputType authoritativeSetsConnection = connectionType(
1222             new GraphQLTypeReference(Entities.AUTHORITATIVE_SET),"authoritativeSets");
1223 
1224     private final GraphQLOutputType conceptsConnection = connectionType(
1225             new GraphQLTypeReference(Entities.CVOC_CONCEPT),"concepts");
1226 
1227     private final GraphQLOutputType vocabulariesConnection = connectionType(
1228             new GraphQLTypeReference(Entities.CVOC_VOCABULARY),"vocabularies");
1229 
1230     private final GraphQLOutputType annotationsConnection = connectionType(
1231             new GraphQLTypeReference(Entities.ANNOTATION),"annotations");
1232 
1233     private final GraphQLOutputType linksConnection = connectionType(
1234             new GraphQLTypeReference(Entities.LINK),"links");
1235 
1236     private GraphQLObjectType queryType() {
1237         return newObject()
1238                 .name("Root")
1239 
1240                 // Single item types...
1241                 .field(itemFieldDefinition(Entities.DOCUMENTARY_UNIT, __("root.single.documentaryUnit.description"),
1242                         documentaryUnitType, entityIdDataFetcher(Entities.DOCUMENTARY_UNIT), idArgument))
1243                 .field(itemFieldDefinition(Entities.REPOSITORY, __("root.single.repository.description"),
1244                         repositoryType, entityIdDataFetcher(Entities.REPOSITORY), idArgument))
1245                 .field(itemFieldDefinition(Entities.COUNTRY, __("root.single.country.description"),
1246                         countryType, entityIdDataFetcher(Entities.COUNTRY), idArgument))
1247                 .field(itemFieldDefinition(Entities.HISTORICAL_AGENT, __("root.single.historicalAgent.description"),
1248                         historicalAgentType, entityIdDataFetcher(Entities.HISTORICAL_AGENT), idArgument))
1249                 .field(itemFieldDefinition(Entities.AUTHORITATIVE_SET, __("root.single.authoritativeSet.description"),
1250                         authoritativeSetType, entityIdDataFetcher(Entities.AUTHORITATIVE_SET), idArgument))
1251                 .field(itemFieldDefinition(Entities.CVOC_CONCEPT, __("root.single.cvocConcept.description"),
1252                         conceptType, entityIdDataFetcher(Entities.CVOC_CONCEPT), idArgument))
1253                 .field(itemFieldDefinition(Entities.CVOC_VOCABULARY, __("root.single.cvocVocabulary.description"),
1254                         vocabularyType, entityIdDataFetcher(Entities.CVOC_VOCABULARY), idArgument))
1255                 .field(itemFieldDefinition(Entities.ANNOTATION, __("root.single.annotation.description"),
1256                         annotationType, entityIdDataFetcher(Entities.ANNOTATION), idArgument))
1257                 .field(itemFieldDefinition(Entities.LINK, __("root.single.link.description"),
1258                         linkType, entityIdDataFetcher(Entities.LINK), idArgument))
1259 
1260                 // Top level item connections
1261                 .field(connectionFieldDefinition("documentaryUnits", __("root.connection.documentaryUnit.description"),
1262                         documentaryUnitsConnection,
1263                         entityTypeConnectionDataFetcher(EntityClass.DOCUMENTARY_UNIT)))
1264                 .field(connectionFieldDefinition("topLevelDocumentaryUnits", __("root.connection.documentaryUnit.topLevel.description"),
1265                         documentaryUnitsConnection,
1266                         topLevelDocDataFetcher()))
1267                 .field(connectionFieldDefinition("repositories", __("root.connection.repository.description"),
1268                         repositoriesConnection,
1269                         entityTypeConnectionDataFetcher(EntityClass.REPOSITORY)))
1270                 .field(connectionFieldDefinition("historicalAgents", __("root.connection.historicalAgent.description"),
1271                         historicalAgentsConnection,
1272                         entityTypeConnectionDataFetcher(EntityClass.HISTORICAL_AGENT)))
1273                 .field(connectionFieldDefinition("countries", __("root.connection.country.description"),
1274                         countriesConnection,
1275                         entityTypeConnectionDataFetcher(EntityClass.COUNTRY)))
1276                 .field(connectionFieldDefinition("authoritativeSets", __("root.connection.authoritativeSet.description"),
1277                         authoritativeSetsConnection,
1278                         entityTypeConnectionDataFetcher(EntityClass.AUTHORITATIVE_SET)))
1279                 .field(connectionFieldDefinition("concepts", __("root.connection.cvocConcept.description"),
1280                         conceptsConnection,
1281                         entityTypeConnectionDataFetcher(EntityClass.CVOC_CONCEPT)))
1282                 .field(connectionFieldDefinition("vocabularies", __("root.connection.cvocVocabulary.description"),
1283                         vocabulariesConnection,
1284                         entityTypeConnectionDataFetcher(EntityClass.CVOC_VOCABULARY)))
1285                 .field(connectionFieldDefinition("annotations", __("root.connection.annotation.description"),
1286                         annotationsConnection,
1287                         entityTypeConnectionDataFetcher(EntityClass.ANNOTATION)))
1288                 .field(connectionFieldDefinition("links", __("root.connection.link.description"),
1289                         linksConnection,
1290                         entityTypeConnectionDataFetcher(EntityClass.LINK)))
1291                 .build();
1292     }
1293 }