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