001package org.hl7.fhir.r5.utils; 002 003import static java.util.Objects.requireNonNull; 004 005import java.io.BufferedWriter; 006import java.io.IOException; 007import java.io.OutputStream; 008import java.io.OutputStreamWriter; 009import java.io.Writer; 010import java.util.ArrayList; 011import java.util.Collections; 012import java.util.EnumSet; 013import java.util.HashMap; 014import java.util.HashSet; 015import java.util.List; 016import java.util.Map; 017import java.util.Set; 018 019/* 020 Copyright (c) 2011+, HL7, Inc. 021 All rights reserved. 022 023 Redistribution and use in source and binary forms, with or without modification, 024 are permitted provided that the following conditions are met: 025 026 * Redistributions of source code must retain the above copyright notice, this 027 list of conditions and the following disclaimer. 028 * Redistributions in binary form must reproduce the above copyright notice, 029 this list of conditions and the following disclaimer in the documentation 030 and/or other materials provided with the distribution. 031 * Neither the name of HL7 nor the names of its contributors may be used to 032 endorse or promote products derived from this software without specific 033 prior written permission. 034 035 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 036 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 037 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 038 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 039 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 040 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 041 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 042 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 043 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 044 POSSIBILITY OF SUCH DAMAGE. 045 046 */ 047 048 049// todo: 050// - generate sort order parameters 051// - generate inherited search parameters 052 053import org.hl7.fhir.exceptions.FHIRException; 054import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; 055import org.hl7.fhir.r5.context.ContextUtilities; 056import org.hl7.fhir.r5.context.IWorkerContext; 057import org.hl7.fhir.r5.model.ElementDefinition; 058import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; 059import org.hl7.fhir.r5.model.Enumerations.SearchParamType; 060import org.hl7.fhir.r5.model.SearchParameter; 061import org.hl7.fhir.r5.model.StructureDefinition; 062import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; 063import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule; 064import org.hl7.fhir.utilities.Utilities; 065 066public class GraphQLSchemaGenerator { 067 068 private static final Set<String> JSON_NUMBER_TYPES = new HashSet<String>() {{ 069 add("decimal"); 070 add("positiveInt"); 071 add("unsignedInt"); 072 }}; 073 private final ProfileUtilities profileUtilities; 074 private final String version; 075 IWorkerContext context; 076 077 public GraphQLSchemaGenerator(IWorkerContext context, String version) { 078 super(); 079 this.context = context; 080 this.version = version; 081 profileUtilities = new ProfileUtilities(context, null, null); 082 } 083 084 public void generateTypes(OutputStream stream) throws IOException, FHIRException { 085 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream)); 086 generateTypes(writer); 087 writer.flush(); 088 writer.close(); 089 } 090 091 public void generateTypes(Writer writer) throws IOException { 092 EnumSet<FHIROperationType> operations = EnumSet.allOf(FHIROperationType.class); 093 generateTypes(writer, operations); 094 } 095 096 public void generateTypes(Writer writer, EnumSet<FHIROperationType> operations) throws IOException { 097 Map<String, StructureDefinition> pl = new HashMap<>(); 098 Map<String, StructureDefinition> tl = new HashMap<>(); 099 Map<String, String> existingTypeNames = new HashMap<>(); 100 for (StructureDefinition sd : new ContextUtilities(context).allStructures()) { 101 if (sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) { 102 pl.put(sd.getName(), sd); 103 } 104 if (sd.getKind() == StructureDefinitionKind.COMPLEXTYPE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) { 105 tl.put(sd.getName(), sd); 106 } 107 if (sd.getKind() == StructureDefinitionKind.RESOURCE && sd.getDerivation() != TypeDerivationRule.CONSTRAINT && sd.getAbstract()) { 108 tl.put(sd.getName(), sd); 109 } 110 } 111 writer.write("# FHIR GraphQL Schema. Version " + version + "\r\n\r\n"); 112 writer.write("# FHIR Defined Primitive types\r\n"); 113 for (String n : sorted(pl.keySet())) 114 generatePrimitive(writer, pl.get(n)); 115 writer.write("\r\n"); 116 writer.write("# FHIR Defined Search Parameter Types\r\n"); 117 for (SearchParamType dir : SearchParamType.values()) { 118 if (pl.containsKey(dir.toCode())) { 119 // Don't double create String and URI 120 continue; 121 } 122 if (dir != SearchParamType.NULL) 123 generateSearchParamType(writer, dir.toCode()); 124 } 125 writer.write("\r\n"); 126 generateElementBase(writer, operations); 127 for (String n : sorted(tl.keySet())) { 128 generateType(existingTypeNames, writer, tl.get(n), operations); 129 } 130 } 131 132 public void generateResource(OutputStream stream, StructureDefinition sd, List<SearchParameter> parameters, EnumSet<FHIROperationType> operations) throws IOException, FHIRException { 133 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream)); 134 generateResource(writer, sd, parameters, operations); 135 writer.flush(); 136 writer.close(); 137 } 138 139 public void generateResource(Writer writer, StructureDefinition sd, List<SearchParameter> parameters, EnumSet<FHIROperationType> operations) throws IOException { 140 Map<String, String> existingTypeNames = new HashMap<>(); 141 142 writer.write("# FHIR GraphQL Schema. Version " + version + "\r\n\r\n"); 143 writer.write("# import * from 'types.graphql'\r\n\r\n"); 144 145 generateType(existingTypeNames, writer, sd, operations); 146 if (operations.contains(FHIROperationType.READ)) 147 generateIdAccess(writer, sd.getName()); 148 if (operations.contains(FHIROperationType.SEARCH)) { 149 generateListAccess(writer, parameters, sd.getName()); 150 generateConnectionAccess(writer, parameters, sd.getName()); 151 } 152 if (operations.contains(FHIROperationType.CREATE)) 153 generateCreate(writer, sd.getName()); 154 if (operations.contains(FHIROperationType.UPDATE)) 155 generateUpdate(writer, sd.getName()); 156 if (operations.contains(FHIROperationType.DELETE)) 157 generateDelete(writer, sd.getName()); 158 } 159 160 private void generateCreate(Writer writer, String name) throws IOException { 161 writer.write("type " + name + "CreateType {\r\n"); 162 writer.write(" " + name + "Create("); 163 param(writer, "resource", name + "Input", false, false); 164 writer.write("): " + name + "Creation\r\n"); 165 writer.write("}\r\n"); 166 writer.write("\r\n"); 167 writer.write("type " + name + "Creation {\r\n"); 168 writer.write(" location: String\r\n"); 169 writer.write(" resource: " + name + "\r\n"); 170 writer.write(" information: OperationOutcome\r\n"); 171 writer.write("}\r\n"); 172 writer.write("\r\n"); 173 } 174 175 private void generateUpdate(Writer writer, String name) throws IOException { 176 writer.write("type " + name + "UpdateType {\r\n"); 177 writer.write(" " + name + "Update("); 178 param(writer, "id", "ID", false, false); 179 writer.write(", "); 180 param(writer, "resource", name + "Input", false, false); 181 writer.write("): " + name + "Update\r\n"); 182 writer.write("}\r\n"); 183 writer.write("\r\n"); 184 writer.write("type " + name + "Update {\r\n"); 185 writer.write(" resource: " + name + "\r\n"); 186 writer.write(" information: OperationOutcome\r\n"); 187 writer.write("}\r\n"); 188 writer.write("\r\n"); 189 } 190 191 private void generateDelete(Writer writer, String name) throws IOException { 192 writer.write("type " + name + "DeleteType {\r\n"); 193 writer.write(" " + name + "Delete("); 194 param(writer, "id", "ID", false, false); 195 writer.write("): " + name + "Delete\r\n"); 196 writer.write("}\r\n"); 197 writer.write("\r\n"); 198 writer.write("type " + name + "Delete {\r\n"); 199 writer.write(" information: OperationOutcome\r\n"); 200 writer.write("}\r\n"); 201 writer.write("\r\n"); 202 } 203 204 private void generateListAccess(Writer writer, List<SearchParameter> parameters, String name) throws IOException { 205 writer.write("type " + name + "ListType {\r\n"); 206 writer.write(" "); 207 generateListAccessQuery(writer, parameters, name); 208 writer.write("}\r\n"); 209 writer.write("\r\n"); 210 } 211 212 public void generateListAccessQuery(Writer writer, List<SearchParameter> parameters, String name) throws IOException { 213 writer.write(name + "List("); 214 param(writer, "_filter", "String", false, false); 215 for (SearchParameter sp : parameters) 216 param(writer, sp.getName().replace("-", "_"), getGqlname(requireNonNull(sp.getType().toCode())), true, true); 217 param(writer, "_sort", "String", false, true); 218 param(writer, "_count", "Int", false, true); 219 param(writer, "_cursor", "String", false, true); 220 writer.write("): [" + name + "]\r\n"); 221 } 222 223 private void param(Writer writer, String name, String type, boolean list, boolean line) throws IOException { 224 if (line) 225 writer.write("\r\n "); 226 writer.write(name); 227 writer.write(": "); 228 if (list) 229 writer.write("["); 230 writer.write(type); 231 if (list) 232 writer.write("]"); 233 } 234 235 private void generateConnectionAccess(Writer writer, List<SearchParameter> parameters, String name) throws IOException { 236 writer.write("type " + name + "ConnectionType {\r\n"); 237 writer.write(" "); 238 generateConnectionAccessQuery(writer, parameters, name); 239 writer.write("}\r\n"); 240 writer.write("\r\n"); 241 writer.write("type " + name + "Connection {\r\n"); 242 writer.write(" count: Int\r\n"); 243 writer.write(" offset: Int\r\n"); 244 writer.write(" pagesize: Int\r\n"); 245 writer.write(" first: ID\r\n"); 246 writer.write(" previous: ID\r\n"); 247 writer.write(" next: ID\r\n"); 248 writer.write(" last: ID\r\n"); 249 writer.write(" edges: [" + name + "Edge]\r\n"); 250 writer.write("}\r\n"); 251 writer.write("\r\n"); 252 writer.write("type " + name + "Edge {\r\n"); 253 writer.write(" mode: String\r\n"); 254 writer.write(" score: Float\r\n"); 255 writer.write(" resource: " + name + "\r\n"); 256 writer.write("}\r\n"); 257 writer.write("\r\n"); 258 } 259 260 public void generateConnectionAccessQuery(Writer writer, List<SearchParameter> parameters, String name) throws IOException { 261 writer.write(name + "Conection("); 262 param(writer, "_filter", "String", false, false); 263 for (SearchParameter sp : parameters) 264 param(writer, sp.getName().replace("-", "_"), getGqlname(requireNonNull(sp.getType().toCode())), true, true); 265 param(writer, "_sort", "String", false, true); 266 param(writer, "_count", "Int", false, true); 267 param(writer, "_cursor", "String", false, true); 268 writer.write("): " + name + "Connection\r\n"); 269 } 270 271 private void generateIdAccess(Writer writer, String name) throws IOException { 272 writer.write("type " + name + "ReadType {\r\n"); 273 writer.write(" " + name + "(id: ID!): " + name + "\r\n"); 274 writer.write("}\r\n"); 275 writer.write("\r\n"); 276 } 277 278 private void generateElementBase(Writer writer, EnumSet<FHIROperationType> operations) throws IOException { 279 if (operations.contains(FHIROperationType.READ) || operations.contains(FHIROperationType.SEARCH)) { 280 writer.write("interface IElement {\r\n"); 281 writer.write(" id: String\r\n"); 282 writer.write(" extension: [Extension]\r\n"); 283 writer.write("}\r\n"); 284 writer.write("\r\n"); 285 286 writer.write("type ElementBase {\r\n"); 287 writer.write(" id: String\r\n"); 288 writer.write(" extension: [Extension]\r\n"); 289 writer.write("}\r\n"); 290 writer.write("\r\n"); 291 } 292 293 if (operations.contains(FHIROperationType.CREATE) || operations.contains(FHIROperationType.UPDATE)) { 294 writer.write("input ElementBaseInput {\r\n"); 295 writer.write(" id : ID\r\n"); 296 writer.write(" extension: [ExtensionInput]\r\n"); 297 writer.write("}\r\n"); 298 writer.write("\r\n"); 299 } 300 } 301 302 private void generateType(Map<String, String> existingTypeNames, Writer writer, StructureDefinition sd, EnumSet<FHIROperationType> operations) throws IOException { 303 if (operations.contains(FHIROperationType.READ) || operations.contains(FHIROperationType.SEARCH)) { 304 List<StringBuilder> list = new ArrayList<>(); 305 StringBuilder b = new StringBuilder(); 306 list.add(b); 307 b.append("interface "); 308 b.append("I" + sd.getName()); 309 generateTypeSuperinterfaceDeclaration(sd, b); 310 b.append(" {\r\n"); 311 ElementDefinition ed = sd.getSnapshot().getElementFirstRep(); 312 generateProperties(existingTypeNames, list, b, sd.getName(), sd, ed, "type", ""); 313 b.append("}"); 314 b.append("\r\n"); 315 b.append("\r\n"); 316 317 b.append("type "); 318 b.append(sd.getName()); 319 generateTypeSuperinterfaceDeclaration(sd, b); 320 b.append(" {\r\n"); 321 generateProperties(existingTypeNames, list, b, sd.getName(), sd, ed, "type", ""); 322 b.append("}"); 323 b.append("\r\n"); 324 b.append("\r\n"); 325 326 for (StringBuilder bs : list) { 327 writer.write(bs.toString()); 328 } 329 list.clear(); 330 } 331 332 if (operations.contains(FHIROperationType.CREATE) || operations.contains(FHIROperationType.UPDATE)) { 333 List<StringBuilder> list = new ArrayList<>(); 334 StringBuilder b = new StringBuilder(); 335 list.add(b); 336 b.append("input "); 337 b.append(sd.getName()); 338 b.append("Input {\r\n"); 339 ElementDefinition ed = sd.getSnapshot().getElementFirstRep(); 340 generateProperties(existingTypeNames, list, b, sd.getName(), sd, ed, "input", "Input"); 341 b.append("}"); 342 b.append("\r\n"); 343 b.append("\r\n"); 344 for (StringBuilder bs : list) { 345 writer.write(bs.toString()); 346 } 347 } 348 349 } 350 351 private void generateTypeSuperinterfaceDeclaration(StructureDefinition theParentSd, StringBuilder theBuilder) { 352 StructureDefinition baseSd = theParentSd; 353 boolean first = true; 354 while (baseSd != null && baseSd.getBaseDefinition() != null) { 355 baseSd = context.fetchResource(StructureDefinition.class, baseSd.getBaseDefinition()); 356 if (baseSd != null) { 357 if (first) { 358 theBuilder.append(" implements "); 359 first = false; 360 } else { 361 theBuilder.append(" & "); 362 } 363 theBuilder.append("I" + baseSd.getType()); 364 } 365 } 366 } 367 368 private void generateProperties(Map<String, String> existingTypeNames, List<StringBuilder> list, StringBuilder b, String typeName, StructureDefinition sd, ElementDefinition ed, String mode, String suffix) throws IOException { 369 List<ElementDefinition> children = profileUtilities.getChildList(sd, ed); 370 for (ElementDefinition child : children) { 371 if (child.hasContentReference()) { 372 ElementDefinition ref = resolveContentReference(sd, child.getContentReference()); 373 generateProperty(existingTypeNames, list, b, typeName, sd, child, ref.getType().get(0), false, ref, mode, suffix); 374 } else if (child.getType().size() == 1) { 375 generateProperty(existingTypeNames, list, b, typeName, sd, child, child.getType().get(0), false, null, mode, suffix); 376 } else { 377 boolean ref = false; 378 for (TypeRefComponent t : child.getType()) { 379 if (!t.hasTarget()) 380 generateProperty(existingTypeNames, list, b, typeName, sd, child, t, true, null, mode, suffix); 381 else if (!ref) { 382 ref = true; 383 generateProperty(existingTypeNames, list, b, typeName, sd, child, t, true, null, mode, suffix); 384 } 385 } 386 } 387 } 388 } 389 390 private ElementDefinition resolveContentReference(StructureDefinition sd, String contentReference) { 391 String id = contentReference.substring(1); 392 for (ElementDefinition ed : sd.getSnapshot().getElement()) { 393 if (id.equals(ed.getId())) 394 return ed; 395 } 396 throw new Error("Unable to find " + id); 397 } 398 399 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 { 400 if (isPrimitive(typeDetails)) { 401 String n = getGqlname(typeDetails.getWorkingCode()); 402 b.append(" "); 403 b.append(tail(child.getPath(), suffix)); 404 if (suffix) 405 b.append(Utilities.capitalize(typeDetails.getWorkingCode())); 406 b.append(": "); 407 if (!child.getMax().equals("1")) { 408 b.append("["); 409 b.append(n); 410 b.append("]"); 411 } else { 412 b.append(n); 413 } 414 if (!child.getPath().endsWith(".id")) { 415 b.append(" _"); 416 b.append(tail(child.getPath(), suffix)); 417 if (suffix) 418 b.append(Utilities.capitalize(typeDetails.getWorkingCode())); 419 if (!child.getMax().equals("1")) { 420 b.append(": [ElementBase"); 421 b.append(suffixS); 422 b.append("]\r\n"); 423 } else { 424 b.append(": ElementBase"); 425 b.append(suffixS); 426 b.append("\r\n"); 427 } 428 } else 429 b.append("\r\n"); 430 } else { 431 b.append(" "); 432 b.append(tail(child.getPath(), suffix)); 433 if (suffix) 434 b.append(Utilities.capitalize(typeDetails.getWorkingCode())); 435 b.append(": "); 436 if (!child.getMax().equals("1")) 437 b.append("["); 438 String type = typeDetails.getWorkingCode(); 439 if (cr != null) 440 b.append(generateInnerType(existingTypeNames, list, sd, typeName, cr, mode, suffixS)); 441 else if (Utilities.existsInList(type, "Element", "BackboneElement")) 442 b.append(generateInnerType(existingTypeNames, list, sd, typeName, child, mode, suffixS)); 443 else 444 b.append(type).append(suffixS); 445 if (!child.getMax().equals("1")) 446 b.append("]"); 447 if (child.getMin() != 0 && !suffix) 448 b.append("!"); 449 b.append("\r\n"); 450 } 451 } 452 453 private String generateInnerType(Map<String, String> existingTypeNames, List<StringBuilder> list, StructureDefinition sd, String name, ElementDefinition child, String mode, String suffix) throws IOException { 454 String key = child.getName() + "." + mode; 455 if (existingTypeNames.containsKey(key)) { 456 return existingTypeNames.get(key); 457 } 458 459 String typeName = name + Utilities.capitalize(tail(child.getPath(), false)) + suffix; 460 existingTypeNames.put(key, typeName + suffix); 461 462 StringBuilder b = new StringBuilder(); 463 list.add(b); 464 b.append(mode); 465 b.append(" "); 466 b.append(typeName); 467 b.append(suffix); 468 b.append(" {\r\n"); 469 generateProperties(existingTypeNames, list, b, typeName, sd, child, mode, suffix); 470 b.append("}"); 471 b.append("\r\n"); 472 b.append("\r\n"); 473 return typeName + suffix; 474 } 475 476 private String tail(String path, boolean suffix) { 477 if (suffix) 478 path = path.substring(0, path.length() - 3); 479 int i = path.lastIndexOf("."); 480 return i < 0 ? path : path.substring(i + 1); 481 } 482 483 private boolean isPrimitive(TypeRefComponent type) { 484 String typeName = type.getWorkingCode(); 485 StructureDefinition sd = context.fetchTypeDefinition(typeName); 486 if (sd == null) 487 return false; 488 return sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE; 489 } 490 491 private List<String> sorted(Set<String> keys) { 492 List<String> sl = new ArrayList<>(keys); 493 Collections.sort(sl); 494 return sl; 495 } 496 497 private void generatePrimitive(Writer writer, StructureDefinition sd) throws IOException, FHIRException { 498 String gqlName = getGqlname(sd.getName()); 499 if (gqlName.equals(sd.getName())) { 500 writer.write("scalar "); 501 writer.write(sd.getName()); 502 writer.write(" # JSON Format: "); 503 writer.write(getJsonFormat(sd)); 504 } else { 505 writer.write("# Type "); 506 writer.write(sd.getName()); 507 writer.write(": use GraphQL Scalar type "); 508 writer.write(gqlName); 509 } 510 writer.write("\r\n"); 511 } 512 513 private void generateSearchParamType(Writer writer, String name) throws IOException, FHIRException { 514 String gqlName = getGqlname(name); 515 if (gqlName.equals("date")) { 516 writer.write("# Search Param "); 517 writer.write(name); 518 writer.write(": already defined as Primitive with JSON Format: string "); 519 } else if (gqlName.equals(name)) { 520 writer.write("scalar "); 521 writer.write(name); 522 writer.write(" # JSON Format: string"); 523 } else { 524 writer.write("# Search Param "); 525 writer.write(name); 526 writer.write(": use GraphQL Scalar type "); 527 writer.write(gqlName); 528 } 529 writer.write("\r\n"); 530 } 531 532 private String getJsonFormat(StructureDefinition sd) throws FHIRException { 533 for (ElementDefinition ed : sd.getSnapshot().getElement()) { 534 if (!ed.getType().isEmpty() && ed.getType().get(0).getCodeElement().hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type")) 535 return ed.getType().get(0).getCodeElement().getExtensionString(" http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type"); 536 } 537 // all primitives but JSON_NUMBER_TYPES are represented as JSON strings 538 if (JSON_NUMBER_TYPES.contains(sd.getName())) { 539 return "number"; 540 } else { 541 return "string"; 542 } 543 } 544 545 private String getGqlname(String name) { 546 if (name.equals("string")) 547 return "String"; 548 if (name.equals("integer")) 549 return "Int"; 550 if (name.equals("boolean")) 551 return "Boolean"; 552 if (name.equals("id")) 553 return "ID"; 554 return name; 555 } 556 557 public enum FHIROperationType {READ, SEARCH, CREATE, UPDATE, DELETE} 558}