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