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