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