001package org.hl7.fhir.r4.utils;
002
003// todo:
004// - generate sort order parameters
005// - generate inherited search parameters
006
007import java.io.BufferedWriter;
008import java.io.IOException;
009import java.io.OutputStream;
010import java.io.OutputStreamWriter;
011import java.io.StringWriter;
012import java.util.ArrayList;
013import java.util.Collections;
014import java.util.EnumSet;
015import java.util.HashMap;
016import java.util.List;
017import java.util.Map;
018import java.util.Set;
019
020import org.hl7.fhir.exceptions.FHIRException;
021import org.hl7.fhir.r4.conformance.ProfileUtilities;
022import org.hl7.fhir.r4.context.IWorkerContext;
023import org.hl7.fhir.r4.model.Constants;
024import org.hl7.fhir.r4.model.ElementDefinition;
025import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent;
026import org.hl7.fhir.r4.model.Enumerations.SearchParamType;
027import org.hl7.fhir.r4.model.SearchParameter;
028import org.hl7.fhir.r4.model.StructureDefinition;
029import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind;
030import org.hl7.fhir.r4.model.StructureDefinition.TypeDerivationRule;
031import org.hl7.fhir.utilities.Utilities;
032
033public class GraphQLSchemaGenerator {
034
035  public enum FHIROperationType {READ, SEARCH, CREATE, UPDATE, DELETE};
036  
037  private static final String INNER_TYPE_NAME = "gql.type.name";
038  IWorkerContext context;
039
040  public GraphQLSchemaGenerator(IWorkerContext context) {
041    super();
042    this.context = context;
043  }
044  
045  public void generateTypes(OutputStream stream) throws IOException, FHIRException {
046    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream));
047    
048    Map<String, StructureDefinition> pl = new HashMap<String, StructureDefinition>();
049    Map<String, StructureDefinition> tl = new HashMap<String, StructureDefinition>();
050    for (StructureDefinition sd : context.allStructures()) {
051      if (sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) {
052        pl.put(sd.getName(), sd);
053      }
054      if (sd.getKind() == StructureDefinitionKind.COMPLEXTYPE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) {
055        tl.put(sd.getName(), sd);
056      }
057    }
058    writer.write("# FHIR GraphQL Schema. Version "+Constants.VERSION+"-"+Constants.REVISION+"\r\n\r\n");
059    writer.write("# FHIR Defined Primitive types\r\n");
060    for (String n : sorted(pl.keySet()))
061      generatePrimitive(writer, pl.get(n));
062    writer.write("\r\n");
063    writer.write("# FHIR Defined Search Parameter Types\r\n");
064    for (SearchParamType dir : SearchParamType.values()) {
065      if (dir != SearchParamType.NULL)
066        generateSearchParamType(writer, dir.toCode());      
067    }
068    writer.write("\r\n");
069    generateElementBase(writer);
070    for (String n : sorted(tl.keySet()))
071      generateType(writer, tl.get(n));
072    writer.flush();
073    writer.close();
074  }
075
076  public void generateResource(OutputStream stream, StructureDefinition sd, List<SearchParameter> parameters, EnumSet<FHIROperationType> operations) throws IOException, FHIRException {
077    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream));
078    writer.write("# FHIR GraphQL Schema. Version "+Constants.VERSION+"-"+Constants.REVISION+"\r\n\r\n");
079    generateType(writer, sd);
080    if (operations.contains(FHIROperationType.READ))
081      generateIdAccess(writer, sd.getName());
082    if (operations.contains(FHIROperationType.SEARCH)) {
083      generateListAccess(writer, parameters, sd.getName());
084      generateConnectionAccess(writer, parameters, sd.getName());
085    }
086    if (operations.contains(FHIROperationType.CREATE))
087      generateCreate(writer, sd.getName());
088    if (operations.contains(FHIROperationType.UPDATE))
089      generateUpdate(writer, sd.getName());
090    if (operations.contains(FHIROperationType.DELETE))
091      generateDelete(writer, sd.getName());
092    writer.flush();
093    writer.close();
094  }
095
096  private void generateCreate(BufferedWriter writer, String name) throws IOException {
097    writer.write("type "+name+"CreateType {\r\n");
098    writer.write("  "+name+"Create(");
099    param(writer, "resource", name, false, false);
100    writer.write(") : "+name+"Creation\r\n");
101    writer.write("}\r\n");
102    writer.write("\r\n");    
103    writer.write("type "+name+"Creation {\r\n");
104    writer.write("  location : String\r\n");
105    writer.write("  resource : "+name+"\r\n");
106    writer.write("  information : OperationOutcome\r\n");
107    writer.write("}\r\n");
108    writer.write("\r\n");    
109  }
110
111  private void generateUpdate(BufferedWriter writer, String name) throws IOException {
112    writer.write("type "+name+"UpdateType {\r\n");
113    writer.write("  "+name+"Update(");
114    param(writer, "id", "ID", false, false);
115    param(writer, "resource", name, false, false);
116    writer.write(") : "+name+"Update\r\n");
117    writer.write("}\r\n");
118    writer.write("\r\n");    
119    writer.write("type "+name+"Update {\r\n");
120    writer.write("  resource : "+name+"\r\n");
121    writer.write("  information : OperationOutcome\r\n");
122    writer.write("}\r\n");
123    writer.write("\r\n");    
124  }
125
126  private void generateDelete(BufferedWriter writer, String name) throws IOException {
127    writer.write("type "+name+"DeleteType {\r\n");
128    writer.write("  "+name+"Delete(");
129    param(writer, "id", "ID", false, false);
130    writer.write(") : "+name+"Delete\r\n");
131    writer.write("}\r\n");
132    writer.write("\r\n");    
133    writer.write("type "+name+"Delete {\r\n");
134    writer.write("  information : OperationOutcome\r\n");
135    writer.write("}\r\n");
136    writer.write("\r\n");    
137  }
138
139  private void generateListAccess(BufferedWriter writer, List<SearchParameter> parameters, String name) throws IOException {
140    writer.write("type "+name+"ListType {\r\n");
141    writer.write("  "+name+"List(");
142    param(writer, "_filter", "String", false, false);
143    for (SearchParameter sp : parameters)
144      param(writer, sp.getName().replace("-", "_"), getGqlname(sp.getType().toCode()), true, true);
145    param(writer, "_sort", "String", false, true);
146    param(writer, "_count", "Int", false, true);
147    param(writer, "_cursor", "String", false, true);
148    writer.write(") : ["+name+"]\r\n");
149    writer.write("}\r\n");
150    writer.write("\r\n");    
151  }
152
153  private void param(BufferedWriter writer, String name, String type, boolean list, boolean line) throws IOException {
154    if (line)
155      writer.write("\r\n    ");
156    writer.write(name);
157    writer.write(" : ");
158    if (list)
159      writer.write("[");
160    writer.write(type);      
161    if (list)
162      writer.write("]");
163  }
164
165  private void generateConnectionAccess(BufferedWriter writer, List<SearchParameter> parameters, String name) throws IOException {
166    writer.write("type "+name+"ConnectionType {\r\n");
167    writer.write("  "+name+"Conection(");
168    param(writer, "_filter", "String", false, false);
169    for (SearchParameter sp : parameters)
170      param(writer, sp.getName().replace("-", "_"), getGqlname(sp.getType().toCode()), true, true);
171    param(writer, "_sort", "String", false, true);
172    param(writer, "_count", "Int", false, true);
173    param(writer, "_cursor", "String", false, true);
174    writer.write(") : "+name+"Connection\r\n");
175    writer.write("}\r\n");
176    writer.write("\r\n");    
177    writer.write("type "+name+"Connection {\r\n");
178    writer.write("  count : Int\r\n");
179    writer.write("  offset : Int\r\n");
180    writer.write("  pagesize : Int\r\n");
181    writer.write("  first : ID\r\n");
182    writer.write("  previous : ID\r\n");
183    writer.write("  next : ID\r\n");
184    writer.write("  last : ID\r\n");
185    writer.write("  edges : ["+name+"Edge]\r\n");
186    writer.write("}\r\n");
187    writer.write("\r\n");    
188    writer.write("type "+name+"Edge {\r\n");
189    writer.write("  mode : String\r\n");
190    writer.write("  score : Float\r\n");
191    writer.write("  resource : "+name+"\r\n");
192    writer.write("}\r\n");
193    writer.write("\r\n");    
194  }
195
196  
197  private void generateIdAccess(BufferedWriter writer, String name) throws IOException {
198    writer.write("type "+name+"ReadType {\r\n");
199    writer.write("  "+name+"(id : ID!) : "+name+"\r\n");
200    writer.write("}\r\n");
201    writer.write("\r\n");    
202  }
203
204  private void generateElementBase(BufferedWriter writer) throws IOException {
205    writer.write("type ElementBase {\r\n");
206    writer.write("  id : ID\r\n");
207    writer.write("  extension : [Extension]{\r\n");
208    writer.write("}\r\n");
209    writer.write("\r\n");
210    
211  }
212
213  private void generateType(BufferedWriter writer, StructureDefinition sd) throws IOException {
214    if (sd.getAbstract())
215      return;
216    
217    List<StringBuilder> list = new ArrayList<StringBuilder>();
218    StringBuilder b = new StringBuilder();
219    list.add(b);
220    b.append("type ");
221    b.append(sd.getName());
222    b.append(" {\r\n");
223    ElementDefinition ed = sd.getSnapshot().getElementFirstRep();
224    generateProperties(list, b, sd.getName(), sd, ed, "type");
225    b.append("}");
226    b.append("\r\n");
227    b.append("\r\n");
228    for (StringBuilder bs : list)
229      writer.write(bs.toString());
230    list.clear();
231    b = new StringBuilder();
232    list.add(b);
233    b.append("input ");
234    b.append(sd.getName());
235    b.append(" {\r\n");
236    ed = sd.getSnapshot().getElementFirstRep();
237    generateProperties(list, b, sd.getName(), sd, ed, "input");
238    b.append("}");
239    b.append("\r\n");
240    b.append("\r\n");
241    for (StringBuilder bs : list)
242      writer.write(bs.toString());
243  }
244
245  private void generateProperties(List<StringBuilder> list, StringBuilder b, String typeName, StructureDefinition sd, ElementDefinition ed, String mode) throws IOException {
246    List<ElementDefinition> children = ProfileUtilities.getChildList(sd, ed);
247    for (ElementDefinition child : children) {
248      if (child.hasContentReference()) {
249        ElementDefinition ref = resolveContentReference(sd, child.getContentReference());        
250        generateProperty(list, b, typeName, sd, child, ref.getType().get(0), false, ref, mode);
251      } else if (child.getType().size() == 1) {
252        generateProperty(list, b, typeName, sd, child, child.getType().get(0), false, null, mode);
253      } else {
254        boolean ref  = false;
255        for (TypeRefComponent t : child.getType()) {
256          if (!t.hasTarget())
257            generateProperty(list, b, typeName, sd, child, t, true, null, mode);
258          else if (!ref) {
259            ref = true;
260            generateProperty(list, b, typeName, sd, child, t, true, null, mode);
261          }
262        }
263      }
264    }
265  }
266
267  private ElementDefinition resolveContentReference(StructureDefinition sd, String contentReference) {
268    String id = contentReference.substring(1);
269    for (ElementDefinition ed : sd.getSnapshot().getElement()) {
270      if (id.equals(ed.getId()))
271        return ed;
272    }
273    throw new Error("Unable to find "+id);
274  }
275
276  private void generateProperty(List<StringBuilder> list, StringBuilder b, String typeName, StructureDefinition sd, ElementDefinition child, TypeRefComponent typeDetails, boolean suffix, ElementDefinition cr, String mode) throws IOException {
277    if (isPrimitive(typeDetails)) {
278      String n = getGqlname(typeDetails.getCode()); 
279      b.append("  ");
280      b.append(tail(child.getPath(), suffix));
281      if (suffix)
282        b.append(Utilities.capitalize(typeDetails.getCode()));
283      b.append(": ");
284      b.append(n);
285      if (!child.getPath().endsWith(".id")) {
286        b.append("  _");
287        b.append(tail(child.getPath(), suffix));
288        if (suffix)
289          b.append(Utilities.capitalize(typeDetails.getCode()));
290        if (!child.getMax().equals("1"))
291          b.append(": [ElementBase]\r\n");
292        else
293          b.append(": ElementBase\r\n");
294      } else
295        b.append("\r\n");
296    } else {
297      b.append("  ");
298      b.append(tail(child.getPath(), suffix));
299      if (suffix)
300        b.append(Utilities.capitalize(typeDetails.getCode()));
301      b.append(": ");
302      if (!child.getMax().equals("1"))
303        b.append("[");
304      String type = typeDetails.getCode();
305      if (cr != null)
306        b.append(generateInnerType(list, sd, typeName, cr, mode));
307      else if (Utilities.existsInList(type, "Element", "BackboneElement"))
308        b.append(generateInnerType(list, sd, typeName, child, mode));
309      else
310        b.append(type);
311      if (!child.getMax().equals("1"))
312        b.append("]");
313      if (child.getMin() != 0 && !suffix)
314        b.append("!");
315      b.append("\r\n");
316    }
317  }
318
319  private String generateInnerType(List<StringBuilder> list, StructureDefinition sd, String name, ElementDefinition child, String mode) throws IOException {
320    if (child.hasUserData(INNER_TYPE_NAME+"."+mode))
321      return child.getUserString(INNER_TYPE_NAME+"."+mode);
322    
323    String typeName = name+Utilities.capitalize(tail(child.getPath(), false));
324    child.setUserData(INNER_TYPE_NAME+"."+mode, typeName);
325    StringBuilder b = new StringBuilder();
326    list.add(b);
327    b.append(mode);
328    b.append(" ");
329    b.append(typeName);
330    b.append(" {\r\n");
331    generateProperties(list, b, typeName, sd, child, mode);
332    b.append("}");
333    b.append("\r\n");
334    b.append("\r\n");
335    return typeName;
336  }
337
338  private String tail(String path, boolean suffix) {
339    if (suffix)
340      path = path.substring(0, path.length()-3);
341    int i = path.lastIndexOf(".");
342    return i < 0 ? path : path.substring(i + 1);
343  }
344
345  private boolean isPrimitive(TypeRefComponent type) {
346    String typeName = type.getCode();
347    StructureDefinition sd = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/"+typeName);
348    if (sd == null)
349      return false;
350    return sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE;
351  }
352
353  private List<String> sorted(Set<String> keys) {
354    List<String> sl = new ArrayList<>();
355    sl.addAll(keys);
356    Collections.sort(sl);
357    return sl;
358  }
359
360  private void generatePrimitive(BufferedWriter writer, StructureDefinition sd) throws IOException, FHIRException {
361    String gqlName = getGqlname(sd.getName());
362    if (gqlName.equals(sd.getName())) { 
363      writer.write("Scalar ");
364      writer.write(sd.getName());
365      writer.write(" # JSON Format: ");
366      writer.write(getJsonFormat(sd));
367    } else  {
368      writer.write("# Type ");
369      writer.write(sd.getName());
370      writer.write(": use GraphQL Scalar type ");
371      writer.write(gqlName);
372    }
373    writer.write("\r\n");
374  }
375
376  private void generateSearchParamType(BufferedWriter writer, String name) throws IOException, FHIRException {
377    String gqlName = getGqlname(name);
378    if (gqlName.equals(name)) { 
379      writer.write("Scalar ");
380      writer.write(name);
381      writer.write(" # JSON Format: String");
382    } else  {
383      writer.write("# Search Param ");
384      writer.write(name);
385      writer.write(": use GraphQL Scalar type ");
386      writer.write(gqlName);
387    }
388    writer.write("\r\n");
389  }
390  
391  private String getJsonFormat(StructureDefinition sd) throws FHIRException {
392    for (ElementDefinition ed : sd.getSnapshot().getElement()) {
393      if (!ed.getType().isEmpty() &&  ed.getType().get(0).getCodeElement().hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type"))
394        return ed.getType().get(0).getCodeElement().getExtensionString("http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type");
395    }
396    return "??";
397  }
398
399  private String getGqlname(String name) {
400    if (name.equals("string"))
401      return "String";
402    if (name.equals("integer"))
403      return "Int";
404    if (name.equals("boolean"))
405      return "Boolean";
406    if (name.equals("id"))
407      return "ID";    
408    return name;
409  }
410}