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