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