001package org.hl7.fhir.r5.utils;
002
003import static java.util.Objects.requireNonNull;
004
005import java.io.BufferedWriter;
006import java.io.IOException;
007import java.io.OutputStream;
008import java.io.OutputStreamWriter;
009import java.io.Writer;
010import java.util.ArrayList;
011import java.util.Collections;
012import java.util.EnumSet;
013import java.util.HashMap;
014import java.util.HashSet;
015import java.util.List;
016import java.util.Map;
017import java.util.Set;
018
019/*
020  Copyright (c) 2011+, HL7, Inc.
021  All rights reserved.
022  
023  Redistribution and use in source and binary forms, with or without modification, 
024  are permitted provided that the following conditions are met:
025    
026   * Redistributions of source code must retain the above copyright notice, this 
027     list of conditions and the following disclaimer.
028   * Redistributions in binary form must reproduce the above copyright notice, 
029     this list of conditions and the following disclaimer in the documentation 
030     and/or other materials provided with the distribution.
031   * Neither the name of HL7 nor the names of its contributors may be used to 
032     endorse or promote products derived from this software without specific 
033     prior written permission.
034  
035  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
036  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
037  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
038  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
039  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
040  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
041  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
042  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
043  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
044  POSSIBILITY OF SUCH DAMAGE.
045  
046 */
047
048
049// todo:
050// - generate sort order parameters
051// - generate inherited search parameters
052
053import org.hl7.fhir.exceptions.FHIRException;
054import org.hl7.fhir.r5.conformance.profile.ProfileUtilities;
055import org.hl7.fhir.r5.context.ContextUtilities;
056import org.hl7.fhir.r5.context.IWorkerContext;
057import org.hl7.fhir.r5.model.ElementDefinition;
058import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent;
059import org.hl7.fhir.r5.model.Enumerations.SearchParamType;
060import org.hl7.fhir.r5.model.SearchParameter;
061import org.hl7.fhir.r5.model.StructureDefinition;
062import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind;
063import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
064import org.hl7.fhir.utilities.MarkedToMoveToAdjunctPackage;
065import org.hl7.fhir.utilities.Utilities;
066
067@MarkedToMoveToAdjunctPackage
068public class GraphQLSchemaGenerator {
069
070  private static final Set<String> JSON_NUMBER_TYPES = new HashSet<String>() {{
071    add("decimal");
072    add("positiveInt");
073    add("unsignedInt");
074  }};
075  private final ProfileUtilities profileUtilities;
076  private final String version;
077  IWorkerContext context;
078
079  public GraphQLSchemaGenerator(IWorkerContext context, String version) {
080    super();
081    this.context = context;
082    this.version = version;
083    profileUtilities = new ProfileUtilities(context, null, null);
084  }
085
086  public void generateTypes(OutputStream stream) throws IOException, FHIRException {
087    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream));
088    generateTypes(writer);
089    writer.flush();
090    writer.close();
091  }
092
093  public void generateTypes(Writer writer) throws IOException {
094    EnumSet<FHIROperationType> operations = EnumSet.allOf(FHIROperationType.class);
095    generateTypes(writer, operations);
096  }
097
098  public void generateTypes(Writer writer, EnumSet<FHIROperationType> operations) throws IOException {
099    Map<String, StructureDefinition> pl = new HashMap<>();
100    Map<String, StructureDefinition> tl = new HashMap<>();
101    Map<String, String> existingTypeNames = new HashMap<>();
102    for (StructureDefinition sd : new ContextUtilities(context).allStructures()) {
103      if (sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) {
104        pl.put(sd.getName(), sd);
105      }
106      if (sd.getKind() == StructureDefinitionKind.COMPLEXTYPE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) {
107        tl.put(sd.getName(), sd);
108      }
109      if (sd.getKind() == StructureDefinitionKind.RESOURCE && sd.getDerivation() != TypeDerivationRule.CONSTRAINT && sd.getAbstract()) {
110        tl.put(sd.getName(), sd);
111      }
112    }
113    writer.write("# FHIR GraphQL Schema. Version " + version + "\r\n\r\n");
114    writer.write("# FHIR Defined Primitive types\r\n");
115    for (String n : sorted(pl.keySet()))
116      generatePrimitive(writer, pl.get(n));
117    writer.write("\r\n");
118    writer.write("# FHIR Defined Search Parameter Types\r\n");
119    for (SearchParamType dir : SearchParamType.values()) {
120      if (pl.containsKey(dir.toCode())) {
121        // Don't double create String and URI
122        continue;
123      }
124      if (dir != SearchParamType.NULL)
125        generateSearchParamType(writer, dir.toCode());
126    }
127    writer.write("\r\n");
128    generateElementBase(writer, operations);
129    for (String n : sorted(tl.keySet())) {
130      generateType(existingTypeNames, writer, tl.get(n), operations);
131    }
132  }
133
134  public void generateResource(OutputStream stream, StructureDefinition sd, List<SearchParameter> parameters, EnumSet<FHIROperationType> operations) throws IOException, FHIRException {
135    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream));
136    generateResource(writer, sd, parameters, operations);
137    writer.flush();
138    writer.close();
139  }
140
141  public void generateResource(Writer writer, StructureDefinition sd, List<SearchParameter> parameters, EnumSet<FHIROperationType> operations) throws IOException {
142    Map<String, String> existingTypeNames = new HashMap<>();
143
144    writer.write("# FHIR GraphQL Schema. Version " + version + "\r\n\r\n");
145    writer.write("# import * from 'types.graphql'\r\n\r\n");
146
147    generateType(existingTypeNames, writer, sd, operations);
148    if (operations.contains(FHIROperationType.READ))
149      generateIdAccess(writer, sd.getName());
150    if (operations.contains(FHIROperationType.SEARCH)) {
151      generateListAccess(writer, parameters, sd.getName());
152      generateConnectionAccess(writer, parameters, sd.getName());
153    }
154    if (operations.contains(FHIROperationType.CREATE))
155      generateCreate(writer, sd.getName());
156    if (operations.contains(FHIROperationType.UPDATE))
157      generateUpdate(writer, sd.getName());
158    if (operations.contains(FHIROperationType.DELETE))
159      generateDelete(writer, sd.getName());
160  }
161
162  private void generateCreate(Writer writer, String name) throws IOException {
163    writer.write("type " + name + "CreateType {\r\n");
164    writer.write("  " + name + "Create(");
165    param(writer, "resource", name + "Input", false, false);
166    writer.write("): " + name + "Creation\r\n");
167    writer.write("}\r\n");
168    writer.write("\r\n");
169    writer.write("type " + name + "Creation {\r\n");
170    writer.write("  location: String\r\n");
171    writer.write("  resource: " + name + "\r\n");
172    writer.write("  information: OperationOutcome\r\n");
173    writer.write("}\r\n");
174    writer.write("\r\n");
175  }
176
177  private void generateUpdate(Writer writer, String name) throws IOException {
178    writer.write("type " + name + "UpdateType {\r\n");
179    writer.write("  " + name + "Update(");
180    param(writer, "id", "ID", false, false);
181    writer.write(", ");
182    param(writer, "resource", name + "Input", false, false);
183    writer.write("): " + name + "Update\r\n");
184    writer.write("}\r\n");
185    writer.write("\r\n");
186    writer.write("type " + name + "Update {\r\n");
187    writer.write("  resource: " + name + "\r\n");
188    writer.write("  information: OperationOutcome\r\n");
189    writer.write("}\r\n");
190    writer.write("\r\n");
191  }
192
193  private void generateDelete(Writer writer, String name) throws IOException {
194    writer.write("type " + name + "DeleteType {\r\n");
195    writer.write("  " + name + "Delete(");
196    param(writer, "id", "ID", false, false);
197    writer.write("): " + name + "Delete\r\n");
198    writer.write("}\r\n");
199    writer.write("\r\n");
200    writer.write("type " + name + "Delete {\r\n");
201    writer.write("  information: OperationOutcome\r\n");
202    writer.write("}\r\n");
203    writer.write("\r\n");
204  }
205
206  private void generateListAccess(Writer writer, List<SearchParameter> parameters, String name) throws IOException {
207    writer.write("type " + name + "ListType {\r\n");
208    writer.write("  ");
209    generateListAccessQuery(writer, parameters, name);
210    writer.write("}\r\n");
211    writer.write("\r\n");
212  }
213
214  public void generateListAccessQuery(Writer writer, List<SearchParameter> parameters, String name) throws IOException {
215    writer.write(name + "List(");
216    param(writer, "_filter", "String", false, false);
217    for (SearchParameter sp : parameters)
218      param(writer, sp.getName().replace("-", "_"), getGqlname(requireNonNull(sp.getType().toCode())), true, true);
219    param(writer, "_sort", "String", false, true);
220    param(writer, "_count", "Int", false, true);
221    param(writer, "_cursor", "String", false, true);
222    writer.write("): [" + name + "]\r\n");
223  }
224
225  private void param(Writer writer, String name, String type, boolean list, boolean line) throws IOException {
226    if (line)
227      writer.write("\r\n    ");
228    writer.write(name);
229    writer.write(": ");
230    if (list)
231      writer.write("[");
232    writer.write(type);
233    if (list)
234      writer.write("]");
235  }
236
237  private void generateConnectionAccess(Writer writer, List<SearchParameter> parameters, String name) throws IOException {
238    writer.write("type " + name + "ConnectionType {\r\n");
239    writer.write("  ");
240    generateConnectionAccessQuery(writer, parameters, name);
241    writer.write("}\r\n");
242    writer.write("\r\n");
243    writer.write("type " + name + "Connection {\r\n");
244    writer.write("  count: Int\r\n");
245    writer.write("  offset: Int\r\n");
246    writer.write("  pagesize: Int\r\n");
247    writer.write("  first: ID\r\n");
248    writer.write("  previous: ID\r\n");
249    writer.write("  next: ID\r\n");
250    writer.write("  last: ID\r\n");
251    writer.write("  edges: [" + name + "Edge]\r\n");
252    writer.write("}\r\n");
253    writer.write("\r\n");
254    writer.write("type " + name + "Edge {\r\n");
255    writer.write("  mode: String\r\n");
256    writer.write("  score: Float\r\n");
257    writer.write("  resource: " + name + "\r\n");
258    writer.write("}\r\n");
259    writer.write("\r\n");
260  }
261
262  public void generateConnectionAccessQuery(Writer writer, List<SearchParameter> parameters, String name) throws IOException {
263    writer.write(name + "Conection(");
264    param(writer, "_filter", "String", false, false);
265    for (SearchParameter sp : parameters)
266      param(writer, sp.getName().replace("-", "_"), getGqlname(requireNonNull(sp.getType().toCode())), true, true);
267    param(writer, "_sort", "String", false, true);
268    param(writer, "_count", "Int", false, true);
269    param(writer, "_cursor", "String", false, true);
270    writer.write("): " + name + "Connection\r\n");
271  }
272
273  private void generateIdAccess(Writer writer, String name) throws IOException {
274    writer.write("type " + name + "ReadType {\r\n");
275    writer.write("  " + name + "(id: ID!): " + name + "\r\n");
276    writer.write("}\r\n");
277    writer.write("\r\n");
278  }
279
280  private void generateElementBase(Writer writer, EnumSet<FHIROperationType> operations) throws IOException {
281    if (operations.contains(FHIROperationType.READ) || operations.contains(FHIROperationType.SEARCH)) {
282      writer.write("interface IElement {\r\n");
283      writer.write("  id: String\r\n");
284      writer.write("  extension: [Extension]\r\n");
285      writer.write("}\r\n");
286      writer.write("\r\n");
287
288      writer.write("type ElementBase {\r\n");
289      writer.write("  id: String\r\n");
290      writer.write("  extension: [Extension]\r\n");
291      writer.write("}\r\n");
292      writer.write("\r\n");
293    }
294
295    if (operations.contains(FHIROperationType.CREATE) || operations.contains(FHIROperationType.UPDATE)) {
296      writer.write("input ElementBaseInput {\r\n");
297      writer.write("  id : ID\r\n");
298      writer.write("  extension: [ExtensionInput]\r\n");
299      writer.write("}\r\n");
300      writer.write("\r\n");
301    }
302  }
303
304  private void generateType(Map<String, String> existingTypeNames, Writer writer, StructureDefinition sd, EnumSet<FHIROperationType> operations) throws IOException {
305    if (operations.contains(FHIROperationType.READ) || operations.contains(FHIROperationType.SEARCH)) {
306      List<StringBuilder> list = new ArrayList<>();
307      StringBuilder b = new StringBuilder();
308      list.add(b);
309      b.append("interface ");
310      b.append("I" + sd.getName());
311      generateTypeSuperinterfaceDeclaration(sd, b);
312      b.append(" {\r\n");
313      ElementDefinition ed = sd.getSnapshot().getElementFirstRep();
314      generateProperties(existingTypeNames, list, b, sd.getName(), sd, ed, "type", "");
315      b.append("}");
316      b.append("\r\n");
317      b.append("\r\n");
318
319      b.append("type ");
320      b.append(sd.getName());
321      generateTypeSuperinterfaceDeclaration(sd, b);
322      b.append(" {\r\n");
323      generateProperties(existingTypeNames, list, b, sd.getName(), sd, ed, "type", "");
324      b.append("}");
325      b.append("\r\n");
326      b.append("\r\n");
327
328      for (StringBuilder bs : list) {
329        writer.write(bs.toString());
330      }
331      list.clear();
332    }
333
334    if (operations.contains(FHIROperationType.CREATE) || operations.contains(FHIROperationType.UPDATE)) {
335      List<StringBuilder> list = new ArrayList<>();
336      StringBuilder b = new StringBuilder();
337      list.add(b);
338      b.append("input ");
339      b.append(sd.getName());
340      b.append("Input {\r\n");
341      ElementDefinition ed = sd.getSnapshot().getElementFirstRep();
342      generateProperties(existingTypeNames, list, b, sd.getName(), sd, ed, "input", "Input");
343      b.append("}");
344      b.append("\r\n");
345      b.append("\r\n");
346      for (StringBuilder bs : list) {
347        writer.write(bs.toString());
348      }
349    }
350
351  }
352
353  private void generateTypeSuperinterfaceDeclaration(StructureDefinition theParentSd, StringBuilder theBuilder) {
354    StructureDefinition baseSd = theParentSd;
355    boolean first = true;
356    while (baseSd != null && baseSd.getBaseDefinition() != null) {
357      baseSd = context.fetchResource(StructureDefinition.class, baseSd.getBaseDefinition());
358      if (baseSd != null) {
359        if (first) {
360          theBuilder.append(" implements ");
361          first = false;
362        } else {
363          theBuilder.append(" & ");
364        }
365        theBuilder.append("I" + baseSd.getType());
366      }
367    }
368  }
369
370  private void generateProperties(Map<String, String> existingTypeNames, List<StringBuilder> list, StringBuilder b, String typeName, StructureDefinition sd, ElementDefinition ed, String mode, String suffix) throws IOException {
371    List<ElementDefinition> children = profileUtilities.getChildList(sd, ed);
372    for (ElementDefinition child : children) {
373      if (child.hasContentReference()) {
374        ElementDefinition ref = resolveContentReference(sd, child.getContentReference());
375        generateProperty(existingTypeNames, list, b, typeName, sd, child, ref.getType().get(0), false, ref, mode, suffix);
376      } else if (child.getType().size() == 1) {
377        generateProperty(existingTypeNames, list, b, typeName, sd, child, child.getType().get(0), false, null, mode, suffix);
378      } else {
379        boolean ref = false;
380        for (TypeRefComponent t : child.getType()) {
381          if (!t.hasTarget())
382            generateProperty(existingTypeNames, list, b, typeName, sd, child, t, true, null, mode, suffix);
383          else if (!ref) {
384            ref = true;
385            generateProperty(existingTypeNames, list, b, typeName, sd, child, t, true, null, mode, suffix);
386          }
387        }
388      }
389    }
390  }
391
392  private ElementDefinition resolveContentReference(StructureDefinition sd, String contentReference) {
393    String id = contentReference.substring(1);
394    for (ElementDefinition ed : sd.getSnapshot().getElement()) {
395      if (id.equals(ed.getId()))
396        return ed;
397    }
398    throw new Error("Unable to find " + id);
399  }
400
401  private void generateProperty(Map<String, String> existingTypeNames, List<StringBuilder> list, StringBuilder b, String typeName, StructureDefinition sd, ElementDefinition child, TypeRefComponent typeDetails, boolean suffix, ElementDefinition cr, String mode, String suffixS) throws IOException {
402    if (isPrimitive(typeDetails)) {
403      String n = getGqlname(typeDetails.getWorkingCode());
404      b.append("  ");
405      b.append(tail(child.getPath(), suffix));
406      if (suffix)
407        b.append(Utilities.capitalize(typeDetails.getWorkingCode()));
408      b.append(": ");
409      if (!child.getMax().equals("1")) {
410        b.append("[");
411        b.append(n);
412        b.append("]");
413      } else {
414        b.append(n);
415      }
416      if (!child.getPath().endsWith(".id")) {
417        b.append("  _");
418        b.append(tail(child.getPath(), suffix));
419        if (suffix)
420          b.append(Utilities.capitalize(typeDetails.getWorkingCode()));
421        if (!child.getMax().equals("1")) {
422          b.append(": [ElementBase");
423          b.append(suffixS);
424          b.append("]\r\n");
425        } else {
426          b.append(": ElementBase");
427          b.append(suffixS);
428          b.append("\r\n");
429        }
430      } else
431        b.append("\r\n");
432    } else {
433      b.append("  ");
434      b.append(tail(child.getPath(), suffix));
435      if (suffix)
436        b.append(Utilities.capitalize(typeDetails.getWorkingCode()));
437      b.append(": ");
438      if (!child.getMax().equals("1"))
439        b.append("[");
440      String type = typeDetails.getWorkingCode();
441      if (cr != null)
442        b.append(generateInnerType(existingTypeNames, list, sd, typeName, cr, mode, suffixS));
443      else if (Utilities.existsInList(type, "Element", "BackboneElement"))
444        b.append(generateInnerType(existingTypeNames, list, sd, typeName, child, mode, suffixS));
445      else
446        b.append(type).append(suffixS);
447      if (!child.getMax().equals("1"))
448        b.append("]");
449      if (child.getMin() != 0 && !suffix)
450        b.append("!");
451      b.append("\r\n");
452    }
453  }
454
455  private String generateInnerType(Map<String, String> existingTypeNames, List<StringBuilder> list, StructureDefinition sd, String name, ElementDefinition child, String mode, String suffix) throws IOException {
456    String key = child.getName() + "." + mode;
457    if (existingTypeNames.containsKey(key)) {
458      return existingTypeNames.get(key);
459    }
460
461    String typeName = name + Utilities.capitalize(tail(child.getPath(), false)) + suffix;
462    existingTypeNames.put(key, typeName + suffix);
463
464    StringBuilder b = new StringBuilder();
465    list.add(b);
466    b.append(mode);
467    b.append(" ");
468    b.append(typeName);
469    b.append(suffix);
470    b.append(" {\r\n");
471    generateProperties(existingTypeNames, list, b, typeName, sd, child, mode, suffix);
472    b.append("}");
473    b.append("\r\n");
474    b.append("\r\n");
475    return typeName + suffix;
476  }
477
478  private String tail(String path, boolean suffix) {
479    if (suffix)
480      path = path.substring(0, path.length() - 3);
481    int i = path.lastIndexOf(".");
482    return i < 0 ? path : path.substring(i + 1);
483  }
484
485  private boolean isPrimitive(TypeRefComponent type) {
486    String typeName = type.getWorkingCode();
487    StructureDefinition sd = context.fetchTypeDefinition(typeName);
488    if (sd == null)
489      return false;
490    return sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE;
491  }
492
493  private List<String> sorted(Set<String> keys) {
494    List<String> sl = new ArrayList<>(keys);
495    Collections.sort(sl);
496    return sl;
497  }
498
499  private void generatePrimitive(Writer writer, StructureDefinition sd) throws IOException, FHIRException {
500    String gqlName = getGqlname(sd.getName());
501    if (gqlName.equals(sd.getName())) {
502      writer.write("scalar ");
503      writer.write(sd.getName());
504      writer.write(" # JSON Format: ");
505      writer.write(getJsonFormat(sd));
506    } else {
507      writer.write("# Type ");
508      writer.write(sd.getName());
509      writer.write(": use GraphQL Scalar type ");
510      writer.write(gqlName);
511    }
512    writer.write("\r\n");
513  }
514
515  private void generateSearchParamType(Writer writer, String name) throws IOException, FHIRException {
516    String gqlName = getGqlname(name);
517    if (gqlName.equals("date")) {
518      writer.write("# Search Param ");
519      writer.write(name);
520      writer.write(": already defined as Primitive with JSON Format: string ");
521    } else if (gqlName.equals(name)) {
522      writer.write("scalar ");
523      writer.write(name);
524      writer.write(" # JSON Format: string");
525    } else {
526      writer.write("# Search Param ");
527      writer.write(name);
528      writer.write(": use GraphQL Scalar type ");
529      writer.write(gqlName);
530    }
531    writer.write("\r\n");
532  }
533
534  private String getJsonFormat(StructureDefinition sd) throws FHIRException {
535    for (ElementDefinition ed : sd.getSnapshot().getElement()) {
536      if (!ed.getType().isEmpty() && ed.getType().get(0).getCodeElement().hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type"))
537        return ed.getType().get(0).getCodeElement().getExtensionString(" http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type");
538    }
539    // all primitives but JSON_NUMBER_TYPES are represented as JSON strings
540    if (JSON_NUMBER_TYPES.contains(sd.getName())) {
541      return "number";
542    } else {
543      return "string";
544    }
545  }
546
547  private String getGqlname(String name) {
548    if (name.equals("string"))
549      return "String";
550    if (name.equals("integer"))
551      return "Int";
552    if (name.equals("boolean"))
553      return "Boolean";
554    if (name.equals("id"))
555      return "ID";
556    return name;
557  }
558
559  public enum FHIROperationType {READ, SEARCH, CREATE, UPDATE, DELETE}
560}