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