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.Utilities;
065
066public class GraphQLSchemaGenerator {
067
068  private static final Set<String> JSON_NUMBER_TYPES = new HashSet<String>() {{
069    add("decimal");
070    add("positiveInt");
071    add("unsignedInt");
072  }};
073  private final ProfileUtilities profileUtilities;
074  private final String version;
075  IWorkerContext context;
076
077  public GraphQLSchemaGenerator(IWorkerContext context, String version) {
078    super();
079    this.context = context;
080    this.version = version;
081    profileUtilities = new ProfileUtilities(context, null, null);
082  }
083
084  public void generateTypes(OutputStream stream) throws IOException, FHIRException {
085    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream));
086    generateTypes(writer);
087    writer.flush();
088    writer.close();
089  }
090
091  public void generateTypes(Writer writer) throws IOException {
092    EnumSet<FHIROperationType> operations = EnumSet.allOf(FHIROperationType.class);
093    generateTypes(writer, operations);
094  }
095
096  public void generateTypes(Writer writer, EnumSet<FHIROperationType> operations) throws IOException {
097    Map<String, StructureDefinition> pl = new HashMap<>();
098    Map<String, StructureDefinition> tl = new HashMap<>();
099    Map<String, String> existingTypeNames = new HashMap<>();
100    for (StructureDefinition sd : new ContextUtilities(context).allStructures()) {
101      if (sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) {
102        pl.put(sd.getName(), sd);
103      }
104      if (sd.getKind() == StructureDefinitionKind.COMPLEXTYPE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) {
105        tl.put(sd.getName(), sd);
106      }
107      if (sd.getKind() == StructureDefinitionKind.RESOURCE && sd.getDerivation() != TypeDerivationRule.CONSTRAINT && sd.getAbstract()) {
108        tl.put(sd.getName(), sd);
109      }
110    }
111    writer.write("# FHIR GraphQL Schema. Version " + version + "\r\n\r\n");
112    writer.write("# FHIR Defined Primitive types\r\n");
113    for (String n : sorted(pl.keySet()))
114      generatePrimitive(writer, pl.get(n));
115    writer.write("\r\n");
116    writer.write("# FHIR Defined Search Parameter Types\r\n");
117    for (SearchParamType dir : SearchParamType.values()) {
118      if (pl.containsKey(dir.toCode())) {
119        // Don't double create String and URI
120        continue;
121      }
122      if (dir != SearchParamType.NULL)
123        generateSearchParamType(writer, dir.toCode());
124    }
125    writer.write("\r\n");
126    generateElementBase(writer, operations);
127    for (String n : sorted(tl.keySet())) {
128      generateType(existingTypeNames, writer, tl.get(n), operations);
129    }
130  }
131
132  public void generateResource(OutputStream stream, StructureDefinition sd, List<SearchParameter> parameters, EnumSet<FHIROperationType> operations) throws IOException, FHIRException {
133    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream));
134    generateResource(writer, sd, parameters, operations);
135    writer.flush();
136    writer.close();
137  }
138
139  public void generateResource(Writer writer, StructureDefinition sd, List<SearchParameter> parameters, EnumSet<FHIROperationType> operations) throws IOException {
140    Map<String, String> existingTypeNames = new HashMap<>();
141
142    writer.write("# FHIR GraphQL Schema. Version " + version + "\r\n\r\n");
143    writer.write("# import * from 'types.graphql'\r\n\r\n");
144
145    generateType(existingTypeNames, writer, sd, operations);
146    if (operations.contains(FHIROperationType.READ))
147      generateIdAccess(writer, sd.getName());
148    if (operations.contains(FHIROperationType.SEARCH)) {
149      generateListAccess(writer, parameters, sd.getName());
150      generateConnectionAccess(writer, parameters, sd.getName());
151    }
152    if (operations.contains(FHIROperationType.CREATE))
153      generateCreate(writer, sd.getName());
154    if (operations.contains(FHIROperationType.UPDATE))
155      generateUpdate(writer, sd.getName());
156    if (operations.contains(FHIROperationType.DELETE))
157      generateDelete(writer, sd.getName());
158  }
159
160  private void generateCreate(Writer writer, String name) throws IOException {
161    writer.write("type " + name + "CreateType {\r\n");
162    writer.write("  " + name + "Create(");
163    param(writer, "resource", name + "Input", false, false);
164    writer.write("): " + name + "Creation\r\n");
165    writer.write("}\r\n");
166    writer.write("\r\n");
167    writer.write("type " + name + "Creation {\r\n");
168    writer.write("  location: String\r\n");
169    writer.write("  resource: " + name + "\r\n");
170    writer.write("  information: OperationOutcome\r\n");
171    writer.write("}\r\n");
172    writer.write("\r\n");
173  }
174
175  private void generateUpdate(Writer writer, String name) throws IOException {
176    writer.write("type " + name + "UpdateType {\r\n");
177    writer.write("  " + name + "Update(");
178    param(writer, "id", "ID", false, false);
179    writer.write(", ");
180    param(writer, "resource", name + "Input", false, false);
181    writer.write("): " + name + "Update\r\n");
182    writer.write("}\r\n");
183    writer.write("\r\n");
184    writer.write("type " + name + "Update {\r\n");
185    writer.write("  resource: " + name + "\r\n");
186    writer.write("  information: OperationOutcome\r\n");
187    writer.write("}\r\n");
188    writer.write("\r\n");
189  }
190
191  private void generateDelete(Writer writer, String name) throws IOException {
192    writer.write("type " + name + "DeleteType {\r\n");
193    writer.write("  " + name + "Delete(");
194    param(writer, "id", "ID", false, false);
195    writer.write("): " + name + "Delete\r\n");
196    writer.write("}\r\n");
197    writer.write("\r\n");
198    writer.write("type " + name + "Delete {\r\n");
199    writer.write("  information: OperationOutcome\r\n");
200    writer.write("}\r\n");
201    writer.write("\r\n");
202  }
203
204  private void generateListAccess(Writer writer, List<SearchParameter> parameters, String name) throws IOException {
205    writer.write("type " + name + "ListType {\r\n");
206    writer.write("  ");
207    generateListAccessQuery(writer, parameters, name);
208    writer.write("}\r\n");
209    writer.write("\r\n");
210  }
211
212  public void generateListAccessQuery(Writer writer, List<SearchParameter> parameters, String name) throws IOException {
213    writer.write(name + "List(");
214    param(writer, "_filter", "String", false, false);
215    for (SearchParameter sp : parameters)
216      param(writer, sp.getName().replace("-", "_"), getGqlname(requireNonNull(sp.getType().toCode())), true, true);
217    param(writer, "_sort", "String", false, true);
218    param(writer, "_count", "Int", false, true);
219    param(writer, "_cursor", "String", false, true);
220    writer.write("): [" + name + "]\r\n");
221  }
222
223  private void param(Writer writer, String name, String type, boolean list, boolean line) throws IOException {
224    if (line)
225      writer.write("\r\n    ");
226    writer.write(name);
227    writer.write(": ");
228    if (list)
229      writer.write("[");
230    writer.write(type);
231    if (list)
232      writer.write("]");
233  }
234
235  private void generateConnectionAccess(Writer writer, List<SearchParameter> parameters, String name) throws IOException {
236    writer.write("type " + name + "ConnectionType {\r\n");
237    writer.write("  ");
238    generateConnectionAccessQuery(writer, parameters, name);
239    writer.write("}\r\n");
240    writer.write("\r\n");
241    writer.write("type " + name + "Connection {\r\n");
242    writer.write("  count: Int\r\n");
243    writer.write("  offset: Int\r\n");
244    writer.write("  pagesize: Int\r\n");
245    writer.write("  first: ID\r\n");
246    writer.write("  previous: ID\r\n");
247    writer.write("  next: ID\r\n");
248    writer.write("  last: ID\r\n");
249    writer.write("  edges: [" + name + "Edge]\r\n");
250    writer.write("}\r\n");
251    writer.write("\r\n");
252    writer.write("type " + name + "Edge {\r\n");
253    writer.write("  mode: String\r\n");
254    writer.write("  score: Float\r\n");
255    writer.write("  resource: " + name + "\r\n");
256    writer.write("}\r\n");
257    writer.write("\r\n");
258  }
259
260  public void generateConnectionAccessQuery(Writer writer, List<SearchParameter> parameters, String name) throws IOException {
261    writer.write(name + "Conection(");
262    param(writer, "_filter", "String", false, false);
263    for (SearchParameter sp : parameters)
264      param(writer, sp.getName().replace("-", "_"), getGqlname(requireNonNull(sp.getType().toCode())), true, true);
265    param(writer, "_sort", "String", false, true);
266    param(writer, "_count", "Int", false, true);
267    param(writer, "_cursor", "String", false, true);
268    writer.write("): " + name + "Connection\r\n");
269  }
270
271  private void generateIdAccess(Writer writer, String name) throws IOException {
272    writer.write("type " + name + "ReadType {\r\n");
273    writer.write("  " + name + "(id: ID!): " + name + "\r\n");
274    writer.write("}\r\n");
275    writer.write("\r\n");
276  }
277
278  private void generateElementBase(Writer writer, EnumSet<FHIROperationType> operations) throws IOException {
279    if (operations.contains(FHIROperationType.READ) || operations.contains(FHIROperationType.SEARCH)) {
280      writer.write("interface IElement {\r\n");
281      writer.write("  id: String\r\n");
282      writer.write("  extension: [Extension]\r\n");
283      writer.write("}\r\n");
284      writer.write("\r\n");
285
286      writer.write("type ElementBase {\r\n");
287      writer.write("  id: String\r\n");
288      writer.write("  extension: [Extension]\r\n");
289      writer.write("}\r\n");
290      writer.write("\r\n");
291    }
292
293    if (operations.contains(FHIROperationType.CREATE) || operations.contains(FHIROperationType.UPDATE)) {
294      writer.write("input ElementBaseInput {\r\n");
295      writer.write("  id : ID\r\n");
296      writer.write("  extension: [ExtensionInput]\r\n");
297      writer.write("}\r\n");
298      writer.write("\r\n");
299    }
300  }
301
302  private void generateType(Map<String, String> existingTypeNames, Writer writer, StructureDefinition sd, EnumSet<FHIROperationType> operations) throws IOException {
303    if (operations.contains(FHIROperationType.READ) || operations.contains(FHIROperationType.SEARCH)) {
304      List<StringBuilder> list = new ArrayList<>();
305      StringBuilder b = new StringBuilder();
306      list.add(b);
307      b.append("interface ");
308      b.append("I" + sd.getName());
309      generateTypeSuperinterfaceDeclaration(sd, b);
310      b.append(" {\r\n");
311      ElementDefinition ed = sd.getSnapshot().getElementFirstRep();
312      generateProperties(existingTypeNames, list, b, sd.getName(), sd, ed, "type", "");
313      b.append("}");
314      b.append("\r\n");
315      b.append("\r\n");
316
317      b.append("type ");
318      b.append(sd.getName());
319      generateTypeSuperinterfaceDeclaration(sd, b);
320      b.append(" {\r\n");
321      generateProperties(existingTypeNames, list, b, sd.getName(), sd, ed, "type", "");
322      b.append("}");
323      b.append("\r\n");
324      b.append("\r\n");
325
326      for (StringBuilder bs : list) {
327        writer.write(bs.toString());
328      }
329      list.clear();
330    }
331
332    if (operations.contains(FHIROperationType.CREATE) || operations.contains(FHIROperationType.UPDATE)) {
333      List<StringBuilder> list = new ArrayList<>();
334      StringBuilder b = new StringBuilder();
335      list.add(b);
336      b.append("input ");
337      b.append(sd.getName());
338      b.append("Input {\r\n");
339      ElementDefinition ed = sd.getSnapshot().getElementFirstRep();
340      generateProperties(existingTypeNames, list, b, sd.getName(), sd, ed, "input", "Input");
341      b.append("}");
342      b.append("\r\n");
343      b.append("\r\n");
344      for (StringBuilder bs : list) {
345        writer.write(bs.toString());
346      }
347    }
348
349  }
350
351  private void generateTypeSuperinterfaceDeclaration(StructureDefinition theParentSd, StringBuilder theBuilder) {
352    StructureDefinition baseSd = theParentSd;
353    boolean first = true;
354    while (baseSd != null && baseSd.getBaseDefinition() != null) {
355      baseSd = context.fetchResource(StructureDefinition.class, baseSd.getBaseDefinition());
356      if (baseSd != null) {
357        if (first) {
358          theBuilder.append(" implements ");
359          first = false;
360        } else {
361          theBuilder.append(" & ");
362        }
363        theBuilder.append("I" + baseSd.getType());
364      }
365    }
366  }
367
368  private void generateProperties(Map<String, String> existingTypeNames, List<StringBuilder> list, StringBuilder b, String typeName, StructureDefinition sd, ElementDefinition ed, String mode, String suffix) throws IOException {
369    List<ElementDefinition> children = profileUtilities.getChildList(sd, ed);
370    for (ElementDefinition child : children) {
371      if (child.hasContentReference()) {
372        ElementDefinition ref = resolveContentReference(sd, child.getContentReference());
373        generateProperty(existingTypeNames, list, b, typeName, sd, child, ref.getType().get(0), false, ref, mode, suffix);
374      } else if (child.getType().size() == 1) {
375        generateProperty(existingTypeNames, list, b, typeName, sd, child, child.getType().get(0), false, null, mode, suffix);
376      } else {
377        boolean ref = false;
378        for (TypeRefComponent t : child.getType()) {
379          if (!t.hasTarget())
380            generateProperty(existingTypeNames, list, b, typeName, sd, child, t, true, null, mode, suffix);
381          else if (!ref) {
382            ref = true;
383            generateProperty(existingTypeNames, list, b, typeName, sd, child, t, true, null, mode, suffix);
384          }
385        }
386      }
387    }
388  }
389
390  private ElementDefinition resolveContentReference(StructureDefinition sd, String contentReference) {
391    String id = contentReference.substring(1);
392    for (ElementDefinition ed : sd.getSnapshot().getElement()) {
393      if (id.equals(ed.getId()))
394        return ed;
395    }
396    throw new Error("Unable to find " + id);
397  }
398
399  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 {
400    if (isPrimitive(typeDetails)) {
401      String n = getGqlname(typeDetails.getWorkingCode());
402      b.append("  ");
403      b.append(tail(child.getPath(), suffix));
404      if (suffix)
405        b.append(Utilities.capitalize(typeDetails.getWorkingCode()));
406      b.append(": ");
407      if (!child.getMax().equals("1")) {
408        b.append("[");
409        b.append(n);
410        b.append("]");
411      } else {
412        b.append(n);
413      }
414      if (!child.getPath().endsWith(".id")) {
415        b.append("  _");
416        b.append(tail(child.getPath(), suffix));
417        if (suffix)
418          b.append(Utilities.capitalize(typeDetails.getWorkingCode()));
419        if (!child.getMax().equals("1")) {
420          b.append(": [ElementBase");
421          b.append(suffixS);
422          b.append("]\r\n");
423        } else {
424          b.append(": ElementBase");
425          b.append(suffixS);
426          b.append("\r\n");
427        }
428      } else
429        b.append("\r\n");
430    } else {
431      b.append("  ");
432      b.append(tail(child.getPath(), suffix));
433      if (suffix)
434        b.append(Utilities.capitalize(typeDetails.getWorkingCode()));
435      b.append(": ");
436      if (!child.getMax().equals("1"))
437        b.append("[");
438      String type = typeDetails.getWorkingCode();
439      if (cr != null)
440        b.append(generateInnerType(existingTypeNames, list, sd, typeName, cr, mode, suffixS));
441      else if (Utilities.existsInList(type, "Element", "BackboneElement"))
442        b.append(generateInnerType(existingTypeNames, list, sd, typeName, child, mode, suffixS));
443      else
444        b.append(type).append(suffixS);
445      if (!child.getMax().equals("1"))
446        b.append("]");
447      if (child.getMin() != 0 && !suffix)
448        b.append("!");
449      b.append("\r\n");
450    }
451  }
452
453  private String generateInnerType(Map<String, String> existingTypeNames, List<StringBuilder> list, StructureDefinition sd, String name, ElementDefinition child, String mode, String suffix) throws IOException {
454    String key = child.getName() + "." + mode;
455    if (existingTypeNames.containsKey(key)) {
456      return existingTypeNames.get(key);
457    }
458
459    String typeName = name + Utilities.capitalize(tail(child.getPath(), false)) + suffix;
460    existingTypeNames.put(key, typeName + suffix);
461
462    StringBuilder b = new StringBuilder();
463    list.add(b);
464    b.append(mode);
465    b.append(" ");
466    b.append(typeName);
467    b.append(suffix);
468    b.append(" {\r\n");
469    generateProperties(existingTypeNames, list, b, typeName, sd, child, mode, suffix);
470    b.append("}");
471    b.append("\r\n");
472    b.append("\r\n");
473    return typeName + suffix;
474  }
475
476  private String tail(String path, boolean suffix) {
477    if (suffix)
478      path = path.substring(0, path.length() - 3);
479    int i = path.lastIndexOf(".");
480    return i < 0 ? path : path.substring(i + 1);
481  }
482
483  private boolean isPrimitive(TypeRefComponent type) {
484    String typeName = type.getWorkingCode();
485    StructureDefinition sd = context.fetchTypeDefinition(typeName);
486    if (sd == null)
487      return false;
488    return sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE;
489  }
490
491  private List<String> sorted(Set<String> keys) {
492    List<String> sl = new ArrayList<>(keys);
493    Collections.sort(sl);
494    return sl;
495  }
496
497  private void generatePrimitive(Writer writer, StructureDefinition sd) throws IOException, FHIRException {
498    String gqlName = getGqlname(sd.getName());
499    if (gqlName.equals(sd.getName())) {
500      writer.write("scalar ");
501      writer.write(sd.getName());
502      writer.write(" # JSON Format: ");
503      writer.write(getJsonFormat(sd));
504    } else {
505      writer.write("# Type ");
506      writer.write(sd.getName());
507      writer.write(": use GraphQL Scalar type ");
508      writer.write(gqlName);
509    }
510    writer.write("\r\n");
511  }
512
513  private void generateSearchParamType(Writer writer, String name) throws IOException, FHIRException {
514    String gqlName = getGqlname(name);
515    if (gqlName.equals("date")) {
516      writer.write("# Search Param ");
517      writer.write(name);
518      writer.write(": already defined as Primitive with JSON Format: string ");
519    } else if (gqlName.equals(name)) {
520      writer.write("scalar ");
521      writer.write(name);
522      writer.write(" # JSON Format: string");
523    } else {
524      writer.write("# Search Param ");
525      writer.write(name);
526      writer.write(": use GraphQL Scalar type ");
527      writer.write(gqlName);
528    }
529    writer.write("\r\n");
530  }
531
532  private String getJsonFormat(StructureDefinition sd) throws FHIRException {
533    for (ElementDefinition ed : sd.getSnapshot().getElement()) {
534      if (!ed.getType().isEmpty() && ed.getType().get(0).getCodeElement().hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type"))
535        return ed.getType().get(0).getCodeElement().getExtensionString(" http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type");
536    }
537    // all primitives but JSON_NUMBER_TYPES are represented as JSON strings
538    if (JSON_NUMBER_TYPES.contains(sd.getName())) {
539      return "number";
540    } else {
541      return "string";
542    }
543  }
544
545  private String getGqlname(String name) {
546    if (name.equals("string"))
547      return "String";
548    if (name.equals("integer"))
549      return "Int";
550    if (name.equals("boolean"))
551      return "Boolean";
552    if (name.equals("id"))
553      return "ID";
554    return name;
555  }
556
557  public enum FHIROperationType {READ, SEARCH, CREATE, UPDATE, DELETE}
558}