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