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