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