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